diff --git a/.claude/commands/benchmark.md b/.claude/commands/benchmark.md new file mode 100644 index 0000000000000000000000000000000000000000..f4843f90bc1e8203af5fff825c80c88acc383270 --- /dev/null +++ b/.claude/commands/benchmark.md @@ -0,0 +1,61 @@ +Run a TAU-bench evaluation end-to-end and present results. + +**Syntax:** `/benchmark [mode] [extra-args]` + +**Arguments:** + +- `config` (from $ARGUMENTS, first word) — profile name: `haiku`, `sonnet`, `gpt4.1-mini`, `gpt4.1`, `fast`, `default` +- `mode` (from $ARGUMENTS, second word, optional) — `baseline` (default), `compare`, or `ace-only` +- `extra-args` (from $ARGUMENTS, remaining words) — forwarded verbatim to the CLI + +**Workflow:** + +1. **Parse arguments** from `$ARGUMENTS`: + - Split into: `config` (first word), `mode` (second word if it matches baseline/compare/ace-only, else default to baseline), and `extra-args` (the rest) + +2. **Build the command:** + ``` + uv run python scripts/run_tau_benchmark.py --config --save-detailed + ``` + Mode flags: + - `baseline` → `--skip-ace` + - `compare` → `--compare` + - `ace-only` → (no flag) + +3. **Show the command** to the user before running + +4. **Run the command** with a 10-minute timeout (TAU-bench runs are long) + +5. **Find the latest result**: list `tau_benchmark_results/` sorted by modification time, pick the newest `*_summary.json` + +6. **Read the summary JSON** and present results using this format: + + For baseline runs: + ``` + ## : (test split, k=) + + | Setting | Value | + |---------|-------| + | Model | | + | User LLM | | + | Domain | | + | Split | ( tasks) | + | Max steps | | + | Seed | | + + | Metric | Score | + |--------|-------| + | pass^1 | XX.XX% | + | pass^2 | XX.XX% | + | ... | ... | + ``` + + For comparison runs, add Baseline / ACE / Delta columns. + +**Examples:** +- `/benchmark haiku` → baseline haiku run +- `/benchmark haiku compare` → baseline vs ACE comparison +- `/benchmark fast` → quick smoke test (3 tasks, k=1) +- `/benchmark sonnet compare --domain retail` → sonnet comparison on retail + +**Key fields** to always include in the results table: exact model ID, user LLM, domain, split + task count, skillbook status, and all pass^k metrics. diff --git a/.claude/commands/checkout-branch.md b/.claude/commands/checkout-branch.md new file mode 100644 index 0000000000000000000000000000000000000000..208790eadaa8e4eeb3634e968089070e8c2bce33 --- /dev/null +++ b/.claude/commands/checkout-branch.md @@ -0,0 +1,64 @@ +Switch to an existing branch by checking out its worktree, or creating one if needed. + +**Arguments:** $ARGUMENTS should be the branch name (full or partial match) + +**Examples:** +- `/checkout-branch feature/john/add-caching` → switch to branch (create worktree if needed) +- `/checkout-branch add-caching` → partial match, resolve to full branch name +- `/checkout-branch fix` → if multiple matches, list them and ask to be specific + +**Steps:** +1. Parse branch name from arguments +2. Fetch latest from remote: `git fetch --prune` +3. Get all branches (local + remote): `git branch -a --format='%(refname:short)'` +4. Resolve branch name: + - Exact match: use directly + - Partial match: find branches containing the search term + - No match: show error with similar branches (if any) +5. Get worktree list: `git worktree list --porcelain` +6. Check if resolved branch has an existing worktree +7. If worktree exists: + - Show path and suggest `cd ` +8. If no worktree: + - Construct worktree path: `../` (replace all `/` with `-`) + - If remote-only branch (starts with `origin/`): `git worktree add -b ` + - If local branch: `git worktree add ` + - Symlink `.env`: if `/.env` exists (get main worktree from `git worktree list --porcelain | head -1`), run `ln -s /.env /.env` + - Show path and suggest `cd ` + +**On success (worktree exists), output:** +``` +✓ Branch already has worktree at: + +To switch to the worktree: + cd +``` + +**On success (worktree created from local branch), output:** +``` +✓ Created worktree: +✓ Linked .env → /.env + +To switch to the worktree: + cd +``` + +**On success (worktree created from remote branch), output:** +``` +✓ Created local branch: (tracking origin/) +✓ Created worktree: +✓ Linked .env → /.env + +To switch to the worktree: + cd +``` + +**Error handling:** +- Branch not found: "Branch not found: . Did you mean one of these?" (list similar branches) +- Multiple partial matches: "Multiple branches match '':" (list matches, ask to be more specific) +- No branches at all: "No branches found matching ''" + +**Branch resolution priority:** +1. Exact match on full branch name +2. Exact match on last segment (e.g., "add-caching" matches "feature/john/add-caching") +3. Partial substring match anywhere in branch name diff --git a/.claude/commands/create-branch.md b/.claude/commands/create-branch.md new file mode 100644 index 0000000000000000000000000000000000000000..f4d284fcbead2aa89ade6c5e989fd53702c212e7 --- /dev/null +++ b/.claude/commands/create-branch.md @@ -0,0 +1,39 @@ +Create a new git branch with an associated worktree following the project naming convention. + +**Format:** `//` +**Worktree:** `../` (sibling to current worktree) + +**Arguments:** $ARGUMENTS should be in format: ` ` + +**Examples:** +- `/create-branch feature add-caching` → branch: `feature//add-caching`, worktree: `../feature--add-caching` +- `/create-branch fix login-error` → branch: `fix//login-error`, worktree: `../fix--login-error` + +**Steps:** +1. Parse type and description from arguments (validate type is one of: feature, fix, docs, refactor, test, chore) +2. Get developer name from `git config user.name` (sanitize: lowercase, replace spaces with hyphens) +3. Construct branch name: `//` +4. Construct worktree path: `../--` (replace all `/` with `-`) +5. Create branch and worktree atomically: `git worktree add -b ` +6. Symlink `.env` from the main worktree into the new worktree: + - Get the main worktree path: `git worktree list --porcelain | head -1` (first `worktree` line) + - If `/.env` exists, create symlink: `ln -s /.env /.env` + - If `.env` doesn't exist in main worktree, skip silently +7. Report success with the created branch name and worktree path + +**Valid types:** feature, fix, docs, refactor, test, chore + +**On success, output:** +``` +✓ Created branch: +✓ Created worktree: +✓ Linked .env → /.env + +To switch to the new worktree: + cd +``` + +**Error handling:** +- If type is invalid, show valid types and abort +- If branch already exists, suggest checking it out instead +- If worktree path exists, suggest using existing worktree diff --git a/.claude/commands/create-pr.md b/.claude/commands/create-pr.md new file mode 100644 index 0000000000000000000000000000000000000000..9a94cc82f2c31a7855a1e54dc72e85117bdf9946 --- /dev/null +++ b/.claude/commands/create-pr.md @@ -0,0 +1,56 @@ +Create a pull request for the current branch against main. + +**Arguments:** $ARGUMENTS (optional) — base branch override (defaults to `main`) + +**Steps:** + +1. **Validate branch state:** + - Get current branch: `git branch --show-current` + - If on `main`, abort with error: "You're on main. Switch to a feature branch first." + - Determine base branch: use $ARGUMENTS if provided, otherwise `main` + +2. **Check for unmerged commits:** + - Run `git log ..HEAD --oneline` + - If no commits, abort: "No unmerged commits against . Nothing to PR." + +3. **Gather context (run in parallel):** + - `git diff ...HEAD --stat` — file change summary + - `git log ..HEAD --format='%h %s'` — commit list + - `git diff ...HEAD` — full diff for understanding changes + +4. **Draft PR title and body:** + - Analyze the commits and diff to understand the change + - Write a short PR title (under 70 chars, imperative mood) + - Write the body using this format: + ``` + ## Summary + <1-3 bullet points explaining the changes> + + ## Changes + + + ## Test plan + + ``` + +5. **Present draft to user:** + - Show the proposed title and body + - Ask: "Push and create this PR?" with options: Yes (create), Edit (let me revise), Cancel + +6. **On approval:** + - Check if branch has upstream: `git rev-parse --abbrev-ref @{upstream}` + - Push branch: `git push -u origin HEAD` + - Create PR: `gh pr create --title "" --body "<body>" --base <base>` + - Show the resulting PR URL + +**On success, output:** +``` +✓ Pushed branch: <branch-name> +✓ Created PR: <pr-url> +``` + +**Error handling:** +- If `gh` is not installed: "GitHub CLI (gh) is required. Install it: https://cli.github.com" +- If not authenticated: "Run `gh auth login` first." +- If PR already exists: show the existing PR URL with `gh pr view --web` +- If push fails: show the git error and abort diff --git a/.claude/commands/finalize.md b/.claude/commands/finalize.md new file mode 100644 index 0000000000000000000000000000000000000000..4742ede006c20fb1e39252d10a0bd68d511f093d --- /dev/null +++ b/.claude/commands/finalize.md @@ -0,0 +1,79 @@ +Finalize the current work: format, test, fix, commit, and push. + +Run this command after making code changes to complete the development cycle. It handles formatting, testing (with auto-fix retries), committing, and pushing. + +**Arguments:** $ARGUMENTS is an optional commit message override. If not provided, compose one automatically from the diff. + +**Workflow:** + +1. **Format code** + - Run `uv run black ace/ tests/ examples/` + +2. **Update CLAUDE.md** + - Run `/init` to update the project's CLAUDE.md file with current codebase context + +3. **Review test coverage** + - Check if the changes have adequate test coverage + - Look at which lines/branches are untested for the modified files + - If coverage gaps exist for the changed code, add targeted tests before proceeding + - Focus on: new functions, error paths, edge cases, and branches introduced by this changeset + +4. **Run tests** + - Run `uv run pytest` + +5. **Test-fix loop (max 3 retries)** + - If tests pass, continue to step 6 + - If tests fail: + - Analyze the failure output + - Fix the failing code or tests + - Add missing tests if the failures reveal gaps + - Re-run formatter: `uv run black ace/ tests/ examples/` + - Re-run tests: `uv run pytest` + - If tests still fail after 3 total attempts, **stop entirely** and report the failures. Never commit broken code. + +6. **Security review** + - Run `/security-review` to scan changed code for vulnerabilities + - If Critical or High severity issues are found, fix them before proceeding + - After fixing, re-run formatter (`uv run black ace/ tests/ examples/`) and tests (`uv run pytest`) + - If issues can't be auto-fixed, report them and stop + +7. **Review changes** + - Run `git diff` to review all changes + - Run `git status` to see untracked/modified files + - Determine which files to stage + +8. **Stage files selectively** + - Use explicit `git add <file>` for each file. **Never use `git add -A` or `git add .`** + - **Never stage:** `.env`, credentials, secrets, `__pycache__/`, `*.pyc`, large binaries, `.DS_Store` + - **Only stage `uv.lock`** if dependency changes in `pyproject.toml` were intentional + - If unsure about a file, ask the user + +9. **Compose commit message** + - If $ARGUMENTS was provided, use it as the commit message + - Otherwise, compose a Conventional Commit message: `<type>(<scope>): <short description>` + - Types: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`, `perf`, `style` + - Derive scope from the primary changed file path (e.g., `ace/skillbook.py` -> `skillbook`, `ace/integrations/litellm.py` -> `integrations`, `tests/test_foo.py` -> `tests`) + - Keep messages short and imperative + +10. **Check branch safety** + - Run `git branch --show-current` to get the current branch + - If on `main` or `master`, **warn the user** and ask for explicit confirmation before committing + - If denied, stop without committing + +11. **Commit and push** + - Commit with the composed message + - Push to remote: `git push` (or `git push -u origin <branch>` if no upstream is set) + +12. **Report summary** + - Commit hash (short) + - Branch name + - Files changed count + - Test results (pass count) + - Any warnings encountered + +**Error handling:** +- If no changes exist (clean working tree), report "Nothing to finalize" and stop +- If tests fail after 3 retries, report failures and stop without committing +- If push fails, report the error but keep the local commit +- If formatter fails, report the error and stop +- If security review finds unfixable Critical/High issues, report and stop without committing diff --git a/.claude/commands/list-branches.md b/.claude/commands/list-branches.md new file mode 100644 index 0000000000000000000000000000000000000000..eb24f6752ef0c1f6174f85a2567dd4af8bd99c65 --- /dev/null +++ b/.claude/commands/list-branches.md @@ -0,0 +1,40 @@ +List project branches with their worktree status. + +**Arguments:** $ARGUMENTS (optional filters) +- No args: List all branches matching naming convention `<type>/<developer>/<description>` +- `--all`: Include all branches (not just convention-named) +- `--worktrees`: Only show branches with active worktrees +- `<type>`: Filter by type (feature, fix, docs, refactor, test, chore) + +**Examples:** +- `/list-branches` → all convention-named branches +- `/list-branches feature` → only feature branches +- `/list-branches --worktrees` → only branches with worktrees +- `/list-branches --all` → all branches including main + +**Steps:** +1. Get all local branches: `git branch --format='%(refname:short)'` +2. Get worktree list: `git worktree list --porcelain` +3. Parse worktree output to map branches to paths +4. Filter branches based on arguments +5. Format and display results + +**Output format:** +``` +Branch Worktree Status +────────────────────────────────────────────────────────────────────────────── +* main . current + feature/john/add-caching ../feature-john-add-caching active + fix/jane/login-error (no worktree) - + +Summary: 3 branches, 2 with worktrees +``` + +**Column meanings:** +- `*` indicates current branch +- Worktree shows path relative to repo root, or "(no worktree)" if none +- Status: "current" (HEAD), "active" (has worktree), "-" (no worktree) + +**Tips shown after output:** +- To create a worktree for a branch: `git worktree add ../<path> <branch>` +- To remove a branch with worktree: use `/remove-branch <branch>` diff --git a/.claude/commands/release.md b/.claude/commands/release.md new file mode 100644 index 0000000000000000000000000000000000000000..d554fa3a5a78456fc7fb10d4f3ce698011606fdf --- /dev/null +++ b/.claude/commands/release.md @@ -0,0 +1,75 @@ +Release a new version: bump version, update changelog, tag, push, and create a GitHub release. + +**Arguments:** $ARGUMENTS — the new version number (e.g. `0.9.0`). Required. + +**Steps:** + +1. **Validate inputs** + - If $ARGUMENTS is empty, abort: "Usage: /release <version> (e.g. /release 0.9.0)" + - Strip leading `v` if present (e.g. `v0.9.0` → `0.9.0`) + - Validate format matches `X.Y.Z` (semver) + +2. **Validate branch state** + - Must be on `main`: `git branch --show-current` + - If not on main, abort: "Switch to main first." + - Pull latest: `git pull origin main` + - Working tree must be clean: `git status --porcelain` + +3. **Check version isn't already used** + - Read current version from `pyproject.toml` (line with `version = "..."`) + - If new version equals current version, abort: "Version <ver> is already set." + - Check tag doesn't exist: `git tag -l v<version>` + - If tag exists, abort: "Tag v<version> already exists." + +4. **Build changelog entry from git history** + - Find the latest tag: `git describe --tags --abbrev=0` + - Get commits since that tag: `git log <last-tag>..HEAD --format='%s'` + - Get merged PRs since that tag: `gh pr list --state merged --base main --search "merged:>=$(git log -1 --format=%ci <last-tag> | cut -d' ' -f1)" --json title,number --limit 50` + - From the commits/PRs, compose a changelog section with **only `### Added` items** — user-facing features. Skip fixes, refactors, chores, docs-only changes, and CI changes. + - Format: + ``` + ## [X.Y.Z] - YYYY-MM-DD + + ### Added + - **Feature name** — short description + - **Feature name** — short description + ``` + - Also prepare a compare link for the bottom of CHANGELOG.md: + `[X.Y.Z]: https://github.com/Kayba-ai/agentic-context-engine/compare/v<prev>...vX.Y.Z` + +5. **Present draft to user** + - Show: new version, changelog entry, and the release note (same as changelog "Added" bullets) + - Ask: "Create this release?" with options: Yes, Edit (let me revise), Cancel + +6. **On approval — apply changes** + - Update `pyproject.toml`: replace `version = "<old>"` with `version = "<new>"` + - Insert the changelog entry in `CHANGELOG.md` after line 7 (before the previous release) + - Add the compare link at the bottom of CHANGELOG.md + - Stage files: `git add pyproject.toml CHANGELOG.md` + - Commit: `git commit -m "chore(release): bump version to <version>"` + +7. **Tag and push** + - `git tag v<version>` + - `git push origin main --tags` + +8. **Create GitHub release** + - Title: `v<version>` + - Notes: **only the "Added" bullets** from the changelog entry — short and clean, no preamble + - Append: `**Full Changelog**: https://github.com/kayba-ai/agentic-context-engine/compare/v<prev>...v<version>` + - Run: `gh release create v<version> --title "v<version>" --notes "<notes>"` + - This triggers `.github/workflows/publish.yml` → PyPI publish + +9. **Report summary** + ``` + Released v<version> + - Commit: <short-hash> + - Tag: v<version> + - Release: <github-release-url> + - PyPI: publishing via workflow (check Actions tab) + ``` + +**Error handling:** +- If `gh` is not installed: "GitHub CLI (gh) is required. Install it: https://cli.github.com" +- If not authenticated: "Run `gh auth login` first." +- If push fails: report error, keep local commit (user can retry) +- If `gh release create` fails: show the error, suggest manual creation diff --git a/.claude/commands/remove-branch.md b/.claude/commands/remove-branch.md new file mode 100644 index 0000000000000000000000000000000000000000..b7c7c68efa7b8ffd6e54e2c48b88928372d8e3fe --- /dev/null +++ b/.claude/commands/remove-branch.md @@ -0,0 +1,41 @@ +Remove a branch and its associated worktree. + +**Arguments:** $ARGUMENTS should be the branch name (full or partial match) +- `--force`: Skip confirmations and force delete unmerged branches + +**Examples:** +- `/remove-branch feature/john/add-caching` → remove branch and worktree +- `/remove-branch add-caching` → partial match, will prompt to confirm +- `/remove-branch feature/john/add-caching --force` → skip all confirmations + +**Steps:** +1. Parse branch name and flags from arguments +2. Resolve branch name (support partial matching if unique) +3. Safety checks: + - Abort if trying to remove main/master + - Abort if trying to remove current branch (must switch first) + - Warn if branch has unmerged commits (show `git log main..<branch> --oneline`) +4. Check if branch has an associated worktree: `git worktree list` +5. If worktree exists: + - Remove worktree first: `git worktree remove <path>` (or `--force` if needed) + - Prune worktree list: `git worktree prune` +6. Delete the branch: `git branch -d <branch>` (or `-D` with `--force`) +7. Confirm success + +**On success, output:** +``` +✓ Removed worktree: <worktree-path> +✓ Removed branch: <branch-name> +``` + +**Error handling:** +- Multiple partial matches: list matches and ask user to be more specific +- Unmerged commits without --force: show commits and ask for confirmation +- Protected branches (main/master): refuse with explanation +- Current branch: instruct user to switch branches first + +**Protected branches:** main, master + +**Confirmation prompts (unless --force):** +- "Branch has N unmerged commits. Remove anyway? (show commits first)" +- For partial match: "Did you mean <full-branch-name>?" diff --git a/.claude/package.json b/.claude/package.json new file mode 100644 index 0000000000000000000000000000000000000000..e1fddefe73e581ac85714308470df05712fc254c --- /dev/null +++ b/.claude/package.json @@ -0,0 +1 @@ +{"type":"commonjs"} diff --git a/.claude/projects/-home-david-projects-Kayba-agentic-context-engine/memory/feedback_bedrock_only.md b/.claude/projects/-home-david-projects-Kayba-agentic-context-engine/memory/feedback_bedrock_only.md new file mode 100644 index 0000000000000000000000000000000000000000..db7c4477759aae6556e147d034e9bd90db0d4df3 --- /dev/null +++ b/.claude/projects/-home-david-projects-Kayba-agentic-context-engine/memory/feedback_bedrock_only.md @@ -0,0 +1,11 @@ +--- +name: Always use Bedrock +description: Never use direct Anthropic API key or fall back to OpenAI — always use Bedrock via AWS_BEARER_TOKEN_BEDROCK +type: feedback +--- + +Always use Bedrock for LLM calls. Never use the Anthropic API key directly, never fall back to OpenAI or any other provider. + +**Why:** The user has Bedrock configured with `AWS_BEARER_TOKEN_BEDROCK` and does not want direct Anthropic API usage (burns quota/money on the wrong account). Fallback logic is unacceptable — it silently uses the wrong provider. + +**How to apply:** In integration tests and any code that needs an LLM model string, use `bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0` (or similar Bedrock model). Never write fallback chains like "if ANTHROPIC_KEY else OPENAI". Just use Bedrock, period. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..902dd721e7f62097340627a3b7377dc74a4e6ddf --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(uv run pytest:*)", + "Bash(uv run black:*)" + ] + } +} diff --git a/.claude/skills/kayba-pipeline/SKILL.md b/.claude/skills/kayba-pipeline/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..7e88e52021c7debaa84d0f5c5767ead28d4fc12d --- /dev/null +++ b/.claude/skills/kayba-pipeline/SKILL.md @@ -0,0 +1,145 @@ +--- +name: kayba-pipeline +description: End-to-end agent evaluation and improvement pipeline. Takes a traces folder and optional HITL flag, then orchestrates sub-agents through 7 stages — each stage is its own skill invoked by a dedicated sub-agent. Trigger when the user says "run the pipeline", "kayba pipeline", "evaluate and fix", "full eval", "analyze traces and fix", or provides a traces folder with intent to improve their agent. +--- + +# kayba-pipeline + +End-to-end pipeline: analyze traces → define metrics → build rubric → plan fixes → implement fixes. + +Each stage is a separate skill file that can be run independently or as part of this pipeline. + +## Inputs + +The user provides two things: + +1. **`TRACES_FOLDER`** — path to a directory containing trace JSON files +2. **`HITL`** — `true` or `false` — whether to pause for human review before implementing fixes + +If the user doesn't specify HITL, default to `true` (safe default). + +--- + +## Pipeline overview + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Stage 1: Kayba API Analysis → skill: kayba-pipeline:stage-1-api-analysis │ +│ Stage 2: Domain Context Gathering → skill: kayba-pipeline:stage-2-domain-context │ +│ ─── stages 1 & 2 run in parallel ─── │ +│ Stage 3: Metrics & Analysis → skill: kayba-pipeline:stage-3-metrics │ +│ Stage 4: Rubric Definition → skill: kayba-pipeline:stage-4-rubric │ +│ Stage 5: Action Plan → skill: kayba-pipeline:stage-5-action-plan │ +│ Stage 6: HITL Gate → skill: kayba-pipeline:stage-6-hitl │ +│ Stage 7: Fix Implementation → skill: kayba-pipeline:stage-7-fixer │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Orchestration instructions + +You are the orchestrator. Your job is to: +1. Create the `eval/` directory and `eval/pipeline_log.md` +2. Spawn sub-agents that invoke stage skills via the Skill tool +3. Coordinate stage ordering and handle the HITL gate + +### Setup + +Create `eval/` directory and initialize `eval/pipeline_log.md`: + +```markdown +# Pipeline Log + +| Stage | Name | Status | Started | Completed | Notes | +|-------|------|--------|---------|-----------|-------| +| 1 | Kayba API Analysis | pending | | | | +| 2 | Domain Context | pending | | | | +| 3 | Metrics & Analysis | pending | | | | +| 4 | Rubric Definition | pending | | | | +| 5 | Action Plan | pending | | | | +| 6 | HITL Gate | pending | | | | +| 7 | Fix Implementation | pending | | | | +``` + +### Stages 1 & 2 — run in parallel + +Spawn two sub-agents in parallel using the Agent tool: + +**Agent 1:** +- Name: `api-analyst` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-1-api-analysis" using the Skill tool. The traces folder is: {TRACES_FOLDER}. Follow the skill instructions completely.` + +**Agent 2:** +- Name: `domain-scout` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-2-domain-context" using the Skill tool. The traces folder is: {TRACES_FOLDER}. Follow the skill instructions completely.` + +Wait for both to complete before proceeding. + +### Stage 3 — sequential + +Spawn one sub-agent after stages 1 & 2 complete: + +- Name: `metric-engineer` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-3-metrics" using the Skill tool. The traces folder is: {TRACES_FOLDER}. Follow the skill instructions completely — this includes iterating on the metrics until you're satisfied.` + +### Stage 4 — sequential + +Spawn one sub-agent after stage 3 completes: + +- Name: `rubric-builder` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-4-rubric" using the Skill tool. Follow the skill instructions completely.` + +### Stage 5 — sequential + +Spawn one sub-agent after stage 4 completes: + +- Name: `action-planner` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-5-action-plan" using the Skill tool. Follow the skill instructions completely.` + +### Stage 6 — HITL Gate + +**If `HITL` is `true`:** + +Spawn one sub-agent after stage 5 completes: + +- Name: `hitl-reviewer` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-6-hitl" using the Skill tool. Follow the skill instructions completely. Present the full review to the user and collect their decision before proceeding.` + +Wait for the sub-agent to complete. Check `eval/stage6_decision.md` for the outcome: +- If decision is "Approve all" or "Approve with modifications" — proceed to Stage 7 +- If decision is "Reject" — re-run Stage 5 with the user feedback recorded in `eval/stage6_decision.md`, then re-run Stage 6 +- Only proceed to Stage 7 after a clear approval is recorded + +**If `HITL` is `false`:** +- Skip to Stage 7 +- Log "HITL skipped" in `eval/pipeline_log.md` + +### Stage 7 — sequential + +Spawn one sub-agent after stage 6 completes (or is skipped): + +- Name: `fixer` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-7-fixer" using the Skill tool. Follow the skill instructions completely.` + +--- + +## Error handling + +- If any stage fails, log the failure in `eval/pipeline_log.md` with the stage number and error +- Do not proceed to dependent stages if a prerequisite failed +- If Stage 1 fails (kayba CLI issues), ask the user whether to proceed without API insights — if yes, skip Stage 1 and have Stage 3 work from domain context + raw traces only + +## After completion + +Update `eval/pipeline_log.md` with final status for all stages. Report to the user: +- How many stages completed successfully +- Summary of metrics (from rubric) +- Summary of fixes applied (from changes log) diff --git a/.claude/skills/kayba-pipeline/stage-1-api-analysis/SKILL.md b/.claude/skills/kayba-pipeline/stage-1-api-analysis/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..34b14aba1f0bdfcf16e601a8a8ffdcb014160a22 --- /dev/null +++ b/.claude/skills/kayba-pipeline/stage-1-api-analysis/SKILL.md @@ -0,0 +1,84 @@ +--- +name: kayba-stage-1-api-analysis +description: Fetch pre-computed insights from the Kayba API and build a structured summary. Does NOT upload traces or trigger generation — analysis is assumed to already exist. Trigger when the user says "run stage 1", "get insights", "fetch skills", "kayba analyze", or when invoked by the kayba-pipeline orchestrator. Requires the kayba CLI to be installed and KAYBA_API_KEY to be set. +--- + +# Stage 1: Kayba API Analysis (Fetch-Only Mode) + +Fetch pre-computed insights from the Kayba API. Traces have already been uploaded and analyzed — this stage only pulls results. + +## Inputs + +- **`TRACES_FOLDER`** — passed by the orchestrator but **ignored** in this stage. Traces are already uploaded and analyzed on the Kayba side. Do NOT upload, validate, or read trace files. + +## Process + +### Step 1: Setup + +Ensure `eval/` directory exists at the project root. + +### Step 2: Fetch insights + +``` +kayba insights list --json > eval/insights.json +``` + +If `kayba` is not found in PATH, search common locations (`.venv/bin/kayba`, project virtualenvs). If found, use the full path. If not found anywhere, report the error and stop. + +If `KAYBA_API_KEY` is not set, report the error and stop. + +### Step 3: Insight quality gate + +Read `eval/insights.json` and run quality checks before building the summary: + +1. **Empty check**: if the insights array is empty (0 insights returned), report this as a warning. Write a minimal summary noting "0 insights generated" and stop — downstream stages cannot proceed without insights. +2. **Duplicate detection**: compare insight `content` fields pairwise. If two insights cover substantially the same behavior (same section, overlapping evidence traces, similar corrective action), flag them as potential duplicates in the summary. Do not remove them — just annotate. +3. **Evidence coverage**: for each insight, check if the `evidence` field references specific traces (e.g., "task_7 turn 4"). Insights with no trace-specific evidence are lower quality — flag as "low-evidence" in the summary. +4. **Vote signal**: insights with `status: "accepted"` and `helpful > 0` have been human-validated. Insights with `status: "new"` and `helpful: 0, harmful: 0` are unvalidated — note this distinction in the summary. + +Log the quality gate result: `"Insight quality: {total} insights, {accepted} accepted, {new_unvalidated} unvalidated, {duplicates} potential duplicate pairs, {low_evidence} low-evidence"` + +### Step 4: Build structured summary + +Extract a structured summary of each insight: +- Insight ID and title/summary (use the `section` field as the title) +- Status +- Evidence citations — specific trace references, error strings, behavioral patterns the reflector identified +- Justification / reasoning chain — the reflector's full analysis of why this is a real pattern +- Confidence score if available +- Helpful/harmful counts if available +- Quality flags from Step 3 (potential duplicate, low-evidence, unvalidated) + +Write the structured summary to `eval/stage1_insights_summary.md` using this format: + +```markdown +# Kayba Insights Summary + +Generated from: Kayba API (pre-computed analysis) +Total insights: N +Quality: {accepted} accepted, {unvalidated} unvalidated, {duplicate_pairs} potential duplicate pairs, {low_evidence} low-evidence + +## Insight: [ID] — [section title] +**Status:** [status] [quality flags if any, e.g., "[potential duplicate with ID]", "[low-evidence]", "[unvalidated]"] +**Confidence:** [score if available] +**Evidence:** +- [citation 1 — trace reference, error string, or behavioral pattern] +- [citation 2] +**Justification:** [reflector's reasoning for why this is a real pattern] +**Helpful/Harmful:** [counts if available] + +--- +[repeat for each insight] +``` + +## Error handling + +- If `kayba` is not found in PATH or common locations, report the error and stop +- If `KAYBA_API_KEY` is not set, report the error and stop +- If `kayba insights list` fails (network error, auth error), report the error and stop +- If 0 insights are returned, write a minimal summary and stop — downstream stages need insights + +## Outputs + +- `eval/insights.json` — raw API response +- `eval/stage1_insights_summary.md` — structured summary with quality annotations for downstream stages diff --git a/.claude/skills/kayba-pipeline/stage-2-domain-context/SKILL.md b/.claude/skills/kayba-pipeline/stage-2-domain-context/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..72791ddd80c0159a7e27f589a5f91469b122713c --- /dev/null +++ b/.claude/skills/kayba-pipeline/stage-2-domain-context/SKILL.md @@ -0,0 +1,166 @@ +--- +name: kayba-stage-2-domain-context +description: Gather domain context about the repository and agent — system prompt, tool definitions, domain docs, and behavior patterns from traces. Trigger when the user says "run stage 2", "gather context", "domain context", or when invoked by the kayba-pipeline orchestrator. +--- + +# Stage 2: Domain Context Gathering + +Understand the agent's world — what it does, what tools it has, and what "success" looks like. + +## Inputs + +- **`TRACES_FOLDER`** — path to directory containing trace JSON files + +## Process + +### 0. Detect trace format + +Before reading traces, identify the framework that produced them. Read 1 trace file and check: + +| Signal | Framework | +|--------|-----------| +| `info.agent_info.implementation`, `info.environment_info`, `simulation.messages[]` with `role`/`tool_calls`/`turn_idx` | **tau2-bench** | +| `runs[].steps[]` with `type: "tool"`, `lc_kwargs` | **LangChain / LangSmith** | +| `events[]` with `event_type`, `span_id`, `parent_id` | **LlamaIndex** | +| `choices[].message.tool_calls[]` at top level | **Raw OpenAI API logs** | +| `trace.spans[]` with `attributes`, `trace_id` | **OpenTelemetry / Arize / Langfuse** | + +Record the detected format in the output under **Trace Format**. All subsequent trace-reading steps use the field paths appropriate for that format. + +If the format is unrecognized, note the top-level keys and structure, then proceed best-effort with field names found in the data. + +### 1. Detect architecture + +Read 2-3 traces and determine if this is a single-agent or multi-agent system: + +- **Single agent**: one `agent_info` entry, one conversation thread, tool calls from one identity +- **Multi-agent / router**: look for multiple `agent_info` entries, routing tool calls (e.g., `transfer_to_*`, `delegate_to_*`), sub-conversation arrays, or distinct system prompts per agent identity + +If multi-agent: document each agent separately (name, role, tools, handoff triggers) and note the routing logic. The remaining steps apply per-agent. + +### 2. Find the system prompt + +Use a fallback chain — stop at the first hit: + +1. **Config files** — grep for keys: `system_prompt`, `system_message`, `instructions`, `AGENT_INSTRUCTION`, `SYSTEM_PROMPT` in YAML/JSON/TOML/Python/JS files +2. **Source code** — search for prompt template strings, f-strings, or `.format()` calls that build the system message (look in agent implementation files) +3. **Trace extraction** — read 3 trace files from `{TRACES_FOLDER}`: + - Check `info.environment_info.policy` (tau2-bench format) + - Check first message with `role: "system"` in the messages array + - Check `raw_data` fields for system-level content +4. **Not found** — if none of the above yields a system prompt, explicitly record `SYSTEM_PROMPT_STATUS: NOT_FOUND` in the output and flag this for the orchestrator. Do not fabricate or guess. + +When found, record both the prompt content and its **source location** (file path + line, or trace field path). + +### 3. Extract tool definitions + +Two-pass approach: source code first (ground truth), then traces (usage evidence). + +**Pass 1 — Source code discovery:** +- Search for tool/function definition patterns: `@tool`, `@is_tool`, `def tool_`, function schema arrays, OpenAPI specs, `tools=[]` arguments +- For each tool, extract from source: + - Name + - Input parameters with types and defaults + - Return type / output schema (document the structure, not just "returns a dict") + - Side effects: READ (no state change), WRITE (mutates state), GENERIC (neither) + - Validation rules the tool does NOT enforce (critical — grep for comments like "API does not check", "agent must enforce") + +**Pass 2 — Trace usage evidence:** +- Read ALL traces (if <= 20) or a stratified sample (see step 4 for sampling) +- Extract every unique `tool_calls[].name` from assistant messages +- Extract every `role: "tool"` response to document actual output shapes +- For each tool, record one example input/output pair from traces + +**Reconcile the two passes:** +- Tools in source but NOT in traces = "available but unused" — flag these; they may be relevant for edge cases the agent should handle +- Tools in traces but NOT in source = possible dynamic tools or external APIs — investigate + +Output the full tool inventory as a table with columns: Name, Category, Input Schema, Output Schema, Observed in Traces (Y/N), Unvalidated Rules. + +### 4. Find domain documentation + +- READMEs, product docs, wiki links +- Policy files (e.g., `data/*/policy.md`, domain-specific docs) +- Inline code comments explaining business logic +- Test files that describe expected behavior +- Anything that explains what the agent does and what "success" means for its users + +### 5. Catalogue agent behavior patterns + +**Trace selection — stratified sampling** (do not just grab "5-10 random traces"): + +1. Count total traces in `{TRACES_FOLDER}`. If <= 20, read ALL of them. +2. If > 20, select a stratified sample: + - Sort by `termination_reason` — include at least 2 per unique reason + - Sort by conversation length (message count) — include shortest, longest, and 2 median + - Sort by tool call count — include lowest and highest + - If task outcomes are available (pass/fail), include at least 3 of each + - Target: ~15 traces total, or 30% of the corpus, whichever is larger + +For each selected trace, document: +- **Function call frequency** — which tools are called most, in what order +- **Tool call sequences** — common tool chains (e.g., get_user -> get_reservation -> cancel) +- **Success patterns** — what does a thread that accomplishes its goal look like? +- **Failure patterns** — what does a thread that fails or gets stuck look like? +- **Error patterns** — what error strings appear in tool outputs? Group by root cause +- **Policy violation patterns** — where does the agent break its own rules? (e.g., multiple tool calls per turn, acting without confirmation) +- **User feedback signals** — reverts, ratings, explicit corrections, escalations, stop tokens, transfer tokens + +### 6. Write findings + +Write all findings to `eval/stage2_domain_context.md`: + +```markdown +# Domain Context + +## Trace Format +- Framework: [detected framework name] +- Key field paths: [e.g., simulation.messages[], info.environment_info.policy] + +## Architecture +- Type: [single-agent | multi-agent] +- [If multi-agent: agent roster with roles and handoff triggers] + +## Agent Purpose +[1-2 sentence summary of what this agent does] + +## System Prompt +- **Source**: [file path + line, or trace field path, or NOT_FOUND] +- **Status**: [verbatim | reconstructed | not_found] + +[The system prompt content, or "NOT_FOUND — downstream stages should account for missing system prompt"] + +## Tools +| Tool | Category | Input Schema | Output Schema | In Traces? | Unvalidated Rules | +|------|----------|-------------|---------------|------------|-------------------| +| tool_name | READ/WRITE/GENERIC | `{param: type}` | `{field: type}` | Y/N | "API does not check X" | + +### Tools available but never called in traces +- [tool_name — why it matters] + +## Domain Rules +[Key business rules, constraints, policies the agent must follow] + +## Behavior Patterns + +### Success patterns +- [pattern 1] + +### Failure patterns +- [pattern 1] + +### Policy violation patterns +- [violation with frequency: N/M turns] + +### Error patterns +| Error | Frequency | Root cause | +|-------|-----------|------------| +| error string | N traces | cause | + +### User feedback signals +- [signal 1] +``` + +## Outputs + +- `eval/stage2_domain_context.md` diff --git a/.claude/skills/kayba-pipeline/stage-3-metrics/SKILL.md b/.claude/skills/kayba-pipeline/stage-3-metrics/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..a6ea98c0c84c7632c3140de9a0cad2ef4c6b8fce --- /dev/null +++ b/.claude/skills/kayba-pipeline/stage-3-metrics/SKILL.md @@ -0,0 +1,180 @@ +--- +name: kayba-stage-3-metrics +description: Define metrics from Kayba insights, implement them as Python measurement code, run against traces, and iterate until the metrics are clean and meaningful. Trigger when the user says "run stage 3", "define metrics", "build metrics", "compute baselines", or when invoked by the kayba-pipeline orchestrator. Requires eval/stage1_insights_summary.md and eval/stage2_domain_context.md to exist. +--- + +# Stage 3: Metrics and Programmatic Analysis + +Define metrics from insights, implement as code, run, review, iterate. + +## Inputs + +- **`TRACES_FOLDER`** — path to directory containing trace JSON files +- **`eval/stage1_insights_summary.md`** — output from Stage 1 +- **`eval/stage2_domain_context.md`** — output from Stage 2 + +Read both input files before starting. + +## Process + +This stage is iterative. You cycle through define → implement → run → review, with a hard cap of **3 iterations**. A metric set is "clean" when ALL of the following hold: + +1. **No small-sample metrics in the priority set** — every metric used for priority ranking has denominator >= 5. Metrics with denominator < 5 are kept but labeled `"confidence": "directional-only"` and excluded from priority sorting. +2. **No unexplained extremes** — no metric reads 0% or 100% unless you can write a one-sentence justification (e.g., "0% is correct because the agent never calls send_certificate anywhere in the dataset"). Record the justification in the metric's `"extreme_justification"` field. +3. **No redundant pairs** — no two metrics share > 70% of their denominator events. Check this: for each pair, compute `|events_A ∩ events_B| / min(|events_A|, |events_B|)`. If > 0.70, merge or drop one. +4. **Script runs without errors** on the full trace set. + +If after 3 iterations the set is not fully clean, ship what you have and log remaining issues in `eval/baseline_metrics.json` under a top-level `"warnings"` key. + +### Step 1: Define metrics + +For each insight from the Kayba analysis, use the evidence fields to identify observable signals in the traces: + +1. Read the insights summary — focus on evidence citations, error strings, behavioral patterns +2. For each valid insight, determine what trace signal would change if the agent followed the skill +3. Classify each metric by detector pattern type: + +**Recovery detectors** — consecutive calls to the same function where first has error, next succeeds +```python +def has_recovery(calls, function_name): + for i in range(len(calls) - 1): + if calls[i]['name'] == function_name and is_error(calls[i]['output']): + if calls[i+1]['name'] == function_name and is_success(calls[i+1]['output']): + return True + return False +``` + +**Loop detectors** — N+ consecutive calls to the same function (stuck agent) + +**Give-up detectors** — regex match agent output for abandonment phrases ("I'm unable to", "cannot complete", "beyond my capabilities") + +**Error classifiers** — match function outputs against domain-specific error patterns. Build a pattern table: +```python +ERROR_PATTERNS = { + 'pattern_name': r'regex matching the error', + # one entry per distinct error type +} +``` + +**Over-exploration detectors** — ratio of explore vs action calls. Use the tool categories from Stage 2. If explore ratio exceeds threshold AND task didn't complete → analysis paralysis + +**Ground-truth comparison detectors** — agent claims a value (dollar amount, flight number, policy rule) in natural language, and the preceding tool response contains the actual value. Extract candidate values from agent text via regex, then compare against structured fields in the tool response JSON. Examples: +```python +# Extract dollar amounts from agent text +DOLLAR_PATTERN = r'\$\s?([\d,]+(?:\.\d{2})?)' + +# Extract flight numbers (3 letters + 3 digits) +FLIGHT_PATTERN = r'\b([A-Z]{2,3}\d{3,4})\b' + +def check_agent_claims_against_tool(agent_text, preceding_tool_response): + """Compare values the agent states against the tool response ground truth.""" + claimed_amounts = re.findall(DOLLAR_PATTERN, agent_text) + actual_amounts = extract_amounts_from_json(preceding_tool_response) + # A claim is fabricated if it doesn't match any actual value + fabricated = [c for c in claimed_amounts if not any(matches(c, a) for a in actual_amounts)] + return len(fabricated) == 0, fabricated +``` +This pattern covers data accuracy (fabricated prices/flights), post-action verification (quoted vs actual cost), and policy accuracy (claimed restrictions vs policy text). These are NOT qualitative-only — regex + JSON comparison is noisy but produces a real signal. Build the detector even if it's imperfect; a noisy metric that produces a fix is better than a clean classification that produces nothing. + +**Ordering/sequencing detectors** — agent performs actions in the wrong order (e.g., searches for flights before checking if the reservation is even modifiable). Check whether tool call A appears before tool call B when B should come first. + +**Clean success** — threads where all tasks completed with no errors and no other tags + +4. **Validate each detector before coding it at scale.** Pick 2-3 traces where you already know the ground truth from Stage 1 evidence. Run your detector logic mentally (or in a scratch script) against those traces. If it misclassifies any of them, fix the logic before writing the full implementation. This catches regex and pattern bugs early — the Stage 3 trace showed multiple iterations wasted on broken confirmation-phrase matching that a quick manual check would have caught. + +### Step 2: Implement and run + +1. Write `eval/compute_baselines.py` with: + - CLI args: `--traces-dir` (required), `--output` (default: `eval/baseline_metrics.json`) + - `load_traces(traces_dir)` — loads all JSON trace files + - Error pattern table built from reading 20-30 traces + - `tag_thread(thread)` — combines all detectors, returns list of tags + - One measurement function per metric, computing `numerator / denominator` + - `compute_all_baselines(traces_dir)` — runs all metrics, returns dict + - Main block that runs everything and prints summary + +2. Run it: + ``` + python eval/compute_baselines.py --traces-dir {TRACES_FOLDER} --output eval/baseline_metrics.json + ``` + +### Step 3: Review and iterate + +Run these checks in order after every run. Each check either passes or produces a concrete fix action. + +**Check A — Script health.** Did the script error or produce `null` values? → fix and re-run. This is iteration 0-cost; don't count it toward the 3-iteration cap. + +**Check B — Small-sample guard.** For each metric, examine the denominator: +- denominator >= 5 → full-confidence metric, usable for priority ranking +- denominator 1-4 → label `"confidence": "directional-only"` in the output JSON. The metric stays in the report but is excluded from priority sorting in Stage 4. Do NOT drop it — small-sample metrics can still inform qualitative analysis. +- denominator 0 → the detector found no applicable events. Either the detector is broken (fix it) or the behavior genuinely doesn't occur in this trace set (log as `"confidence": "not-observed"` and move on). + +**Check C — Extreme-value triage.** For any metric at exactly 0% or 100%: +- Ask: "Is there a plausible trace where this metric would NOT be extreme?" If yes → detector is likely broken, fix it. +- If no (the behavior legitimately always/never happens in this dataset) → write a one-sentence justification and add it as `"extreme_justification"` in the output. Example: M5=0% is correct because both cancellations in the dataset were on ineligible reservations. +- Do NOT reflexively drop 0%/100% metrics. A metric that correctly reads 0% is a strong signal for Stage 5 action planning. +- **Ceiling/floor flag for 100% and 0% metrics:** If a metric baseline is already at 100% (or 0% where 0% is the desired direction), add `"at_ceiling": true` (or `"at_floor": true`) to its entry in the output JSON. This signals to Stage 4 (direction setting) and Stage 5 (action planning) that the metric is already optimal and should NOT be listed as needing improvement. Stage 4 must set its direction to `"↑ maintain"` or `"— already optimal"`, never bare `"↑"`. + +**Check D — Correlation / overlap audit.** For every pair of metrics, compute event overlap: `|denom_A ∩ denom_B| / min(|denom_A|, |denom_B|)`. If > 0.70: +- The two metrics are measuring overlapping populations. Keep the one with the sharper behavioral distinction (measures a more specific failure mode). Drop or merge the other. +- In the Stage 3 trace, M1 and M2 shared identical denominators (29 tool-calling turns) and were never flagged. They survived because they measure different *properties* of the same events — this is acceptable only if the numerator overlap is also checked. If both numerators move in lockstep (one is a strict subset of the other), merge them. + +**Check E — Coverage (strict).** For EVERY Stage 1 insight, verify it has a corresponding metric. If an insight has no metric: +- First, try harder to build one. Can you extract values from agent text and compare against tool responses? Can you detect the wrong tool-call ordering? Can you pattern-match the failure mode with keywords + JSON field checks? +- Only after a concrete failed attempt, classify as unmeasurable with a specific reason why the approach you tried doesn't work. +- An insight classified as unmeasurable means Stage 5 will NOT produce a fix for it. That is a real cost. Treat every unmeasurable classification as a missed fix. + +After checks, if any produced a fix action: apply fixes and re-run (counts as one iteration). If all checks pass → the metric set is clean. **Stop iterating.** + +### Design principles + +- **Target one metric per insight.** Every insight should have a metric unless it is genuinely unmeasurable (see above). If you end up with fewer metrics than insights, you are being too conservative. Directional-only metrics (denominator < 5) still count — they produce fixes in Stage 5. Only apply the redundancy check (Check D) to merge metrics that truly overlap; do not use the metric count as a reason to skip building detectors. +- **Express every metric as a ratio or percentage.** Absolute counts aren't comparable across trace sets. +- **Prefer per-event denominators over per-thread.** "% of EditScript calls with errors" is sharper than "% of threads with any EditScript error." Per-thread denominators compress information — a thread with 10 violations and a thread with 1 both count the same. +- **One metric per behavioral change.** If two would always move together, keep only the sharper one. Use Check D (overlap audit) to enforce this mechanically, not just by intuition. +- **Build a metric for EVERY insight. "Unmeasurable" is a last resort, not a default.** Before classifying an insight as unmeasurable, you MUST attempt to build a programmatic detector. The bar for "unmeasurable" is: you tried a concrete approach, it fundamentally cannot work (not just "it's noisy"), and you can explain why in one sentence. + + Specifically: + - **"Agent claims X but tool response says Y"** — this is ALWAYS measurable. Use regex to extract values (dollar amounts, IDs, flight numbers) from agent text, compare against structured JSON fields in the preceding tool response. Noisy matches are fine — a metric that catches 70% of fabrications is far more useful than a qualitative note that catches 0%. + - **"Agent violates policy rule Z"** — if the policy rule can be stated as a condition on trace data (tool call ordering, presence/absence of a call, argument values), build a detector. Only classify as qualitative-only if the rule requires understanding the *meaning* of free-text agent output beyond keyword/pattern matching. + - **"Insufficient data"** — if the detector logic is clear but n < 5, build the detector anyway and label it `"confidence": "directional-only"`. Do NOT skip building the metric. A directional-only metric still produces a fix in Stage 5. + + If after genuine effort an insight truly cannot be measured programmatically, classify it as: + - `"qualitative-only"` — requires semantic understanding that regex/JSON comparison cannot approximate. Must explain what specific semantic judgment is needed and why pattern matching fails. + - `"insufficient-data"` — detector exists but denominator is 0 (not just small — literally zero applicable events). Note what scenarios would need to appear in traces. + - `"needs-ground-truth"` — requires task-specific expected outcomes that aren't in the trace format. + + Record any remaining unmeasurable insights in the output JSON under a `"unmeasurable"` key. **The goal is for this list to be as short as possible — ideally empty.** + +## Outputs + +- `eval/compute_baselines.py` — runnable script with `--traces-dir` and `--output` CLI args +- `eval/baseline_metrics.json` — computed baseline values, structured as: + ```json + { + "M1": { + "name": "single_tool_call_compliance", + "value": 0.414, + "numerator": 12, + "denominator": 29, + "confidence": "full" + }, + "M5": { + "name": "cancellation_policy_compliance", + "value": 0.0, + "numerator": 0, + "denominator": 2, + "confidence": "directional-only", + "extreme_justification": "0% correct: both cancellations in dataset were on ineligible reservations" + }, + "warnings": ["M5 and M6 have denominator < 5; excluded from priority ranking"], + "unmeasurable": [ + { + "insight_id": "d7494740", + "name": "Cabin Change Constraints", + "classification": "insufficient-data", + "reason": "Only 1 update_reservation_flights call in dataset" + } + ] + } + ``` diff --git a/.claude/skills/kayba-pipeline/stage-4-rubric/SKILL.md b/.claude/skills/kayba-pipeline/stage-4-rubric/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..f41f5e7b3a1c3d5575ed3539f1c31f64311a778c --- /dev/null +++ b/.claude/skills/kayba-pipeline/stage-4-rubric/SKILL.md @@ -0,0 +1,163 @@ +--- +name: kayba-stage-4-rubric +description: Organize computed metrics into a tiered evaluation rubric with leading, lagging, and quality indicators. Trigger when the user says "run stage 4", "build rubric", "tier metrics", or when invoked by the kayba-pipeline orchestrator. Requires eval/baseline_metrics.json and eval/compute_baselines.py to exist. +--- + +# Stage 4: Rubric Definition + +Organize metrics into a tiered evaluation rubric. Detect and resolve redundancy quantitatively. Ensure every insight is accounted for. + +## Inputs + +- `eval/baseline_metrics.json` — computed baseline values from Stage 3 +- `eval/compute_baselines.py` — to understand what each metric measures +- `eval/stage1_insights_summary.md` — the original insights +- `eval/stage2_domain_context.md` — domain context + +Read all four files before starting. + +## Process + +### 1. Quantitative redundancy check + +Before tiering, check every pair of metrics for overlap. Two metrics are redundancy candidates if ANY of the following hold: + +- **Denominator overlap >70%**: compute `|denom_events(A) ∩ denom_events(B)| / min(|denom(A)|, |denom(B)|)`. If >0.70, they are candidates. To compute this, trace through the detector functions in `compute_baselines.py` and determine which trace events (turns, calls, threads) each denominator iterates over. When denominators are identical sets (same loop, same filter), overlap is 100%. +- **Same skill set**: the metrics map to the exact same set of insight/skill IDs from Stage 1. +- **Logical subsumption**: one metric's positive case is a strict subset of the other's (e.g., "turn has exactly 1 tool call" is a subset of "turn has no user-facing content alongside tool calls" only if every single-call turn also has no content — check this, don't assume it). + +For each candidate pair, make an explicit decision with reasoning: + +| Pair | Denom overlap | Skill overlap | Subsumption? | Decision | Reasoning | +|------|---------------|---------------|--------------|----------|-----------| +| M1/M2 | 100% (same 29 turns) | identical | No — can violate one without the other | **Keep both** | Independently actionable: batching vs. content leaking are distinct fixes | + +Valid decisions: **keep both** (with reasoning why they're independently actionable), **merge** (combine into one metric, specify how), or **drop** (specify which and why). "They feel different" is not sufficient reasoning — cite the specific behavior that one catches and the other misses. + +Final count target: 5-7 metrics after redundancy resolution. + +### 2. Tier each metric + +Use this decision flowchart for every metric: + +``` +Q1: Can a SINGLE skill/instruction change directly move this metric? + → If the agent follows one new instruction and the metric improves, + regardless of other behaviors: LEADING. + +Q2: Does moving this metric require MULTIPLE skills to be adopted together? + → If improvement depends on several upstream behaviors all working + (e.g., proper turn structure + confirmation flow + execution): + LAGGING. + +Q3: Does moving this metric require domain reasoning beyond following instructions? + → If the agent needs to correctly interpret policy rules, evaluate + eligibility criteria, or make judgment calls that can't be reduced + to a single instruction: QUALITY. +``` + +Apply the flowchart to each metric and record the Q1/Q2/Q3 answer that determined the tier. If a metric could arguably be two tiers, pick the lower one (Leading < Lagging < Quality) and note the ambiguity. + +Tier summary for reference: + +| Tier | Purpose | Moves when... | Diagnostic signal | +|------|---------|---------------|-------------------| +| **Leading** | Behaviors a single skill directly changes | Skill is adopted | If leading moves but lagging doesn't → skill adopted but not solving the right problem | +| **Lagging** | Aggregate outcomes requiring multiple skills | Multiple skills coordinate | If lagging moves but leading doesn't → something else improved, not your skills | +| **Quality** | Requires domain understanding, not just instruction-following | Agent reasons correctly | If quality moves but lagging doesn't → agent got lucky or metric is mis-tiered | + +### 3. Flag low-confidence baselines + +Any metric with denominator < 5 events is a **low-confidence baseline**. These metrics: +- ARE included in the rubric (they measure real behaviors) +- Are marked with `**Confidence: low** (n=X)` in the rubric +- Must NOT drive priority decisions in Stage 5 — they inform direction only +- Should note what denominator size would make them reliable (rule of thumb: n >= 10 for a rate metric to be meaningful, n >= 30 for statistical comparisons) + +### 4. Set direction + +For each metric, indicate whether it should go **up higher** or **down lower**. Don't set arbitrary numerical targets — baseline + direction is enough. + +**Ceiling guard:** If a metric's baseline is already 100%, its direction MUST be `"↑ maintain"` or `"— already optimal"`, never `"↑"` as if it needs to go higher. A 100% metric is at ceiling — the goal is to sustain it, not improve it. Similarly, if a metric is at 0% and the desired direction is `"↓"`, mark it `"↓ maintain"` or `"— already at floor"`. Do not let any downstream stage (Stage 5 action plan, Stage 7 fixes) list a ceiling/floor metric as needing improvement. + +### 5. Map insights to metrics (completeness check) + +For every insight from `eval/stage1_insights_summary.md`, assign it to one of three categories: + +1. **Mapped** — directly linked to one or more metrics. List which ones. +2. **Indirectly mapped** — supports a metric but isn't the primary driver. List the metric and explain the indirect relationship. +3. **Qualitative-only** — no programmatic metric captures this insight. Explicitly mark it and state why (e.g., "requires LLM-as-judge," "measures explanation quality," "efficiency pattern with no clear denominator"). + +Every insight MUST appear in exactly one category. If you find an insight that should have a metric but doesn't, note it as a gap for future Stage 3 iterations — but do not invent metrics at this stage. + +At the end, report: +- `X / N insights mapped to metrics` +- `Y / N insights indirectly mapped` +- `Z / N insights qualitative-only` + +### 6. Add invalidation notes + +For each metric, write one sentence answering: "What would make this tier assignment wrong?" + +Examples: +- M1 (Leading): "Wrong if fixing batching also requires the agent to change its confirmation flow — that would make it Lagging." +- M5 (Quality): "Wrong if cancellation compliance can be fixed by a single checklist instruction without requiring the agent to reason about policy — that would make it Leading." + +These notes exist so Stage 5 can catch tier errors. If Stage 5 finds evidence that a tier is wrong (e.g., a single skill would move a "Quality" metric), it should flag the conflict rather than silently inheriting the error. + +### 7. Write the rubric + +Write to `eval/baseline_metrics.md`: + +```markdown +# Eval Rubric — Baseline Metrics + +## Summary +| # | Metric | Tier | Baseline | Direction | Confidence | +|---|--------|------|----------|-----------|------------| +| M1 | First-call success rate | Leading | 37.6% | up | ok (n=29) | +| M2 | ... | ... | ... | ... | ... | + +## Tier Definitions + +- **Leading** — Single skill directly moves this. Should change first after deployment. +- **Lagging** — Multiple skills must coordinate. Improves as a consequence of adoption. +- **Quality** — Requires domain reasoning beyond instruction-following. Hardest to move. + +## Metric Details + +### M1: [name] +**Tier:** Leading +**Baseline:** 37.6% (685 / 1,821) +**Confidence:** ok (n=1821) | low (n=X) — needs n>=Y for reliable comparison +**Direction:** up higher is better +**What it measures:** [description] +**How it's computed:** [reference to function in compute_baselines.py] +**Skills that should move this:** [list insight/skill IDs from stage 1] +**Tier rationale:** [which flowchart question determined the tier] +**Invalidation note:** [what would make this tier wrong] + +### M2: [name] +... + +## Redundancy Analysis + +| Pair | Denom overlap | Skill overlap | Subsumption? | Decision | Reasoning | +|------|---------------|---------------|--------------|----------|-----------| +| ... | ... | ... | ... | ... | ... | + +## Insight Coverage + +### Mapped (X / N) +- `insight_id` — [title] → M1, M3 + +### Indirectly mapped (Y / N) +- `insight_id` — [title] → supports M5 via [explanation] + +### Qualitative-only (Z / N) +- `insight_id` — [title] — [why no metric: e.g., "requires LLM-as-judge"] +``` + +## Outputs + +- `eval/baseline_metrics.md` — human-readable tiered rubric with redundancy analysis, confidence flags, insight coverage, and invalidation notes diff --git a/.claude/skills/kayba-pipeline/stage-5-action-plan/SKILL.md b/.claude/skills/kayba-pipeline/stage-5-action-plan/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..7486e2ac0a954fc13ca7b51e70db1d8e749ee7b4 --- /dev/null +++ b/.claude/skills/kayba-pipeline/stage-5-action-plan/SKILL.md @@ -0,0 +1,201 @@ +--- +name: kayba-stage-5-action-plan +description: Triage each insight into discard/code-fix/prompt-fix and produce a prioritized action plan with specific recommendations. Trigger when the user says "run stage 5", "make action plan", "triage skills", or when invoked by the kayba-pipeline orchestrator. Requires eval outputs from stages 1-4. +--- + +# Stage 5: Action Plan + +Triage each insight and produce a concrete, prioritized action plan. + +## Inputs + +- `eval/stage1_insights_summary.md` — insights from Kayba +- `eval/stage2_domain_context.md` — domain context +- `eval/baseline_metrics.md` — the evaluation rubric +- `eval/baseline_metrics.json` — baseline values +- `eval/compute_baselines.py` — measurement code + +Read all files before starting. + +## Process + +### 1. Triage each insight + +For each insight/skill, answer three questions in order: Is it valid? Is it already handled? Is it a code fix or prompt fix? + +#### 1a. Validity check + +- Does it describe a real, recurring problem visible in traces — or noise from a one-off edge case? +- Is it actionable — can the agent actually change this behavior given its tools and context? +- If not valid → verdict: **discard** with a one-sentence reason. + +#### 1b. "Already handled" verification + +Do not rely on memory or assumption. Run these checks and cite what you find: + +1. **Grep the codebase** for 2-3 key terms from the insight (tool names, error strings, behavioral keywords). Example: for an insight about cancellation eligibility, grep for `cancel`, `eligibility`, `criteria`. +2. **Read the existing system prompt text** — check `AGENT_INSTRUCTION` in the agent file and the domain policy file. Quote any existing language that addresses this behavior. +3. **Verdict:** + - If existing text partially covers it → **keep** as a strengthening fix, note what's missing. + - If no existing coverage → **keep**. + - If existing prompt text already covers the behavior thoroughly AND the baseline metric is >= 95% → **discard** (cite the existing text and metric). A high baseline alone is NOT sufficient to discard — if the metric is below 95%, there are still failures to fix. An 87% baseline means 1 in 8 attempts still fails; that is worth fixing. + +#### 1c. Code-vs-prompt decision tree + +Walk through this tree for every non-discarded insight: + +``` +Q1: Can the agent fix this by following different instructions? + (Does it have the right tools, correct data in tool responses, + and sufficient context to behave correctly?) + │ + ├─ YES → PROMPT FIX + │ The agent has everything it needs but acts wrong. + │ A system prompt addition would fix it. + │ + └─ NO → Q2: What is the agent missing? + │ + ├─ Tool doesn't exist, schema is wrong, API returns + │ incomplete data, infrastructure drops information, + │ timeout/error not surfaced to agent + │ → CODE FIX + │ Name the file, function, and specific change. + │ + └─ The agent has partial information but the prompt + can't fully compensate (e.g., needs a new tool + but a heuristic prompt workaround exists) + → PROMPT FIX (primary) + CODE FIX (optional) + Note both. Mark the code fix as "optional" with + a one-sentence justification for why it's lower priority. +``` + +**Ambiguity default:** When genuinely uncertain, default to **prompt fix** and add a note: `"Classification uncertain — defaulting to prompt fix. Revisit if prompt change doesn't move metrics."` This is safer because prompt fixes are cheaper to test and revert, and Stage 7 handles prompt fixes and code fixes through different paths. + +Use the reflector's reasoning from Stage 1 insights — it often explicitly identifies root causes that clarify the code-vs-prompt distinction. + +### 2. Consolidate related insights + +Before writing recommendations, merge insights that are redundant. Two insights should merge when ALL three conditions hold: + +1. **Same target behavior** — they describe the agent doing (or failing to do) the same thing. +2. **Overlapping fix text** — the prompt instructions you'd write for each would share >50% of their content. +3. **Addressing one substantially addresses the other** — fixing insight A would fix >80% of the cases described by insight B. + +**When NOT to merge** — two insights about the same tool or domain area but different failure modes should remain separate. Example: "agent doesn't check cancellation eligibility" and "agent doesn't execute cancellation after user confirms" both involve `cancel_reservation` but are completely different behavioral failures with different prompt fixes. Keep them separate. + +For each merge, document: +- Which insight IDs are combined +- Which insight's framing is primary (use the one with stronger trace evidence) +- What, if anything, is lost from the secondary insight (add it as a sub-point) + +### 3. Write specific recommendations + +For each insight (after merging): + +- **Discards:** one sentence on why it's not valid or actionable. +- **Code fixes:** what code/schema/infrastructure to change. Name the file, the function, the specific change. If Stage 7 needs to find the right code location, give it enough to grep for. +- **Prompt fixes:** the exact instruction text to add to the system prompt, where it should go (e.g., appended to `AGENT_INSTRUCTION`, added to domain policy, or as a standalone skill block), and why this wording over alternatives. + +### 4. Assess risk per fix + +For each non-discarded fix, assess whether the change could break currently-working behaviors: + +| Risk | Definition | Example | +|------|-----------|---------| +| **None** | Change is additive; no existing behavior could be affected | Adding a new metric to compute_baselines.py | +| **Low** | Change targets a behavior that is currently failing; working cases are unrelated | Adding a cancellation checklist when current cancellation compliance is 0% | +| **Medium** | Change modifies a behavior where some cases already work correctly | Strengthening confirmation protocol when 28.6% already succeed — could the new wording break the working 28.6%? | +| **High** | Change rewrites or constrains a behavior that mostly works | Restricting tool-call patterns when 41.4% already comply — overly rigid wording could cause the agent to under-call tools | + +For Medium and High risk fixes, add a one-sentence mitigation: what to watch for, or how to word the prompt to preserve working cases. + +### 5. Handle qualitative-only insights — STILL PRODUCE FIXES + +Some insights from Stage 3 may be flagged as "unmeasurable." **These still get fixes.** An insight that the agent fabricates data or violates policy is a real problem whether or not we can measure it programmatically. Treat them the same as any other insight: + +- Run the same triage (validity → already-handled → code-vs-prompt) as every other insight. +- Include them in the **priority-ranked implementation list** alongside all other fixes. They are NOT second-class. +- Use the trace evidence from the insight (not the metric) to assess impact and priority. If the insight has strong trace evidence showing clear failures, rank it accordingly. +- For prioritization: since there is no metric denominator, use confidence = 0.5 and estimate impact from the severity described in the insight evidence. +- In the fix entry, note that this fix has no programmatic metric for automated before/after comparison, so improvement should be verified via manual trace review or LLM-as-judge after generating new traces. + +Only relegate an insight to a non-actionable "Monitor Items" section if the triage concludes it should be **discarded** (not valid or not actionable). Being unmeasurable is NOT a reason to skip fixing it. + +### 6. Link to metrics + +For each non-discarded fix, identify which metric(s) from the rubric would move if this fix is implemented. Use the metric IDs from `eval/baseline_metrics.md` (e.g., M1, M2). + +### 7. Prioritize + +Rank non-discarded fixes using this formula: + +``` +Priority Score = Impact × Confidence × Tier Bonus ÷ Risk Factor +``` + +Where: +- **Impact** = estimated metric delta. Use the gap between baseline and 100% as the ceiling. A fix expected to close 50% of that gap on M1 (baseline 41.4%) has impact = 0.5 × (1.0 - 0.414) = 0.293. +- **Confidence** = sample size reliability. Use the denominator from `baseline_metrics.json`: + - denominator >= 20: confidence = 1.0 + - denominator 10-19: confidence = 0.8 + - denominator 5-9: confidence = 0.6 + - denominator < 5: confidence = 0.3 +- **Tier Bonus** = leading metrics get a 1.5x multiplier (they validate adoption), lagging and quality get 1.0x. Rationale: leading metrics move first and tell you if your fix is even being adopted — you want those signals early. +- **Risk Factor** = None: 1.0, Low: 1.0, Medium: 1.5, High: 2.0 + +You do not need to compute exact scores to three decimal places. The formula is a tiebreaker and sanity check. The point is: +- High-impact, high-confidence, leading-metric fixes with low risk go first. +- Low-confidence fixes (small denominators) get deprioritized even if the metric is at 0%. +- High-risk fixes get deprioritized unless impact is overwhelming. + +After scoring, apply one manual adjustment pass: if a fix is a prerequisite for another fix (e.g., "confirmation protocol" must exist before "post-confirmation execution" can be measured), promote the prerequisite even if its standalone score is lower. + +## Output format + +Write to `eval/action_plan.md`: + +```markdown +# Action Plan + +## Summary +- Total insights: N +- Discarded: X (with reasons) +- Code fixes: Y +- Prompt fixes: Z +- Fixes without programmatic metric (verify manually): Q + +## Implementation Priority +| Rank | Fix | Type | Metrics | Risk | Score rationale | +|------|-----|------|---------|------|-----------------| +| 1 | [name] | prompt | M1, M2 | Low | [one-line: why this ranks here] | +| 2 | ... | ... | ... | ... | ... | + +--- + +## Skill: [insight ID(s)] — [title] +**Summary:** [one-line description of what the skill addresses] +**Verdict:** `prompt fix` | `code fix` | `discard` +**Classification path:** [which branch of the decision tree — e.g., "Agent has tools and data but acts wrong → prompt fix"] +**Rationale:** [why this verdict — reference specific trace evidence from insights] +**Risk:** None | Low | Medium | High — [one-sentence justification] +**Risk mitigation:** [for Medium/High only — what to watch for or how to preserve working cases] +**Recommendation:** [specific change to make] +**Files to modify:** [list of files, for code fixes] +**Metric link:** [which metrics would move, with baseline values] +**Already-handled check:** [what you grepped, what existing prompt text you found, verdict] + +--- +[repeat for each insight] + +## Consolidated Prompt Skills +[After all per-insight entries, list the final merged prompt skill texts in priority order, ready for Stage 7 to implement] + +## Monitor Items (Non-Actionable Only) +[Only insights that were triaged as genuinely non-actionable — e.g., the agent cannot change this behavior, or the insight is noise. Unmeasurable insights that are still real problems should appear in the priority list above, NOT here.] +``` + +Group related insights under cluster headings when they address the same underlying behavior. For merged insights, list all constituent insight IDs in the heading. + +## Outputs + +- `eval/action_plan.md` diff --git a/.claude/skills/kayba-pipeline/stage-6-hitl/SKILL.md b/.claude/skills/kayba-pipeline/stage-6-hitl/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..3f7820b0c257a3a5a153ae53aac16e183b0e80f3 --- /dev/null +++ b/.claude/skills/kayba-pipeline/stage-6-hitl/SKILL.md @@ -0,0 +1,258 @@ +--- +name: kayba-stage-6-hitl +description: Human-In-The-Loop gate that presents the action plan with full context, collects an informed approval/modification/rejection decision, and records the outcome. Trigger when the user says "run stage 6", "HITL review", "approve action plan", or when invoked by the kayba-pipeline orchestrator. Requires eval/action_plan.md and eval/baseline_metrics.md to exist. +--- + +# Stage 6: Human-In-The-Loop Gate + +Present the action plan with enough context for an informed decision, collect the user's approval, and record the outcome. + +The goal is not rubber-stamping. The user must receive enough information to genuinely evaluate, modify, or reject the plan -- even if they have not seen Stages 1-5. + +## Inputs + +- `eval/action_plan.md` -- the prioritized action plan from Stage 5 +- `eval/baseline_metrics.md` -- the evaluation rubric with baseline values +- `eval/baseline_metrics.json` -- raw metric data (for exact numerator/denominator counts) +- `eval/stage1_insights_summary.md` -- original insights (for trace evidence references) + +Read all four files before starting. + +## Process + +### 1. Build the executive summary + +Compute and present the following counts from the action plan: + +- Total insights analyzed (raw count before deduplication) +- Distinct actionable items after deduplication +- Breakdown: prompt fixes, code fixes, discarded +- Discard rate with one-line reason per discard (e.g., "5ac7f4ce: efficiency optimization, conflicts with turn discipline constraint") + +Format: + +``` +EXECUTIVE SUMMARY +----------------- +Insights analyzed: 19 (raw) -> 12 distinct after dedup +Actionable: 9 (8 prompt fixes, 1 code fix) +Discarded: 3 (reasons listed below) + +Discards: + - 5ac7f4ce (Upfront Info Collection): conflicts with higher-priority turn discipline + - fe2d51cb (Proactive Reservation Lookup): already default behavior, no failure evidence + - 1fa1b826 (Cancellation Denial Enumeration): subsumed into cancellation checklist +``` + +### 2. Present the top 3 highest-impact changes + +For each of the top 3 fixes by priority, present: + +**Before/after behavior** -- use concrete examples from actual traces referenced in the insights. Quote the specific agent behavior that was wrong (before) and describe what the agent should do instead (after). Reference the trace task ID. + +**Target metric delta** -- which metric(s) this fix targets, the current baseline value, and the expected direction. Do not fabricate precise target numbers. Use the format: "M1: 41.4% -> higher (target: 90%+)" only when the action plan provides a target; otherwise use "M1: 41.4% -> up". + +**Risk rating** -- assess each fix: +- `Low` -- additive prompt instruction, no behavioral side effects expected +- `Medium` -- changes existing behavior, could affect adjacent workflows +- `High` -- modifies code/infrastructure, or could degrade a metric while improving another + +Format each as a numbered block: + +``` +#1: Turn Discipline (covers 55c00c40, d9683144) + Type: prompt fix + Metrics: M1 (41.4% -> up), M2 (20.7% -> up) + Risk: Low + + BEFORE (task_1, task_5, task_7, ...): + Agent batches 2-3 tool calls per turn (e.g., get_reservation + get_flight_status + in a single response). Also includes user-facing text alongside tool calls. + + AFTER: + Exactly one tool call per response. No user-facing content in tool-call turns. + Agent processes each result before making the next call. +``` + +### 3. Present the full prioritized fix list + +Display all non-discarded fixes in a table: + +``` +| Priority | Fix Name | Type | Target Metrics | Risk | Effort | +|----------|-----------------------------------|------------|-----------------|--------|--------| +| 1 | Turn Discipline | prompt fix | M1, M2 | Low | Low | +| 2 | Post-Confirmation Execution | prompt fix | M3 | Low | Low | +| 3 | Cancellation Checklist | prompt fix | M5 | Low | Low | +| ... | ... | ... | ... | ... | ... | +``` + +Effort ratings: +- `Low` -- single prompt addition, under 5 lines +- `Medium` -- multiple prompt additions or minor code change +- `High` -- significant code changes, new metric implementation, or architectural changes + +### 4. Present "What we are NOT fixing and why" + +List every discarded insight with: +- Insight ID and name +- One-line reason for discard +- What would change your mind (under what conditions should this be revisited) + +This section exists so the user can override a discard if they disagree. + +### 5. Flag small-sample and low-confidence items + +Any metric with denominator < 5 must be explicitly called out: + +``` +LOW-CONFIDENCE METRICS (small sample size): + - M5 (Cancellation Policy Compliance): based on 2 observations -- directional only + - M6 (Compensation Execution Rate): based on 1 observation -- directional only + +Fixes targeting these metrics (Cancellation Checklist, Compensation Rules) are +still recommended because the policy violations are clear from trace evidence, +but the measured improvement may not be statistically meaningful until the +trace corpus grows. +``` + +Also flag any fix where the action plan notes uncertainty or partial evidence. + +### 6. Show the insight-to-fix traceability chain + +For each fix, present the chain: insight -> metric -> fix -> expected improvement. This can be a compact list or a table. The purpose is to let the user verify that nothing was lost or invented between stages. + +``` +TRACEABILITY: + 55c00c40 (Tool Call Discipline) -> M1, M2 -> Skill 1 (Turn Discipline) -> M1 up, M2 up + 6ea141e1 (Execution Discipline) -> M3 -> Skill 2 (Post-Confirmation) -> M3 up + 0f4a952b + 6ce88ebb (Cancellation) -> M5 -> Skill 3 (Cancellation Checklist) -> M5 up + ... +``` + +### 7. Collect the decision + +Present exactly three options: + +``` +OPTIONS: + [A] Approve all -- implement all 9 fixes as described + [B] Approve with modifications -- review each fix individually + [C] Reject -- return to Stage 5 with feedback +``` + +Use the appropriate mechanism to collect the user's choice (direct question or AskUserQuestion if available). + +#### If the user selects [A] Approve all + +Record the decision and proceed. No further interaction needed. + +#### If the user selects [B] Approve with modifications + +Walk through each fix individually, in priority order. For each fix, present: +- The fix name, type, and target metrics +- The recommended prompt/code change (quote the exact text from the action plan) +- Risk and effort ratings + +Then ask: "Approve / Skip / Modify?" + +- **Approve** -- keep as-is +- **Skip** -- remove from the plan, record reason +- **Modify** -- ask the user what to change, record the original and the modification + +After walking through all fixes, present a summary of changes: +- Fixes approved as-is: N +- Fixes skipped: M (list with reasons) +- Fixes modified: K (list with what changed) + +Ask for final confirmation: "Proceed with this modified plan?" + +Then update `eval/action_plan.md`: +- Remove skipped fixes (move to a "Skipped by HITL" section at the bottom with reasons) +- Update modified fixes with the user's changes, preserving the original recommendation in a "Original recommendation" sub-field +- Add a header note: "Modified during HITL review on [date]. See eval/stage6_decision.md for details." + +#### If the user selects [C] Reject + +Ask the user for specific feedback: +- What was wrong with the plan? +- Which insights or metrics should be reconsidered? +- Any new constraints or priorities? + +Record the feedback in `eval/stage6_decision.md` and signal that Stage 5 should be re-run with the user's feedback incorporated. + +## Output format + +### eval/stage6_decision.md + +Write this file regardless of which option was selected. + +```markdown +# Stage 6: HITL Decision Record + +## Date +[timestamp] + +## Decision +[Approve all | Approve with modifications | Reject] + +## What was presented +- Total insights: N (M distinct after dedup) +- Actionable fixes: X (Y prompt, Z code) +- Discarded: W +- Metrics: [list metric IDs and baselines] +- Low-confidence flags: [list metrics with small denominators] + +## Top 3 changes presented +1. [fix name] -- [type] -- targets [metrics] -- risk [rating] +2. ... +3. ... + +## Decision details + +### If Approve all: +User approved all N fixes without modification. +Reasoning: [any reasoning the user provided, or "No additional reasoning provided"] + +### If Approve with modifications: +| Fix | Original Status | Decision | Reason | +|-----|----------------|----------|--------| +| Turn Discipline | Priority 1 | Approved | -- | +| Compensation Rules | Priority 5 | Modified | User changed wording to... | +| Cabin Change Rules | Priority 8 | Skipped | User considers low priority | + +Modifications detail: +- [Fix name]: Original: "..." -> Modified: "..." -- User rationale: "..." + +### If Reject: +User feedback: [verbatim feedback] +Specific concerns: [list] +Re-run instructions for Stage 5: [what to change] + +## Traceability snapshot +[Copy of the traceability chain from step 6, so the decision record is self-contained] +``` + +### eval/action_plan.md (updated, only if modifications were made) + +If the user selected [B] and made changes: +- Add a modification header at the top of the file +- Update individual fix entries with user changes +- Move skipped fixes to a "Skipped by HITL" section +- Preserve original recommendations as sub-fields for auditability + +## Rules + +- Do NOT auto-approve. The entire point of this stage is human judgment. +- Do NOT summarize so aggressively that the user cannot evaluate. When in doubt, include more context. +- Do NOT proceed to Stage 7 until a clear approval (full or modified) is recorded. +- Do NOT modify `eval/action_plan.md` unless the user explicitly requests modifications. +- Do NOT skip the small-sample warnings. If M5 has denominator 2 and M6 has denominator 1, the user must know this. +- Do NOT fabricate target metric values. Use targets from the action plan when available; otherwise state direction only. +- Always present the "What we are NOT fixing" section. Omitting discards hides information the user needs. +- If the user asks clarifying questions, answer them fully before re-presenting the decision options. + +## Outputs + +- `eval/stage6_decision.md` -- full record of what was presented, decided, and why +- `eval/action_plan.md` -- updated only if the user selected "Approve with modifications" diff --git a/.claude/skills/kayba-pipeline/stage-7-fixer/SKILL.md b/.claude/skills/kayba-pipeline/stage-7-fixer/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..809adf043eadf4b9086838351d3fc5fbce8a72ca --- /dev/null +++ b/.claude/skills/kayba-pipeline/stage-7-fixer/SKILL.md @@ -0,0 +1,191 @@ +--- +name: kayba-stage-7-fixer +description: Implement the approved fixes from the action plan and log all changes. Trigger when the user says "run stage 7", "implement fixes", "apply action plan", or when invoked by the kayba-pipeline orchestrator. Requires eval/action_plan.md to exist. +--- + +# Stage 7: Fix Implementation + +Implement every non-discarded fix from the approved action plan. + +## Inputs + +- `eval/action_plan.md` -- the approved action plan from Stage 5 (possibly modified during HITL in Stage 6) +- `eval/stage6_decision.md` -- if it exists, the HITL decision record from Stage 6 (contains user modifications) +- `eval/baseline_metrics.json` -- the pre-fix baseline metrics from Stage 3 (for reference in changes log) + +Read the action plan and stage6 decision (if present) before starting. + +## Pre-flight: Git Safety Checkpoint + +Before making ANY changes to source files: + +1. Run `git status` to confirm the working tree state +2. Create a safety commit or stash: + ``` + git stash push -m "pre-pipeline-fixes-$(date +%Y%m%d-%H%M%S)" + ``` + If there are no uncommitted changes to stash, create a lightweight tag instead: + ``` + git tag pre-pipeline-fixes-$(date +%Y%m%d-%H%M%S) + ``` +3. Record the stash ref or tag name in `eval/changes_log.md` under a "Rollback" section so the user can restore if needed + +This ensures every fix is reversible with a single `git stash pop` or `git checkout`. + +## Pre-flight: HITL Modification Check + +If `eval/stage6_decision.md` exists: + +1. Read it and identify any items the user modified, added, or re-prioritized during Stage 6 +2. Build a set of `HITL_MODIFIED_IDS` -- the insight/skill IDs that the user changed +3. When logging each fix later, tag modified items with `[HITL-MODIFIED]` in the changes log so reviewers know which fixes reflect user judgment vs. the original pipeline output + +If the file does not exist, assume no HITL modifications were made. + +## Pre-flight: Conflict Scan + +Before implementing any fixes, scan the action plan for potential conflicts: + +1. Build a map of `file_path -> [fix IDs that touch it]` +2. If two or more fixes modify the same file, flag them as **co-located** +3. If two or more fixes modify the same section (within ~20 lines of each other), flag them as **overlapping** +4. For overlapping fixes: plan to apply them sequentially in priority order, re-reading the file between each edit to ensure the second fix still makes sense on top of the first +5. Log any detected conflicts at the top of `eval/changes_log.md` under a "Conflict Notes" section + +## Process + +Work through the action plan in priority order. For each non-discarded fix: + +### 1. Understand the fix + +- Read the recommendation carefully +- Read the referenced files in the codebase +- Understand the surrounding code before making changes +- Check if this fix was flagged as co-located or overlapping in the conflict scan. If overlapping with a previously-applied fix, re-read the target file to see the current state after prior edits + +### 2. Implement the change + +**For code fixes:** +- Find the relevant files +- Make the minimal, targeted change described in the recommendation +- Do not refactor surrounding code unless the fix obviously breaks without light adjacent cleanup (e.g., an import is missing, a variable was renamed). If you make adjacent cleanup, log it explicitly as "adjacent cleanup" in the change entry +- Do not add features beyond what was recommended + +**For prompt fixes:** +- Find the system prompt file (use domain context from Stage 2 if needed) +- Add the recommended instruction at the appropriate location +- Do not rewrite existing prompt text unless the recommendation explicitly says to + +### 3. Log the change + +Append to `eval/changes_log.md`: + +```markdown +## Fix N: [skill/insight name] [HITL-MODIFIED if applicable] +**Type:** code fix | prompt fix +**Verdict from action plan:** [quote the recommendation] +**Files modified:** +- `path/to/file.py` -- [what changed and why] +**Before:** +\``` +[relevant snippet before change] +\``` +**After:** +\``` +[relevant snippet after change] +\``` +**Linked metrics:** [which metrics this should improve] +**Conflict notes:** [if this fix overlapped with another, note it here; otherwise "none"] +``` + +### 4. Handle uncertainty (NEEDS REVIEW workflow) + +If a fix requires changes you are unsure about: + +1. Do NOT implement it +2. Log it as `NEEDS REVIEW` in the changes log with: + - What specifically is unclear + - What information would resolve the ambiguity + - The files and lines you examined +3. **Continue to the next fix** -- do not block the pipeline +4. At the end of all fixes, collect all NEEDS REVIEW items into a dedicated section (see Output format below). The pipeline does NOT stop; these items are presented to the user after all other fixes are applied. + +## Post-Fix: Next Steps (Do NOT Re-run Baselines) + +Do NOT re-run `compute_baselines.py` as part of this stage. The baseline metrics were computed against the original traces, which reflect old agent behavior. Re-running against the same traces will show zero movement for prompt-only fixes and is misleading. + +Instead, after all fixes are applied, include a **Next Steps** section in the changes log that tells the user: + +1. Generate new traces by running the agent with the updated prompts/code +2. Then re-run baselines against the new traces: + ```bash + python eval/compute_baselines.py --traces-dir <new_traces_folder> --output eval/post_fix_metrics.json + ``` +3. Compare `eval/post_fix_metrics.json` against `eval/baseline_metrics.json` to measure actual improvement + +## Rules + +- Do NOT modify trace files +- Do NOT make changes beyond what the action plan recommends (except adjacent cleanup logged explicitly) +- Make minimal, targeted changes -- don't clean up or refactor surrounding code +- If the action plan says "discard", skip that entry entirely +- You MAY write `eval/changes_log.md` as the primary output +- Do NOT run `eval/compute_baselines.py` -- baselines should only be re-computed after new traces are generated with the updated agent + +## Output format + +Write `eval/changes_log.md`: + +```markdown +# Changes Log + +## Rollback +- **Safety ref:** `git stash` ref or tag name +- **To undo all fixes:** `git stash pop` or `git checkout <tag>` + +## Conflict Notes +- [any file/region conflicts detected, or "No conflicts detected"] + +## Summary +- Code fixes applied: N +- Prompt fixes applied: M +- Skipped / needs review: K +- HITL-modified items: J + +--- + +## Fix 1: [skill name] +... + +## Fix 2: [skill name] +... + +--- + +## Needs Review +[Collected list of all NEEDS REVIEW items with context, or "None -- all fixes applied successfully"] + +For each NEEDS REVIEW item: +- **Fix N: [skill name]** +- **What is unclear:** [specific ambiguity] +- **What would resolve it:** [information needed] +- **Files examined:** [paths and lines] + +--- + +## Next Steps + +To measure actual improvement: +1. Generate new traces by running the agent with the updated prompts/code +2. Re-run baselines: +\```bash +python eval/compute_baselines.py --traces-dir <new_traces_folder> --output eval/post_fix_metrics.json +\``` +3. Compare `eval/post_fix_metrics.json` against `eval/baseline_metrics.json` to measure metric deltas +``` + +## Outputs + +- `eval/changes_log.md` -- full log of all changes, conflicts, NEEDS REVIEW items, and next steps +- The actual code/prompt changes in the repository +- A git stash or tag for rollback diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..aac1a386cd6f6114edd6adfe46dc0f35f50a40de --- /dev/null +++ b/.env.example @@ -0,0 +1,69 @@ +# API Keys for LLM Providers +# Copy this file to .env and add your actual API keys + +# OpenAI +OPENAI_API_KEY=your-openai-api-key-here + +# Anthropic (Claude) +ANTHROPIC_API_KEY=your-anthropic-api-key-here + +# Google (Gemini) +GOOGLE_API_KEY=your-google-api-key-here + +# DeepSeek +DEEPSEEK_API_KEY=your-deepseek-api-key-here + +# Ollama Cloud +OLLAMA_API_KEY=your_api_key + +# Cohere +COHERE_API_KEY=your-cohere-api-key-here + +# Bedrock bearer token (passed as api_key to LiteLLM, not as AWS_ACCESS_KEY_ID) +BEDROCK_API_KEY=your-bedrock-api-key-here + +# Azure OpenAI (optional) +AZURE_API_KEY=your-azure-api-key-here +AZURE_API_BASE=https://your-resource.openai.azure.com +AZURE_API_VERSION=2024-02-15-preview + +# AWS Bedrock (optional) +AWS_ACCESS_KEY_ID=your-aws-access-key +AWS_SECRET_ACCESS_KEY=your-aws-secret-key +AWS_REGION_NAME=us-east-1 + +# Hugging Face (optional) +HUGGINGFACE_API_KEY=your-huggingface-api-key-here + +# Replicate (optional) +REPLICATE_API_KEY=your-replicate-api-key-here + +# Together AI (optional) +TOGETHER_API_KEY=your-together-api-key-here + +# Model Configuration (optional) +DEFAULT_MODEL=gpt-4o-mini +DEFAULT_TEMPERATURE=0.0 +DEFAULT_MAX_TOKENS=512 + +# Cost Tracking (optional) +TRACK_COSTS=true +MAX_BUDGET=10.0 + +# Opik Observability (optional) +# Disable Opik tracing when not running a local Opik server +# Either variable works: +# OPIK_ENABLED=false +# OPIK_DISABLED=true + +# Benchmark Configuration +# Cache directories for benchmark data (optional - defaults to ~/.cache/huggingface) +BENCHMARK_CACHE_DIR=/path/to/benchmark/cache +HF_DATASETS_CACHE=/path/to/huggingface/cache +HF_HUB_CACHE=/path/to/huggingface/hub/cache + +# AppWorld configuration (required for AppWorld benchmark) +APPWORLD_ROOT=/path/to/appworld/data + +# Results output directory (optional) +BENCHMARK_RESULTS_DIR=./benchmark_results diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..af4a7c4b030ddbbdb33ce6fc9fd7f722ed0c88df 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +assets/kayba-banner.png filter=lfs diff=lfs merge=lfs -text +benchmarks/tasks/tau_bench/Tau2Benchmark[[:space:]]Result[[:space:]]Haiku4.5.png filter=lfs diff=lfs merge=lfs -text +examples/seahorse-emoji-ace.gif filter=lfs diff=lfs merge=lfs -text +mlflow.db filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000000000000000000000000000000000..3aa62bc5c521fbda4188c6a4c7dcb998196c5ca2 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,55 @@ +name: Docs + +on: + push: + branches: + - main + tags: + - "v*" + workflow_dispatch: + inputs: + version: + description: "Version to deploy (e.g. 0.8)" + required: true + default: "dev" + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for mike versioning + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install docs dependencies + run: pip install mkdocs-material mike + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Deploy versioned docs (on tag) + if: startsWith(github.ref, 'refs/tags/v') + run: | + VERSION=${GITHUB_REF#refs/tags/v} + mike deploy --push --update-aliases $VERSION latest + mike set-default --push latest + + - name: Deploy dev docs (on main push) + if: github.ref == 'refs/heads/main' + run: mike deploy --push dev + + - name: Deploy manually triggered version + if: github.event_name == 'workflow_dispatch' + run: | + mike deploy --push --update-aliases ${{ github.event.inputs.version }} latest + mike set-default --push latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000000000000000000000000000000000000..e03e5f899a92c881370f20257d0d311fd10081b5 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,290 @@ +name: Publish Packages + +on: + release: + types: [published] + workflow_dispatch: + inputs: + test_pypi: + description: 'Publish to TestPyPI instead of PyPI' + required: false + default: 'false' + type: choice + options: + - 'true' + - 'false' + +jobs: + # ── ace-framework (Python) ────────────────────────────────────────── + + build-ace: + name: Build ace-framework + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build + + - name: Build package + run: python -m build + + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: ace-framework-dist + path: dist/ + + # ── kayba-tracing (Python) ───────────────────────────────────────── + + build-kayba-tracing: + name: Build kayba-tracing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build + + - name: Build package + run: python -m build sdk/python + + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: kayba-tracing-dist + path: sdk/python/dist/ + + # ── @kayba_ai/tracing (TypeScript) ──────────────────────────────────── + + build-kayba-tracing-ts: + name: Build @kayba_ai/tracing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + working-directory: sdk/typescript + + - name: Build + run: npm run build + working-directory: sdk/typescript + + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: kayba-tracing-ts-dist + path: | + sdk/typescript/dist/ + sdk/typescript/package.json + + # ── @kayba_ai/openclaw-tracing (TypeScript) ─────────────────────────── + + build-openclaw-tracing: + name: Build @kayba_ai/openclaw-tracing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + working-directory: sdk/openclaw + + - name: Build + run: npm run build + working-directory: sdk/openclaw + + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: openclaw-tracing-dist + path: | + sdk/openclaw/dist/ + sdk/openclaw/package.json + sdk/openclaw/openclaw.plugin.json + sdk/openclaw/README.md + + # ── Publish to TestPyPI ──────────────────────────────────────────── + + publish-to-testpypi: + name: Publish ace-framework to TestPyPI + if: github.event.inputs.test_pypi == 'true' || github.event_name == 'workflow_dispatch' + needs: [build-ace] + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/project/ace-framework/ + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: ace-framework-dist + path: dist/ + + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + skip-existing: true + + publish-kayba-tracing-to-testpypi: + name: Publish kayba-tracing to TestPyPI + if: github.event.inputs.test_pypi == 'true' || github.event_name == 'workflow_dispatch' + needs: [build-kayba-tracing] + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/project/kayba-tracing/ + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: kayba-tracing-dist + path: dist/ + + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + skip-existing: true + + # ── Publish to PyPI ──────────────────────────────────────────────── + + publish-to-pypi: + name: Publish ace-framework to PyPI + if: github.event_name == 'release' + needs: [build-ace] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/ace-framework/ + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: ace-framework-dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + publish-kayba-tracing-to-pypi: + name: Publish kayba-tracing to PyPI + if: github.event_name == 'release' + needs: [build-kayba-tracing] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/kayba-tracing/ + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: kayba-tracing-dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + # ── Publish to npm ───────────────────────────────────────────────── + + publish-to-npm: + name: Publish @kayba_ai/tracing to npm + if: github.event_name == 'release' + needs: [build-kayba-tracing-ts] + runs-on: ubuntu-latest + environment: + name: npm + url: https://www.npmjs.com/package/@kayba_ai/tracing + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + working-directory: sdk/typescript + + - name: Build + run: npm run build + working-directory: sdk/typescript + + - name: Publish to npm + run: npm publish --access public + working-directory: sdk/typescript + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + publish-openclaw-tracing-to-npm: + name: Publish @kayba_ai/openclaw-tracing to npm + if: github.event_name == 'release' + needs: [build-openclaw-tracing] + runs-on: ubuntu-latest + environment: + name: npm + url: https://www.npmjs.com/package/@kayba_ai/openclaw-tracing + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + working-directory: sdk/openclaw + + - name: Build + run: npm run build + working-directory: sdk/openclaw + + - name: Publish to npm + run: npm publish --access public + working-directory: sdk/openclaw + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000000000000000000000000000000000..1e5afff38c94254b95367f464c35317675ba4726 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,47 @@ +name: Tests + +on: + push: + branches: [ main, develop, lanzelot-dev ] + pull_request: + branches: [ main ] + workflow_dispatch: # Allow manual triggering + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ['3.12'] # Matches pyproject.toml requires-python == 3.12.* + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Install dependencies + run: uv sync --extra mcp --extra tracing + + - name: Run tests with coverage + run: | + uv run pytest -m "not requires_api" + + - name: Upload coverage reports (Linux only) + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: htmlcov/ + retention-days: 30 + + - name: Type checking (Linux only) + if: matrix.os == 'ubuntu-latest' + run: | + uv run mypy ace/ --ignore-missing-imports \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..51a41543b901f5265aee98185c149a3e13431492 --- /dev/null +++ b/.gitignore @@ -0,0 +1,124 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store + +# Project specific +*.pdf +*.png +!benchmarks/tasks/tau_bench/*.png +!examples/browser-use/**/*.png +!assets/*.png +ACE_IMPROVEMENTS.md +ACE_ROADMAP.md +DEMO_TODO.md +*.egg-info/ +reports/ +docs/method_outline.md +logs/ + +# Generated skillbook and result files +*_skillbook.json +ace_domain_skillbook.json +kayba_learned.json +kayba_test_skillbook.json +my_agent.json +my_trained_agent.json +*.log + +# Checkpoint files (generated during training) +**/checkpoints/*.json +*_checkpoint_*.json +*_latest.json +evaluation_results/*.json + +# Benchmark data and cache +benchmark_results/ +benchmark_cache/ +tau_benchmark_results/ +results/ +appworld_data/ +*.arrow +*.parquet +huggingface_cache/ + +# MkDocs build output +site/ + +# Sensitive/private data (NEVER commit!) +.private/ + +# GSD planning artifacts (NEVER commit!) +.planning/ + +# Node build artifacts +node_modules/ + +# Local scratch / experiment outputs +tmp/ + +spyfly-traces \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..5b0ef2c43fa3ef069660e4ce011d0cf4ec7b86a8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ace-eval"] + path = ace-eval + url = git@github.com:kayba-ai/ace-eval.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3a28f2099206f855d51b713a6741c46427b76f16 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + # Black - code formatting (auto-fixes) + - repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + args: [--line-length=88] + language_version: python3.12 + + # MyPy - type checking (blocks commits on errors) + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.11.2 + hooks: + - id: mypy + args: [--ignore-missing-imports, --warn-unused-configs] + additional_dependencies: + - types-requests + - types-pyyaml + files: ^ace/ + language_version: python3.12 diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md new file mode 100644 index 0000000000000000000000000000000000000000..4d1f2a4809a1f2d4717f8935bc10bfa1745fc072 --- /dev/null +++ b/.specify/memory/constitution.md @@ -0,0 +1,121 @@ +<!-- + Sync Impact Report + ================== + Version change: 1.0.0 → 1.1.0 + Principles added: + - IV. Clean & Modular Code + Principles unchanged: + - I. Ease of Use First + - II. Practical Value + - III. Simplicity + Sections unchanged: + - Development Standards + - Quality Gates + - Governance + Removed sections: None + Templates validated: + ✅ .specify/templates/plan-template.md — Constitution Check section compatible + ✅ .specify/templates/spec-template.md — No conflicts + ✅ .specify/templates/tasks-template.md — No conflicts + ✅ .specify/templates/checklist-template.md — No conflicts + Follow-up TODOs: None +--> + +# ACE Framework Constitution + +## Core Principles + +### I. Ease of Use First + +Every public API, integration, and workflow MUST prioritize developer +experience above all else. + +- New users MUST be able to install and run a working example in under + 5 minutes with no more than 3 lines of code. +- Sensible defaults MUST be provided for every configuration option. + Users MUST NOT be required to understand internals to get started. +- Breaking changes to the public API MUST follow deprecation warnings + for at least one minor release before removal. +- Documentation MUST include a copy-pasteable quick start for every + integration (LiteLLM, LangChain, browser-use, Claude Code). + +### II. Practical Value + +Every feature MUST solve a real, demonstrable problem for users +building AI agents. + +- Features MUST NOT be added speculatively. Each addition MUST have a + concrete use case tied to agent improvement or developer workflow. +- Performance claims MUST be backed by reproducible benchmarks or + examples. No unsubstantiated marketing language in docs or code. +- Integration wrappers MUST add measurable value (learning, skillbook + evolution) beyond what the wrapped framework already provides. + +### III. Simplicity + +Prefer the simplest solution that works. Complexity MUST be justified. + +- YAGNI: Do not build for hypothetical future requirements. Three + similar lines of code are better than a premature abstraction. +- New abstractions MUST be used in at least two places before + extraction into a shared utility. +- Dependencies MUST be kept minimal. Optional extras (observability, + LangChain, transformers) stay optional — the core install MUST + remain lightweight. + +### IV. Clean & Modular Code + +All code MUST be clean, modular, and extensible. + +- Modules MUST have a single, clear responsibility. Each file MUST + do one thing well and expose a well-defined interface. +- Public APIs MUST be designed for extension without modification. + New integrations, LLM providers, and adapters MUST be addable + without changing existing code (open/closed principle). +- Internal boundaries MUST be respected: core library (`ace/`), + integrations (`ace/integrations/`), LLM providers + (`ace/llm_providers/`), and observability (`ace/observability/`) + MUST NOT have circular dependencies. +- Functions and classes MUST be small enough to understand at a + glance. If a function requires scrolling, it MUST be decomposed. + +## Development Standards + +- **Language**: Python 3.12 with type hints on all public APIs. +- **Formatting**: Black (line length 88). All code MUST pass + `black --check` before merge. +- **Testing**: pytest with coverage enforcement (`--cov-fail-under=25`). + New features MUST include tests. Bug fixes MUST include regression + tests. +- **Distribution**: PyPI package `ace-framework`. Core install MUST NOT + exceed ~150MB. Heavy dependencies belong in optional extras. +- **Commit style**: Conventional Commits (`feat(scope): subject`). + +## Quality Gates + +- All PRs MUST pass CI (formatting, type checks, test suite) before + merge. +- Public API changes MUST update relevant documentation (README, + docstrings, quick start guides). +- Benchmark results MUST NOT regress without explicit justification in + the PR description. +- Skillbook format changes MUST maintain backward compatibility with + existing saved skillbooks or provide a migration path. + +## Governance + +This constitution is the highest-authority document for the ACE +Framework project. All design decisions, PRs, and code reviews MUST +verify compliance with these principles. + +- **Amendments**: Any change to this constitution MUST be documented + with a version bump, rationale, and updated `LAST_AMENDED_DATE`. +- **Versioning**: MAJOR for principle removals or redefinitions, MINOR + for new principles or material expansions, PATCH for clarifications. +- **Compliance**: Use `CLAUDE.md` for runtime development guidance. + This constitution defines the non-negotiable rules that `CLAUDE.md` + guidance MUST NOT contradict. +- **Review**: Constitution compliance SHOULD be checked at the start + of each feature planning cycle (`/speckit.plan` Constitution Check). + +**Version**: 1.1.0 | **Ratified**: 2026-02-25 | **Last Amended**: 2026-02-25 diff --git a/.specify/scripts/bash/check-prerequisites.sh b/.specify/scripts/bash/check-prerequisites.sh new file mode 100644 index 0000000000000000000000000000000000000000..594d5a38337a50f552266b4d6e5ab7ee55b700ef --- /dev/null +++ b/.specify/scripts/bash/check-prerequisites.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash + +# Consolidated prerequisite checking script +# +# This script provides unified prerequisite checking for Spec-Driven Development workflow. +# It replaces the functionality previously spread across multiple scripts. +# +# Usage: ./check-prerequisites.sh [OPTIONS] +# +# OPTIONS: +# --json Output in JSON format +# --require-tasks Require tasks.md to exist (for implementation phase) +# --include-tasks Include tasks.md in AVAILABLE_DOCS list +# --paths-only Only output path variables (no validation) +# --help, -h Show help message +# +# OUTPUTS: +# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]} +# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md +# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc. + +set -e + +# Parse command line arguments +JSON_MODE=false +REQUIRE_TASKS=false +INCLUDE_TASKS=false +PATHS_ONLY=false + +for arg in "$@"; do + case "$arg" in + --json) + JSON_MODE=true + ;; + --require-tasks) + REQUIRE_TASKS=true + ;; + --include-tasks) + INCLUDE_TASKS=true + ;; + --paths-only) + PATHS_ONLY=true + ;; + --help|-h) + cat << 'EOF' +Usage: check-prerequisites.sh [OPTIONS] + +Consolidated prerequisite checking for Spec-Driven Development workflow. + +OPTIONS: + --json Output in JSON format + --require-tasks Require tasks.md to exist (for implementation phase) + --include-tasks Include tasks.md in AVAILABLE_DOCS list + --paths-only Only output path variables (no prerequisite validation) + --help, -h Show this help message + +EXAMPLES: + # Check task prerequisites (plan.md required) + ./check-prerequisites.sh --json + + # Check implementation prerequisites (plan.md + tasks.md required) + ./check-prerequisites.sh --json --require-tasks --include-tasks + + # Get feature paths only (no validation) + ./check-prerequisites.sh --paths-only + +EOF + exit 0 + ;; + *) + echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2 + exit 1 + ;; + esac +done + +# Source common functions +SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get feature paths and validate branch +eval $(get_feature_paths) +check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 + +# If paths-only mode, output paths and exit (support JSON + paths-only combined) +if $PATHS_ONLY; then + if $JSON_MODE; then + # Minimal JSON paths payload (no validation performed) + printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ + "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS" + else + echo "REPO_ROOT: $REPO_ROOT" + echo "BRANCH: $CURRENT_BRANCH" + echo "FEATURE_DIR: $FEATURE_DIR" + echo "FEATURE_SPEC: $FEATURE_SPEC" + echo "IMPL_PLAN: $IMPL_PLAN" + echo "TASKS: $TASKS" + fi + exit 0 +fi + +# Validate required directories and files +if [[ ! -d "$FEATURE_DIR" ]]; then + echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2 + echo "Run /speckit.specify first to create the feature structure." >&2 + exit 1 +fi + +if [[ ! -f "$IMPL_PLAN" ]]; then + echo "ERROR: plan.md not found in $FEATURE_DIR" >&2 + echo "Run /speckit.plan first to create the implementation plan." >&2 + exit 1 +fi + +# Check for tasks.md if required +if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then + echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2 + echo "Run /speckit.tasks first to create the task list." >&2 + exit 1 +fi + +# Build list of available documents +docs=() + +# Always check these optional docs +[[ -f "$RESEARCH" ]] && docs+=("research.md") +[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") + +# Check contracts directory (only if it exists and has files) +if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then + docs+=("contracts/") +fi + +[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") + +# Include tasks.md if requested and it exists +if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then + docs+=("tasks.md") +fi + +# Output results +if $JSON_MODE; then + # Build JSON array of documents + if [[ ${#docs[@]} -eq 0 ]]; then + json_docs="[]" + else + json_docs=$(printf '"%s",' "${docs[@]}") + json_docs="[${json_docs%,}]" + fi + + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" +else + # Text output + echo "FEATURE_DIR:$FEATURE_DIR" + echo "AVAILABLE_DOCS:" + + # Show status of each potential document + check_file "$RESEARCH" "research.md" + check_file "$DATA_MODEL" "data-model.md" + check_dir "$CONTRACTS_DIR" "contracts/" + check_file "$QUICKSTART" "quickstart.md" + + if $INCLUDE_TASKS; then + check_file "$TASKS" "tasks.md" + fi +fi diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh new file mode 100644 index 0000000000000000000000000000000000000000..c9c5ced545a97893a36f4788cc1d8443fff80296 --- /dev/null +++ b/.specify/scripts/bash/common.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# Common functions and variables for all scripts + +# Get repository root, with fallback for non-git repositories +get_repo_root() { + if git rev-parse --show-toplevel >/dev/null 2>&1; then + git rev-parse --show-toplevel + else + # Fall back to script location for non-git repos + local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + (cd "$script_dir/../../.." && pwd) + fi +} + +# Get current branch, with fallback for non-git repositories +get_current_branch() { + # First check if SPECIFY_FEATURE environment variable is set + if [[ -n "${SPECIFY_FEATURE:-}" ]]; then + echo "$SPECIFY_FEATURE" + return + fi + + # Then check git if available + if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then + git rev-parse --abbrev-ref HEAD + return + fi + + # For non-git repos, try to find the latest feature directory + local repo_root=$(get_repo_root) + local specs_dir="$repo_root/specs" + + if [[ -d "$specs_dir" ]]; then + local latest_feature="" + local highest=0 + + for dir in "$specs_dir"/*; do + if [[ -d "$dir" ]]; then + local dirname=$(basename "$dir") + if [[ "$dirname" =~ ^([0-9]{3})- ]]; then + local number=${BASH_REMATCH[1]} + number=$((10#$number)) + if [[ "$number" -gt "$highest" ]]; then + highest=$number + latest_feature=$dirname + fi + fi + fi + done + + if [[ -n "$latest_feature" ]]; then + echo "$latest_feature" + return + fi + fi + + echo "main" # Final fallback +} + +# Check if we have git available +has_git() { + git rev-parse --show-toplevel >/dev/null 2>&1 +} + +check_feature_branch() { + local branch="$1" + local has_git_repo="$2" + + # For non-git repos, we can't enforce branch naming but still provide output + if [[ "$has_git_repo" != "true" ]]; then + echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 + return 0 + fi + + if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then + echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 + echo "Feature branches should be named like: 001-feature-name" >&2 + return 1 + fi + + return 0 +} + +get_feature_dir() { echo "$1/specs/$2"; } + +# Find feature directory by numeric prefix instead of exact branch match +# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) +find_feature_dir_by_prefix() { + local repo_root="$1" + local branch_name="$2" + local specs_dir="$repo_root/specs" + + # Extract numeric prefix from branch (e.g., "004" from "004-whatever") + if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then + # If branch doesn't have numeric prefix, fall back to exact match + echo "$specs_dir/$branch_name" + return + fi + + local prefix="${BASH_REMATCH[1]}" + + # Search for directories in specs/ that start with this prefix + local matches=() + if [[ -d "$specs_dir" ]]; then + for dir in "$specs_dir"/"$prefix"-*; do + if [[ -d "$dir" ]]; then + matches+=("$(basename "$dir")") + fi + done + fi + + # Handle results + if [[ ${#matches[@]} -eq 0 ]]; then + # No match found - return the branch name path (will fail later with clear error) + echo "$specs_dir/$branch_name" + elif [[ ${#matches[@]} -eq 1 ]]; then + # Exactly one match - perfect! + echo "$specs_dir/${matches[0]}" + else + # Multiple matches - this shouldn't happen with proper naming convention + echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2 + echo "Please ensure only one spec directory exists per numeric prefix." >&2 + echo "$specs_dir/$branch_name" # Return something to avoid breaking the script + fi +} + +get_feature_paths() { + local repo_root=$(get_repo_root) + local current_branch=$(get_current_branch) + local has_git_repo="false" + + if has_git; then + has_git_repo="true" + fi + + # Use prefix-based lookup to support multiple branches per spec + local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch") + + cat <<EOF +REPO_ROOT='$repo_root' +CURRENT_BRANCH='$current_branch' +HAS_GIT='$has_git_repo' +FEATURE_DIR='$feature_dir' +FEATURE_SPEC='$feature_dir/spec.md' +IMPL_PLAN='$feature_dir/plan.md' +TASKS='$feature_dir/tasks.md' +RESEARCH='$feature_dir/research.md' +DATA_MODEL='$feature_dir/data-model.md' +QUICKSTART='$feature_dir/quickstart.md' +CONTRACTS_DIR='$feature_dir/contracts' +EOF +} + +check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } +check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } + diff --git a/.specify/scripts/bash/create-new-feature.sh b/.specify/scripts/bash/create-new-feature.sh new file mode 100644 index 0000000000000000000000000000000000000000..cfd8113167632a80b14f87539dd5490d0f9c7120 --- /dev/null +++ b/.specify/scripts/bash/create-new-feature.sh @@ -0,0 +1,297 @@ +#!/usr/bin/env bash + +set -e + +JSON_MODE=false +SHORT_NAME="" +BRANCH_NUMBER="" +ARGS=() +i=1 +while [ $i -le $# ]; do + arg="${!i}" + case "$arg" in + --json) + JSON_MODE=true + ;; + --short-name) + if [ $((i + 1)) -gt $# ]; then + echo 'Error: --short-name requires a value' >&2 + exit 1 + fi + i=$((i + 1)) + next_arg="${!i}" + # Check if the next argument is another option (starts with --) + if [[ "$next_arg" == --* ]]; then + echo 'Error: --short-name requires a value' >&2 + exit 1 + fi + SHORT_NAME="$next_arg" + ;; + --number) + if [ $((i + 1)) -gt $# ]; then + echo 'Error: --number requires a value' >&2 + exit 1 + fi + i=$((i + 1)) + next_arg="${!i}" + if [[ "$next_arg" == --* ]]; then + echo 'Error: --number requires a value' >&2 + exit 1 + fi + BRANCH_NUMBER="$next_arg" + ;; + --help|-h) + echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" + echo "" + echo "Options:" + echo " --json Output in JSON format" + echo " --short-name <name> Provide a custom short name (2-4 words) for the branch" + echo " --number N Specify branch number manually (overrides auto-detection)" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0 'Add user authentication system' --short-name 'user-auth'" + echo " $0 'Implement OAuth2 integration for API' --number 5" + exit 0 + ;; + *) + ARGS+=("$arg") + ;; + esac + i=$((i + 1)) +done + +FEATURE_DESCRIPTION="${ARGS[*]}" +if [ -z "$FEATURE_DESCRIPTION" ]; then + echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2 + exit 1 +fi + +# Function to find the repository root by searching for existing project markers +find_repo_root() { + local dir="$1" + while [ "$dir" != "/" ]; do + if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then + echo "$dir" + return 0 + fi + dir="$(dirname "$dir")" + done + return 1 +} + +# Function to get highest number from specs directory +get_highest_from_specs() { + local specs_dir="$1" + local highest=0 + + if [ -d "$specs_dir" ]; then + for dir in "$specs_dir"/*; do + [ -d "$dir" ] || continue + dirname=$(basename "$dir") + number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") + number=$((10#$number)) + if [ "$number" -gt "$highest" ]; then + highest=$number + fi + done + fi + + echo "$highest" +} + +# Function to get highest number from git branches +get_highest_from_branches() { + local highest=0 + + # Get all branches (local and remote) + branches=$(git branch -a 2>/dev/null || echo "") + + if [ -n "$branches" ]; then + while IFS= read -r branch; do + # Clean branch name: remove leading markers and remote prefixes + clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||') + + # Extract feature number if branch matches pattern ###-* + if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then + number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0") + number=$((10#$number)) + if [ "$number" -gt "$highest" ]; then + highest=$number + fi + fi + done <<< "$branches" + fi + + echo "$highest" +} + +# Function to check existing branches (local and remote) and return next available number +check_existing_branches() { + local specs_dir="$1" + + # Fetch all remotes to get latest branch info (suppress errors if no remotes) + git fetch --all --prune 2>/dev/null || true + + # Get highest number from ALL branches (not just matching short name) + local highest_branch=$(get_highest_from_branches) + + # Get highest number from ALL specs (not just matching short name) + local highest_spec=$(get_highest_from_specs "$specs_dir") + + # Take the maximum of both + local max_num=$highest_branch + if [ "$highest_spec" -gt "$max_num" ]; then + max_num=$highest_spec + fi + + # Return next number + echo $((max_num + 1)) +} + +# Function to clean and format a branch name +clean_branch_name() { + local name="$1" + echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' +} + +# Resolve repository root. Prefer git information when available, but fall back +# to searching for repository markers so the workflow still functions in repositories that +# were initialised with --no-git. +SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if git rev-parse --show-toplevel >/dev/null 2>&1; then + REPO_ROOT=$(git rev-parse --show-toplevel) + HAS_GIT=true +else + REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")" + if [ -z "$REPO_ROOT" ]; then + echo "Error: Could not determine repository root. Please run this script from within the repository." >&2 + exit 1 + fi + HAS_GIT=false +fi + +cd "$REPO_ROOT" + +SPECS_DIR="$REPO_ROOT/specs" +mkdir -p "$SPECS_DIR" + +# Function to generate branch name with stop word filtering and length filtering +generate_branch_name() { + local description="$1" + + # Common stop words to filter out + local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$" + + # Convert to lowercase and split into words + local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g') + + # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original) + local meaningful_words=() + for word in $clean_name; do + # Skip empty words + [ -z "$word" ] && continue + + # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms) + if ! echo "$word" | grep -qiE "$stop_words"; then + if [ ${#word} -ge 3 ]; then + meaningful_words+=("$word") + elif echo "$description" | grep -q "\b${word^^}\b"; then + # Keep short words if they appear as uppercase in original (likely acronyms) + meaningful_words+=("$word") + fi + fi + done + + # If we have meaningful words, use first 3-4 of them + if [ ${#meaningful_words[@]} -gt 0 ]; then + local max_words=3 + if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi + + local result="" + local count=0 + for word in "${meaningful_words[@]}"; do + if [ $count -ge $max_words ]; then break; fi + if [ -n "$result" ]; then result="$result-"; fi + result="$result$word" + count=$((count + 1)) + done + echo "$result" + else + # Fallback to original logic if no meaningful words found + local cleaned=$(clean_branch_name "$description") + echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//' + fi +} + +# Generate branch name +if [ -n "$SHORT_NAME" ]; then + # Use provided short name, just clean it up + BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME") +else + # Generate from description with smart filtering + BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") +fi + +# Determine branch number +if [ -z "$BRANCH_NUMBER" ]; then + if [ "$HAS_GIT" = true ]; then + # Check existing branches on remotes + BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR") + else + # Fall back to local directory check + HIGHEST=$(get_highest_from_specs "$SPECS_DIR") + BRANCH_NUMBER=$((HIGHEST + 1)) + fi +fi + +# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal) +FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") +BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" + +# GitHub enforces a 244-byte limit on branch names +# Validate and truncate if necessary +MAX_BRANCH_LENGTH=244 +if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then + # Calculate how much we need to trim from suffix + # Account for: feature number (3) + hyphen (1) = 4 chars + MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4)) + + # Truncate suffix at word boundary if possible + TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) + # Remove trailing hyphen if truncation created one + TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//') + + ORIGINAL_BRANCH_NAME="$BRANCH_NAME" + BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}" + + >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit" + >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)" + >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" +fi + +if [ "$HAS_GIT" = true ]; then + git checkout -b "$BRANCH_NAME" +else + >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME" +fi + +FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" +mkdir -p "$FEATURE_DIR" + +TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md" +SPEC_FILE="$FEATURE_DIR/spec.md" +if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi + +# Set the SPECIFY_FEATURE environment variable for the current session +export SPECIFY_FEATURE="$BRANCH_NAME" + +if $JSON_MODE; then + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" +else + echo "BRANCH_NAME: $BRANCH_NAME" + echo "SPEC_FILE: $SPEC_FILE" + echo "FEATURE_NUM: $FEATURE_NUM" + echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME" +fi diff --git a/.specify/scripts/bash/setup-plan.sh b/.specify/scripts/bash/setup-plan.sh new file mode 100644 index 0000000000000000000000000000000000000000..99566deb98898d781a8acfc85376d36f2aee05d7 --- /dev/null +++ b/.specify/scripts/bash/setup-plan.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -e + +# Parse command line arguments +JSON_MODE=false +ARGS=() + +for arg in "$@"; do + case "$arg" in + --json) + JSON_MODE=true + ;; + --help|-h) + echo "Usage: $0 [--json]" + echo " --json Output results in JSON format" + echo " --help Show this help message" + exit 0 + ;; + *) + ARGS+=("$arg") + ;; + esac +done + +# Get script directory and load common functions +SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get all paths and variables from common functions +eval $(get_feature_paths) + +# Check if we're on a proper feature branch (only for git repos) +check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 + +# Ensure the feature directory exists +mkdir -p "$FEATURE_DIR" + +# Copy plan template if it exists +TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md" +if [[ -f "$TEMPLATE" ]]; then + cp "$TEMPLATE" "$IMPL_PLAN" + echo "Copied plan template to $IMPL_PLAN" +else + echo "Warning: Plan template not found at $TEMPLATE" + # Create a basic plan file if template doesn't exist + touch "$IMPL_PLAN" +fi + +# Output results +if $JSON_MODE; then + printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ + "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT" +else + echo "FEATURE_SPEC: $FEATURE_SPEC" + echo "IMPL_PLAN: $IMPL_PLAN" + echo "SPECS_DIR: $FEATURE_DIR" + echo "BRANCH: $CURRENT_BRANCH" + echo "HAS_GIT: $HAS_GIT" +fi + diff --git a/.specify/scripts/bash/update-agent-context.sh b/.specify/scripts/bash/update-agent-context.sh new file mode 100644 index 0000000000000000000000000000000000000000..6ba03b1f98bdb3acb5aef5686ca691a2f3cd8255 --- /dev/null +++ b/.specify/scripts/bash/update-agent-context.sh @@ -0,0 +1,810 @@ +#!/usr/bin/env bash + +# Update agent context files with information from plan.md +# +# This script maintains AI agent context files by parsing feature specifications +# and updating agent-specific configuration files with project information. +# +# MAIN FUNCTIONS: +# 1. Environment Validation +# - Verifies git repository structure and branch information +# - Checks for required plan.md files and templates +# - Validates file permissions and accessibility +# +# 2. Plan Data Extraction +# - Parses plan.md files to extract project metadata +# - Identifies language/version, frameworks, databases, and project types +# - Handles missing or incomplete specification data gracefully +# +# 3. Agent File Management +# - Creates new agent context files from templates when needed +# - Updates existing agent files with new project information +# - Preserves manual additions and custom configurations +# - Supports multiple AI agent formats and directory structures +# +# 4. Content Generation +# - Generates language-specific build/test commands +# - Creates appropriate project directory structures +# - Updates technology stacks and recent changes sections +# - Maintains consistent formatting and timestamps +# +# 5. Multi-Agent Support +# - Handles agent-specific file paths and naming conventions +# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Amazon Q Developer CLI, or Antigravity +# - Can update single agents or all existing agent files +# - Creates default Claude file if no agent files exist +# +# Usage: ./update-agent-context.sh [agent_type] +# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli +# Leave empty to update all existing agent files + +set -e + +# Enable strict error handling +set -u +set -o pipefail + +#============================================================================== +# Configuration and Global Variables +#============================================================================== + +# Get script directory and load common functions +SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get all paths and variables from common functions +eval $(get_feature_paths) + +NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code +AGENT_TYPE="${1:-}" + +# Agent-specific file paths +CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" +GEMINI_FILE="$REPO_ROOT/GEMINI.md" +COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md" +CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc" +QWEN_FILE="$REPO_ROOT/QWEN.md" +AGENTS_FILE="$REPO_ROOT/AGENTS.md" +WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md" +KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md" +AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" +ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" +CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md" +QODER_FILE="$REPO_ROOT/QODER.md" +AMP_FILE="$REPO_ROOT/AGENTS.md" +SHAI_FILE="$REPO_ROOT/SHAI.md" +Q_FILE="$REPO_ROOT/AGENTS.md" +AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md" +BOB_FILE="$REPO_ROOT/AGENTS.md" + +# Template file +TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md" + +# Global variables for parsed plan data +NEW_LANG="" +NEW_FRAMEWORK="" +NEW_DB="" +NEW_PROJECT_TYPE="" + +#============================================================================== +# Utility Functions +#============================================================================== + +log_info() { + echo "INFO: $1" +} + +log_success() { + echo "✓ $1" +} + +log_error() { + echo "ERROR: $1" >&2 +} + +log_warning() { + echo "WARNING: $1" >&2 +} + +# Cleanup function for temporary files +cleanup() { + local exit_code=$? + rm -f /tmp/agent_update_*_$$ + rm -f /tmp/manual_additions_$$ + exit $exit_code +} + +# Set up cleanup trap +trap cleanup EXIT INT TERM + +#============================================================================== +# Validation Functions +#============================================================================== + +validate_environment() { + # Check if we have a current branch/feature (git or non-git) + if [[ -z "$CURRENT_BRANCH" ]]; then + log_error "Unable to determine current feature" + if [[ "$HAS_GIT" == "true" ]]; then + log_info "Make sure you're on a feature branch" + else + log_info "Set SPECIFY_FEATURE environment variable or create a feature first" + fi + exit 1 + fi + + # Check if plan.md exists + if [[ ! -f "$NEW_PLAN" ]]; then + log_error "No plan.md found at $NEW_PLAN" + log_info "Make sure you're working on a feature with a corresponding spec directory" + if [[ "$HAS_GIT" != "true" ]]; then + log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first" + fi + exit 1 + fi + + # Check if template exists (needed for new files) + if [[ ! -f "$TEMPLATE_FILE" ]]; then + log_warning "Template file not found at $TEMPLATE_FILE" + log_warning "Creating new agent files will fail" + fi +} + +#============================================================================== +# Plan Parsing Functions +#============================================================================== + +extract_plan_field() { + local field_pattern="$1" + local plan_file="$2" + + grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \ + head -1 | \ + sed "s|^\*\*${field_pattern}\*\*: ||" | \ + sed 's/^[ \t]*//;s/[ \t]*$//' | \ + grep -v "NEEDS CLARIFICATION" | \ + grep -v "^N/A$" || echo "" +} + +parse_plan_data() { + local plan_file="$1" + + if [[ ! -f "$plan_file" ]]; then + log_error "Plan file not found: $plan_file" + return 1 + fi + + if [[ ! -r "$plan_file" ]]; then + log_error "Plan file is not readable: $plan_file" + return 1 + fi + + log_info "Parsing plan data from $plan_file" + + NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file") + NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file") + NEW_DB=$(extract_plan_field "Storage" "$plan_file") + NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file") + + # Log what we found + if [[ -n "$NEW_LANG" ]]; then + log_info "Found language: $NEW_LANG" + else + log_warning "No language information found in plan" + fi + + if [[ -n "$NEW_FRAMEWORK" ]]; then + log_info "Found framework: $NEW_FRAMEWORK" + fi + + if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then + log_info "Found database: $NEW_DB" + fi + + if [[ -n "$NEW_PROJECT_TYPE" ]]; then + log_info "Found project type: $NEW_PROJECT_TYPE" + fi +} + +format_technology_stack() { + local lang="$1" + local framework="$2" + local parts=() + + # Add non-empty parts + [[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang") + [[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework") + + # Join with proper formatting + if [[ ${#parts[@]} -eq 0 ]]; then + echo "" + elif [[ ${#parts[@]} -eq 1 ]]; then + echo "${parts[0]}" + else + # Join multiple parts with " + " + local result="${parts[0]}" + for ((i=1; i<${#parts[@]}; i++)); do + result="$result + ${parts[i]}" + done + echo "$result" + fi +} + +#============================================================================== +# Template and Content Generation Functions +#============================================================================== + +get_project_structure() { + local project_type="$1" + + if [[ "$project_type" == *"web"* ]]; then + echo "backend/\\nfrontend/\\ntests/" + else + echo "src/\\ntests/" + fi +} + +get_commands_for_language() { + local lang="$1" + + case "$lang" in + *"Python"*) + echo "cd src && pytest && ruff check ." + ;; + *"Rust"*) + echo "cargo test && cargo clippy" + ;; + *"JavaScript"*|*"TypeScript"*) + echo "npm test \\&\\& npm run lint" + ;; + *) + echo "# Add commands for $lang" + ;; + esac +} + +get_language_conventions() { + local lang="$1" + echo "$lang: Follow standard conventions" +} + +create_new_agent_file() { + local target_file="$1" + local temp_file="$2" + local project_name="$3" + local current_date="$4" + + if [[ ! -f "$TEMPLATE_FILE" ]]; then + log_error "Template not found at $TEMPLATE_FILE" + return 1 + fi + + if [[ ! -r "$TEMPLATE_FILE" ]]; then + log_error "Template file is not readable: $TEMPLATE_FILE" + return 1 + fi + + log_info "Creating new agent context file from template..." + + if ! cp "$TEMPLATE_FILE" "$temp_file"; then + log_error "Failed to copy template file" + return 1 + fi + + # Replace template placeholders + local project_structure + project_structure=$(get_project_structure "$NEW_PROJECT_TYPE") + + local commands + commands=$(get_commands_for_language "$NEW_LANG") + + local language_conventions + language_conventions=$(get_language_conventions "$NEW_LANG") + + # Perform substitutions with error checking using safer approach + # Escape special characters for sed by using a different delimiter or escaping + local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g') + local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g') + local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g') + + # Build technology stack and recent change strings conditionally + local tech_stack + if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then + tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)" + elif [[ -n "$escaped_lang" ]]; then + tech_stack="- $escaped_lang ($escaped_branch)" + elif [[ -n "$escaped_framework" ]]; then + tech_stack="- $escaped_framework ($escaped_branch)" + else + tech_stack="- ($escaped_branch)" + fi + + local recent_change + if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then + recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework" + elif [[ -n "$escaped_lang" ]]; then + recent_change="- $escaped_branch: Added $escaped_lang" + elif [[ -n "$escaped_framework" ]]; then + recent_change="- $escaped_branch: Added $escaped_framework" + else + recent_change="- $escaped_branch: Added" + fi + + local substitutions=( + "s|\[PROJECT NAME\]|$project_name|" + "s|\[DATE\]|$current_date|" + "s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|" + "s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g" + "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|" + "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|" + "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|" + ) + + for substitution in "${substitutions[@]}"; do + if ! sed -i.bak -e "$substitution" "$temp_file"; then + log_error "Failed to perform substitution: $substitution" + rm -f "$temp_file" "$temp_file.bak" + return 1 + fi + done + + # Convert \n sequences to actual newlines + newline=$(printf '\n') + sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file" + + # Clean up backup files + rm -f "$temp_file.bak" "$temp_file.bak2" + + return 0 +} + + + + +update_existing_agent_file() { + local target_file="$1" + local current_date="$2" + + log_info "Updating existing agent context file..." + + # Use a single temporary file for atomic update + local temp_file + temp_file=$(mktemp) || { + log_error "Failed to create temporary file" + return 1 + } + + # Process the file in one pass + local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK") + local new_tech_entries=() + local new_change_entry="" + + # Prepare new technology entries + if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then + new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)") + fi + + if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then + new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)") + fi + + # Prepare new change entry + if [[ -n "$tech_stack" ]]; then + new_change_entry="- $CURRENT_BRANCH: Added $tech_stack" + elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then + new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB" + fi + + # Check if sections exist in the file + local has_active_technologies=0 + local has_recent_changes=0 + + if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then + has_active_technologies=1 + fi + + if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then + has_recent_changes=1 + fi + + # Process file line by line + local in_tech_section=false + local in_changes_section=false + local tech_entries_added=false + local changes_entries_added=false + local existing_changes_count=0 + local file_ended=false + + while IFS= read -r line || [[ -n "$line" ]]; do + # Handle Active Technologies section + if [[ "$line" == "## Active Technologies" ]]; then + echo "$line" >> "$temp_file" + in_tech_section=true + continue + elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then + # Add new tech entries before closing the section + if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then + printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" + tech_entries_added=true + fi + echo "$line" >> "$temp_file" + in_tech_section=false + continue + elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then + # Add new tech entries before empty line in tech section + if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then + printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" + tech_entries_added=true + fi + echo "$line" >> "$temp_file" + continue + fi + + # Handle Recent Changes section + if [[ "$line" == "## Recent Changes" ]]; then + echo "$line" >> "$temp_file" + # Add new change entry right after the heading + if [[ -n "$new_change_entry" ]]; then + echo "$new_change_entry" >> "$temp_file" + fi + in_changes_section=true + changes_entries_added=true + continue + elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then + echo "$line" >> "$temp_file" + in_changes_section=false + continue + elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then + # Keep only first 2 existing changes + if [[ $existing_changes_count -lt 2 ]]; then + echo "$line" >> "$temp_file" + ((existing_changes_count++)) + fi + continue + fi + + # Update timestamp + if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then + echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file" + else + echo "$line" >> "$temp_file" + fi + done < "$target_file" + + # Post-loop check: if we're still in the Active Technologies section and haven't added new entries + if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then + printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" + tech_entries_added=true + fi + + # If sections don't exist, add them at the end of the file + if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then + echo "" >> "$temp_file" + echo "## Active Technologies" >> "$temp_file" + printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" + tech_entries_added=true + fi + + if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then + echo "" >> "$temp_file" + echo "## Recent Changes" >> "$temp_file" + echo "$new_change_entry" >> "$temp_file" + changes_entries_added=true + fi + + # Move temp file to target atomically + if ! mv "$temp_file" "$target_file"; then + log_error "Failed to update target file" + rm -f "$temp_file" + return 1 + fi + + return 0 +} +#============================================================================== +# Main Agent File Update Function +#============================================================================== + +update_agent_file() { + local target_file="$1" + local agent_name="$2" + + if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then + log_error "update_agent_file requires target_file and agent_name parameters" + return 1 + fi + + log_info "Updating $agent_name context file: $target_file" + + local project_name + project_name=$(basename "$REPO_ROOT") + local current_date + current_date=$(date +%Y-%m-%d) + + # Create directory if it doesn't exist + local target_dir + target_dir=$(dirname "$target_file") + if [[ ! -d "$target_dir" ]]; then + if ! mkdir -p "$target_dir"; then + log_error "Failed to create directory: $target_dir" + return 1 + fi + fi + + if [[ ! -f "$target_file" ]]; then + # Create new file from template + local temp_file + temp_file=$(mktemp) || { + log_error "Failed to create temporary file" + return 1 + } + + if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then + if mv "$temp_file" "$target_file"; then + log_success "Created new $agent_name context file" + else + log_error "Failed to move temporary file to $target_file" + rm -f "$temp_file" + return 1 + fi + else + log_error "Failed to create new agent file" + rm -f "$temp_file" + return 1 + fi + else + # Update existing file + if [[ ! -r "$target_file" ]]; then + log_error "Cannot read existing file: $target_file" + return 1 + fi + + if [[ ! -w "$target_file" ]]; then + log_error "Cannot write to existing file: $target_file" + return 1 + fi + + if update_existing_agent_file "$target_file" "$current_date"; then + log_success "Updated existing $agent_name context file" + else + log_error "Failed to update existing agent file" + return 1 + fi + fi + + return 0 +} + +#============================================================================== +# Agent Selection and Processing +#============================================================================== + +update_specific_agent() { + local agent_type="$1" + + case "$agent_type" in + claude) + update_agent_file "$CLAUDE_FILE" "Claude Code" + ;; + gemini) + update_agent_file "$GEMINI_FILE" "Gemini CLI" + ;; + copilot) + update_agent_file "$COPILOT_FILE" "GitHub Copilot" + ;; + cursor-agent) + update_agent_file "$CURSOR_FILE" "Cursor IDE" + ;; + qwen) + update_agent_file "$QWEN_FILE" "Qwen Code" + ;; + opencode) + update_agent_file "$AGENTS_FILE" "opencode" + ;; + codex) + update_agent_file "$AGENTS_FILE" "Codex CLI" + ;; + windsurf) + update_agent_file "$WINDSURF_FILE" "Windsurf" + ;; + kilocode) + update_agent_file "$KILOCODE_FILE" "Kilo Code" + ;; + auggie) + update_agent_file "$AUGGIE_FILE" "Auggie CLI" + ;; + roo) + update_agent_file "$ROO_FILE" "Roo Code" + ;; + codebuddy) + update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" + ;; + qodercli) + update_agent_file "$QODER_FILE" "Qoder CLI" + ;; + amp) + update_agent_file "$AMP_FILE" "Amp" + ;; + shai) + update_agent_file "$SHAI_FILE" "SHAI" + ;; + q) + update_agent_file "$Q_FILE" "Amazon Q Developer CLI" + ;; + agy) + update_agent_file "$AGY_FILE" "Antigravity" + ;; + bob) + update_agent_file "$BOB_FILE" "IBM Bob" + ;; + generic) + log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent." + ;; + *) + log_error "Unknown agent type '$agent_type'" + log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli|generic" + exit 1 + ;; + esac +} + +update_all_existing_agents() { + local found_agent=false + + # Check each possible agent file and update if it exists + if [[ -f "$CLAUDE_FILE" ]]; then + update_agent_file "$CLAUDE_FILE" "Claude Code" + found_agent=true + fi + + if [[ -f "$GEMINI_FILE" ]]; then + update_agent_file "$GEMINI_FILE" "Gemini CLI" + found_agent=true + fi + + if [[ -f "$COPILOT_FILE" ]]; then + update_agent_file "$COPILOT_FILE" "GitHub Copilot" + found_agent=true + fi + + if [[ -f "$CURSOR_FILE" ]]; then + update_agent_file "$CURSOR_FILE" "Cursor IDE" + found_agent=true + fi + + if [[ -f "$QWEN_FILE" ]]; then + update_agent_file "$QWEN_FILE" "Qwen Code" + found_agent=true + fi + + if [[ -f "$AGENTS_FILE" ]]; then + update_agent_file "$AGENTS_FILE" "Codex/opencode" + found_agent=true + fi + + if [[ -f "$WINDSURF_FILE" ]]; then + update_agent_file "$WINDSURF_FILE" "Windsurf" + found_agent=true + fi + + if [[ -f "$KILOCODE_FILE" ]]; then + update_agent_file "$KILOCODE_FILE" "Kilo Code" + found_agent=true + fi + + if [[ -f "$AUGGIE_FILE" ]]; then + update_agent_file "$AUGGIE_FILE" "Auggie CLI" + found_agent=true + fi + + if [[ -f "$ROO_FILE" ]]; then + update_agent_file "$ROO_FILE" "Roo Code" + found_agent=true + fi + + if [[ -f "$CODEBUDDY_FILE" ]]; then + update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" + found_agent=true + fi + + if [[ -f "$SHAI_FILE" ]]; then + update_agent_file "$SHAI_FILE" "SHAI" + found_agent=true + fi + + if [[ -f "$QODER_FILE" ]]; then + update_agent_file "$QODER_FILE" "Qoder CLI" + found_agent=true + fi + + if [[ -f "$Q_FILE" ]]; then + update_agent_file "$Q_FILE" "Amazon Q Developer CLI" + found_agent=true + fi + + if [[ -f "$AGY_FILE" ]]; then + update_agent_file "$AGY_FILE" "Antigravity" + found_agent=true + fi + if [[ -f "$BOB_FILE" ]]; then + update_agent_file "$BOB_FILE" "IBM Bob" + found_agent=true + fi + + # If no agent files exist, create a default Claude file + if [[ "$found_agent" == false ]]; then + log_info "No existing agent files found, creating default Claude file..." + update_agent_file "$CLAUDE_FILE" "Claude Code" + fi +} +print_summary() { + echo + log_info "Summary of changes:" + + if [[ -n "$NEW_LANG" ]]; then + echo " - Added language: $NEW_LANG" + fi + + if [[ -n "$NEW_FRAMEWORK" ]]; then + echo " - Added framework: $NEW_FRAMEWORK" + fi + + if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then + echo " - Added database: $NEW_DB" + fi + + echo + + log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli]" +} + +#============================================================================== +# Main Execution +#============================================================================== + +main() { + # Validate environment before proceeding + validate_environment + + log_info "=== Updating agent context files for feature $CURRENT_BRANCH ===" + + # Parse the plan file to extract project information + if ! parse_plan_data "$NEW_PLAN"; then + log_error "Failed to parse plan data" + exit 1 + fi + + # Process based on agent type argument + local success=true + + if [[ -z "$AGENT_TYPE" ]]; then + # No specific agent provided - update all existing agent files + log_info "No agent specified, updating all existing agent files..." + if ! update_all_existing_agents; then + success=false + fi + else + # Specific agent provided - update only that agent + log_info "Updating specific agent: $AGENT_TYPE" + if ! update_specific_agent "$AGENT_TYPE"; then + success=false + fi + fi + + # Print summary + print_summary + + if [[ "$success" == true ]]; then + log_success "Agent context update completed successfully" + exit 0 + else + log_error "Agent context update completed with errors" + exit 1 + fi +} + +# Execute main function if script is run directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi + diff --git a/.specify/templates/agent-file-template.md b/.specify/templates/agent-file-template.md new file mode 100644 index 0000000000000000000000000000000000000000..754d86ef8b82869c9ee5a0cd0b3bb6ad170187e7 --- /dev/null +++ b/.specify/templates/agent-file-template.md @@ -0,0 +1,28 @@ +# [PROJECT NAME] Development Guidelines + +Auto-generated from all feature plans. Last updated: [DATE] + +## Active Technologies + +[EXTRACTED FROM ALL PLAN.MD FILES] + +## Project Structure + +```text +[ACTUAL STRUCTURE FROM PLANS] +``` + +## Commands + +[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] + +## Code Style + +[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE] + +## Recent Changes + +[LAST 3 FEATURES AND WHAT THEY ADDED] + +<!-- MANUAL ADDITIONS START --> +<!-- MANUAL ADDITIONS END --> diff --git a/.specify/templates/checklist-template.md b/.specify/templates/checklist-template.md new file mode 100644 index 0000000000000000000000000000000000000000..1c18cbbe862b3d08dffbb62cd73542a89a97ced8 --- /dev/null +++ b/.specify/templates/checklist-template.md @@ -0,0 +1,40 @@ +# [CHECKLIST TYPE] Checklist: [FEATURE NAME] + +**Purpose**: [Brief description of what this checklist covers] +**Created**: [DATE] +**Feature**: [Link to spec.md or relevant documentation] + +**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements. + +<!-- + ============================================================================ + IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only. + + The /speckit.checklist command MUST replace these with actual items based on: + - User's specific checklist request + - Feature requirements from spec.md + - Technical context from plan.md + - Implementation details from tasks.md + + DO NOT keep these sample items in the generated checklist file. + ============================================================================ +--> + +## [Category 1] + +- [ ] CHK001 First checklist item with clear action +- [ ] CHK002 Second checklist item +- [ ] CHK003 Third checklist item + +## [Category 2] + +- [ ] CHK004 Another category item +- [ ] CHK005 Item with specific criteria +- [ ] CHK006 Final item in this category + +## Notes + +- Check items off as completed: `[x]` +- Add comments or findings inline +- Link to relevant resources or documentation +- Items are numbered sequentially for easy reference diff --git a/.specify/templates/constitution-template.md b/.specify/templates/constitution-template.md new file mode 100644 index 0000000000000000000000000000000000000000..a84479e5127e2defbeb426e055cf81dcaaad7915 --- /dev/null +++ b/.specify/templates/constitution-template.md @@ -0,0 +1,50 @@ +# [PROJECT_NAME] Constitution +<!-- Example: Spec Constitution, TaskFlow Constitution, etc. --> + +## Core Principles + +### [PRINCIPLE_1_NAME] +<!-- Example: I. Library-First --> +[PRINCIPLE_1_DESCRIPTION] +<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries --> + +### [PRINCIPLE_2_NAME] +<!-- Example: II. CLI Interface --> +[PRINCIPLE_2_DESCRIPTION] +<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats --> + +### [PRINCIPLE_3_NAME] +<!-- Example: III. Test-First (NON-NEGOTIABLE) --> +[PRINCIPLE_3_DESCRIPTION] +<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced --> + +### [PRINCIPLE_4_NAME] +<!-- Example: IV. Integration Testing --> +[PRINCIPLE_4_DESCRIPTION] +<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas --> + +### [PRINCIPLE_5_NAME] +<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity --> +[PRINCIPLE_5_DESCRIPTION] +<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles --> + +## [SECTION_2_NAME] +<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. --> + +[SECTION_2_CONTENT] +<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. --> + +## [SECTION_3_NAME] +<!-- Example: Development Workflow, Review Process, Quality Gates, etc. --> + +[SECTION_3_CONTENT] +<!-- Example: Code review requirements, testing gates, deployment approval process, etc. --> + +## Governance +<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan --> + +[GOVERNANCE_RULES] +<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance --> + +**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE] +<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 --> diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md new file mode 100644 index 0000000000000000000000000000000000000000..0e202399f027654949c7d7f8ef292638dec9015b --- /dev/null +++ b/.specify/templates/plan-template.md @@ -0,0 +1,104 @@ +# Implementation Plan: [FEATURE] + +**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] +**Input**: Feature specification from `/specs/[###-feature-name]/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow. + +## Summary + +[Extract from feature spec: primary requirement + technical approach from research] + +## Technical Context + +<!-- + ACTION REQUIRED: Replace the content in this section with the technical details + for the project. The structure here is presented in advisory capacity to guide + the iteration process. +--> + +**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION] +**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION] +**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A] +**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION] +**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION] +**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION] +**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION] +**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION] +**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION] + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +[Gates determined based on constitution file] + +## Project Structure + +### Documentation (this feature) + +```text +specs/[###-feature]/ +├── plan.md # This file (/speckit.plan command output) +├── research.md # Phase 0 output (/speckit.plan command) +├── data-model.md # Phase 1 output (/speckit.plan command) +├── quickstart.md # Phase 1 output (/speckit.plan command) +├── contracts/ # Phase 1 output (/speckit.plan command) +└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) +``` + +### Source Code (repository root) +<!-- + ACTION REQUIRED: Replace the placeholder tree below with the concrete layout + for this feature. Delete unused options and expand the chosen structure with + real paths (e.g., apps/admin, packages/something). The delivered plan must + not include Option labels. +--> + +```text +# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT) +src/ +├── models/ +├── services/ +├── cli/ +└── lib/ + +tests/ +├── contract/ +├── integration/ +└── unit/ + +# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected) +backend/ +├── src/ +│ ├── models/ +│ ├── services/ +│ └── api/ +└── tests/ + +frontend/ +├── src/ +│ ├── components/ +│ ├── pages/ +│ └── services/ +└── tests/ + +# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected) +api/ +└── [same as backend above] + +ios/ or android/ +└── [platform-specific structure: feature modules, UI flows, platform tests] +``` + +**Structure Decision**: [Document the selected structure and reference the real +directories captured above] + +## Complexity Tracking + +> **Fill ONLY if Constitution Check has violations that must be justified** + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|-----------|------------|-------------------------------------| +| [e.g., 4th project] | [current need] | [why 3 projects insufficient] | +| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | diff --git a/.specify/templates/spec-template.md b/.specify/templates/spec-template.md new file mode 100644 index 0000000000000000000000000000000000000000..a22085cdd7f810aaea93c744ba7e852db7b5eb33 --- /dev/null +++ b/.specify/templates/spec-template.md @@ -0,0 +1,115 @@ +# Feature Specification: [FEATURE NAME] + +**Feature Branch**: `[###-feature-name]` +**Created**: [DATE] +**Status**: Draft +**Input**: User description: "$ARGUMENTS" + +## User Scenarios & Testing *(mandatory)* + +<!-- + IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance. + Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them, + you should still have a viable MVP (Minimum Viable Product) that delivers value. + + Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical. + Think of each story as a standalone slice of functionality that can be: + - Developed independently + - Tested independently + - Deployed independently + - Demonstrated to users independently +--> + +### User Story 1 - [Brief Title] (Priority: P1) + +[Describe this user journey in plain language] + +**Why this priority**: [Explain the value and why it has this priority level] + +**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [expected outcome] +2. **Given** [initial state], **When** [action], **Then** [expected outcome] + +--- + +### User Story 2 - [Brief Title] (Priority: P2) + +[Describe this user journey in plain language] + +**Why this priority**: [Explain the value and why it has this priority level] + +**Independent Test**: [Describe how this can be tested independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [expected outcome] + +--- + +### User Story 3 - [Brief Title] (Priority: P3) + +[Describe this user journey in plain language] + +**Why this priority**: [Explain the value and why it has this priority level] + +**Independent Test**: [Describe how this can be tested independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [expected outcome] + +--- + +[Add more user stories as needed, each with an assigned priority] + +### Edge Cases + +<!-- + ACTION REQUIRED: The content in this section represents placeholders. + Fill them out with the right edge cases. +--> + +- What happens when [boundary condition]? +- How does system handle [error scenario]? + +## Requirements *(mandatory)* + +<!-- + ACTION REQUIRED: The content in this section represents placeholders. + Fill them out with the right functional requirements. +--> + +### Functional Requirements + +- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"] +- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"] +- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"] +- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"] +- **FR-005**: System MUST [behavior, e.g., "log all security events"] + +*Example of marking unclear requirements:* + +- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?] +- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified] + +### Key Entities *(include if feature involves data)* + +- **[Entity 1]**: [What it represents, key attributes without implementation] +- **[Entity 2]**: [What it represents, relationships to other entities] + +## Success Criteria *(mandatory)* + +<!-- + ACTION REQUIRED: Define measurable success criteria. + These must be technology-agnostic and measurable. +--> + +### Measurable Outcomes + +- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"] +- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"] +- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"] +- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"] diff --git a/.specify/templates/tasks-template.md b/.specify/templates/tasks-template.md new file mode 100644 index 0000000000000000000000000000000000000000..5f3e514ae353409f84c5b0ab73231f5a994762b8 --- /dev/null +++ b/.specify/templates/tasks-template.md @@ -0,0 +1,251 @@ +--- + +description: "Task list template for feature implementation" +--- + +# Tasks: [FEATURE NAME] + +**Input**: Design documents from `/specs/[###-feature-name]/` +**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ + +**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification. + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) +- Include exact file paths in descriptions + +## Path Conventions + +- **Single project**: `src/`, `tests/` at repository root +- **Web app**: `backend/src/`, `frontend/src/` +- **Mobile**: `api/src/`, `ios/src/` or `android/src/` +- Paths shown below assume single project - adjust based on plan.md structure + +<!-- + ============================================================================ + IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only. + + The /speckit.tasks command MUST replace these with actual tasks based on: + - User stories from spec.md (with their priorities P1, P2, P3...) + - Feature requirements from plan.md + - Entities from data-model.md + - Endpoints from contracts/ + + Tasks MUST be organized by user story so each story can be: + - Implemented independently + - Tested independently + - Delivered as an MVP increment + + DO NOT keep these sample tasks in the generated tasks.md file. + ============================================================================ +--> + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Project initialization and basic structure + +- [ ] T001 Create project structure per implementation plan +- [ ] T002 Initialize [language] project with [framework] dependencies +- [ ] T003 [P] Configure linting and formatting tools + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented + +**⚠️ CRITICAL**: No user story work can begin until this phase is complete + +Examples of foundational tasks (adjust based on your project): + +- [ ] T004 Setup database schema and migrations framework +- [ ] T005 [P] Implement authentication/authorization framework +- [ ] T006 [P] Setup API routing and middleware structure +- [ ] T007 Create base models/entities that all stories depend on +- [ ] T008 Configure error handling and logging infrastructure +- [ ] T009 Setup environment configuration management + +**Checkpoint**: Foundation ready - user story implementation can now begin in parallel + +--- + +## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP + +**Goal**: [Brief description of what this story delivers] + +**Independent Test**: [How to verify this story works on its own] + +### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️ + +> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** + +- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py +- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py + +### Implementation for User Story 1 + +- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py +- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py +- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013) +- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py +- [ ] T016 [US1] Add validation and error handling +- [ ] T017 [US1] Add logging for user story 1 operations + +**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently + +--- + +## Phase 4: User Story 2 - [Title] (Priority: P2) + +**Goal**: [Brief description of what this story delivers] + +**Independent Test**: [How to verify this story works on its own] + +### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️ + +- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py +- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py + +### Implementation for User Story 2 + +- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py +- [ ] T021 [US2] Implement [Service] in src/services/[service].py +- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py +- [ ] T023 [US2] Integrate with User Story 1 components (if needed) + +**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently + +--- + +## Phase 5: User Story 3 - [Title] (Priority: P3) + +**Goal**: [Brief description of what this story delivers] + +**Independent Test**: [How to verify this story works on its own] + +### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️ + +- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py +- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py + +### Implementation for User Story 3 + +- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py +- [ ] T027 [US3] Implement [Service] in src/services/[service].py +- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py + +**Checkpoint**: All user stories should now be independently functional + +--- + +[Add more user story phases as needed, following the same pattern] + +--- + +## Phase N: Polish & Cross-Cutting Concerns + +**Purpose**: Improvements that affect multiple user stories + +- [ ] TXXX [P] Documentation updates in docs/ +- [ ] TXXX Code cleanup and refactoring +- [ ] TXXX Performance optimization across all stories +- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/ +- [ ] TXXX Security hardening +- [ ] TXXX Run quickstart.md validation + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies - can start immediately +- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories +- **User Stories (Phase 3+)**: All depend on Foundational phase completion + - User stories can then proceed in parallel (if staffed) + - Or sequentially in priority order (P1 → P2 → P3) +- **Polish (Final Phase)**: Depends on all desired user stories being complete + +### User Story Dependencies + +- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories +- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable +- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable + +### Within Each User Story + +- Tests (if included) MUST be written and FAIL before implementation +- Models before services +- Services before endpoints +- Core implementation before integration +- Story complete before moving to next priority + +### Parallel Opportunities + +- All Setup tasks marked [P] can run in parallel +- All Foundational tasks marked [P] can run in parallel (within Phase 2) +- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows) +- All tests for a user story marked [P] can run in parallel +- Models within a story marked [P] can run in parallel +- Different user stories can be worked on in parallel by different team members + +--- + +## Parallel Example: User Story 1 + +```bash +# Launch all tests for User Story 1 together (if tests requested): +Task: "Contract test for [endpoint] in tests/contract/test_[name].py" +Task: "Integration test for [user journey] in tests/integration/test_[name].py" + +# Launch all models for User Story 1 together: +Task: "Create [Entity1] model in src/models/[entity1].py" +Task: "Create [Entity2] model in src/models/[entity2].py" +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Setup +2. Complete Phase 2: Foundational (CRITICAL - blocks all stories) +3. Complete Phase 3: User Story 1 +4. **STOP and VALIDATE**: Test User Story 1 independently +5. Deploy/demo if ready + +### Incremental Delivery + +1. Complete Setup + Foundational → Foundation ready +2. Add User Story 1 → Test independently → Deploy/Demo (MVP!) +3. Add User Story 2 → Test independently → Deploy/Demo +4. Add User Story 3 → Test independently → Deploy/Demo +5. Each story adds value without breaking previous stories + +### Parallel Team Strategy + +With multiple developers: + +1. Team completes Setup + Foundational together +2. Once Foundational is done: + - Developer A: User Story 1 + - Developer B: User Story 2 + - Developer C: User Story 3 +3. Stories complete and integrate independently + +--- + +## Notes + +- [P] tasks = different files, no dependencies +- [Story] label maps task to specific user story for traceability +- Each user story should be independently completable and testable +- Verify tests fail before implementing +- Commit after each task or logical group +- Stop at any checkpoint to validate story independently +- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..4396d0a3527b864aa08bad7e1ddb34d95ec8ad5a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,98 @@ +# AGENTS.md + +This file provides guidance to coding agents working in this repository. + +## Repository Guidelines + +### Pipeline-First Development (MANDATORY) +**All new functionality MUST be implemented as pipeline Steps composed via the Pipeline engine.** Do NOT write standalone scripts, ad-hoc loops, or inline logic that bypasses the pipeline. Before writing any code: + +1. Read `docs/design/PIPELINE_DESIGN.md` to understand the Step → Pipeline → Branch model. +2. Implement logic as a `Step` class with `requires`/`provides` declarations and a `__call__(self, ctx) -> ctx` method. +3. Compose steps using `Pipeline().then(...)` and `.branch(...)` — never manual for-loops or direct function chaining. +4. Use `StepContext.replace()` for immutable context updates — never mutate context directly. +5. Put integration-specific data in `metadata`, not new context fields, unless the field is shared across multiple pipelines. + +**Anti-patterns to reject:** +- Writing a function that calls multiple steps manually instead of composing them in a Pipeline +- Inline reflection/evaluation logic instead of creating a ReflectStep or EvaluateStep +- Ad-hoc `ThreadPoolExecutor` usage instead of `async_boundary` and `max_workers` on steps +- Standalone scripts that duplicate pipeline functionality without using the pipeline engine +- Bypassing `requires`/`provides` contracts by accessing context fields not declared in `requires` + +If a task seems like it cannot fit the pipeline model, explain why to the user before proceeding — do not silently circumvent it. + +### Core Code Protection +**Do NOT modify core modules (`ace/core/`, `pipeline/`) without explicit user approval.** Before proposing any change to these directories: +1. Read the relevant design docs (`docs/design/ACE_ARCHITECTURE.md`, `docs/design/PIPELINE_DESIGN.md`) thoroughly. +2. Evaluate whether the change is truly required or if it can be achieved outside the core (e.g., in an integration, step, or example). +3. Clearly explain the proposed change and its justification to the user **before** making any edits. +4. Wait for the user to explicitly accept before proceeding. + +### Documentation Maintenance +Before working on code in `ace/`, read `docs/design/ACE_ARCHITECTURE.md` to understand the current architecture. +Before working on code in `pipeline/` or `ace/core/`, read `docs/design/PIPELINE_DESIGN.md` to understand the pipeline engine. + +**Docs MUST be kept in sync with code.** Any change that alters a public API, renames a concept, adds/removes a module, or changes execution flow **requires** a corresponding update to the relevant docs. Do not merge code changes that make the documentation inaccurate. + +Key design docs: +- `docs/design/ACE_ARCHITECTURE.md` — ACE architecture: layers, core concepts, roles, steps, runners, integrations +- `docs/design/ACE_REFERENCE.md` — ACE code reference: full implementations, API signatures, usage examples +- `docs/design/ACE_DECISIONS.md` — design decisions and rejected alternatives (ACE, pipeline, migration) +- `docs/design/PIPELINE_DESIGN.md` — pipeline engine: steps, StepProtocol, Pipeline, Branch, concurrency +- If you need to work with collected traces from Logfire, read `agent-guides/logfire.md` + +### Project Structure +- `ace/` — core library: roles (PydanticAI-backed), skillbook, steps, runners, providers, RR, integrations, observability +- `pipeline/` — generic pipeline engine that `ace` is built on (see `docs/design/PIPELINE_DESIGN.md`) +- `ace-eval/` — evaluation framework (submodule, separate repo) +- `tests/` — unit/integration tests (pytest) +- `examples/` — runnable demos grouped by integration +- `agent-guides/` — internal development guides for LLM agents; not part of the public docs site +- `docs/` — guides and reference material + - `docs/design/ACE_ARCHITECTURE.md` — architecture and concepts (keep in sync with code) + - `docs/design/ACE_REFERENCE.md` — code reference and examples (keep in sync with code) + - `docs/design/ACE_DECISIONS.md` — design decisions and rejected alternatives + - `docs/design/PIPELINE_DESIGN.md` — pipeline engine design doc (keep in sync with code) + +### Commands +- `uv sync` — install all dependencies +- `uv run pytest` — run tests (coverage enforced `--cov-fail-under=25`) +- `uv run pytest -m unit` / `-m integration` / `-m slow` — run by marker +- `uv run black ace/ tests/ examples/` — format code +- `uv run mypy ace/` — type check + +### Coding Style +- PEP 8 with Black formatting (line length 88) +- Type hints and docstrings for public APIs +- Python 3.12 target +- Test files: `tests/test_*.py`; functions: `test_*`; classes: `Test*` + +### Testing +- Pytest is the primary runner +- Add tests for new features; include regression tests for bug fixes + +### Commits +- Conventional Commits: `feat(scope): subject`, `fix(scope): subject` +- Do NOT add `Co-Authored-By` trailers to commit messages +- PRs should include description, test results, and relevant docs updates + +### ACE Roles (quick reference) + +| Role | Responsibility | Key Class | +|------|---------------|-----------| +| **Agent** | Executes tasks using skillbook strategies | `Agent` | +| **Reflector** | Analyzes execution results | `Reflector` | +| **SkillManager** | Updates the skillbook with new strategies | `SkillManager` | + +### Integration Runners + +| Runner | Framework | Use Case | +|--------|-----------|----------| +| `ACELiteLLM` | LiteLLM (100+ providers) | Simple self-improving agent | +| `ACELangChain` | LangChain | Wrap chains/agents with learning | +| `ACEBrowserUse` | browser-use | Browser automation with learning | +| `ACEClaudeCode` | Claude Code CLI | Coding tasks with learning | + +NEVER USE FALLBACKS OR IMPLEMENT THINGS I NEVER ASKED FOR. +IF IT'S STRAIGHFORWARD, IMPLEMENT IT STRAIGHFORWARD. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..71c70d0c957f6b77efcaab115e6f82ba2d5d3719 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,445 @@ +# Changelog + +All notable changes to ACE Framework will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.12.0] - 2026-05-06 + +### Added +- **Cross-trace generalization gate** for the SkillManager — four-criterion check + (≥3 instances across ≥2 domains, named slot, no API-specific params in the + action, verifiable runtime trigger) that constrains when SM may write a broad + skill subsuming existing narrow ones. Backed by [skill_generalization.md](ace-eval/research/skill_generalization.md) + (14 cited sources). +- **Action-equivalence rule** for within-run skill writing — splits on action, + not on trigger surface. Prevents over-decomposition of structurally identical + rules. +- **Atomicity rule** in `insight` formatting — one trigger + one action per + skill, with explicit good/bad shape examples in the prompt. +- **Insight format guidance** in the SM prompt sourced from the in-context- + learning research doc ([icl_skill_formatting.md](ace-eval/research/icl_skill_formatting.md)) — 15-50 word cap, imperative + voice, positive framing default, examples only for format/shape rules. +- **Evidence-only tagging** — SM tags only skills the reflection actually + implicates, instead of iterating over every injected_skill_id. +- **Broaden-via-comparison rule** for UPDATE — when two skills target the same + root cause in different niches, broaden `issue` rather than adding a duplicate. +- **Prompt caching for SM** via `CachePoint(ttl="5m")` mirroring RR's caching; + cache_read/write tokens forwarded in run metadata. +- **SM behavior spec + harness** — `ace-eval/scripts/sm_behavior_check.py`, + `sm_iterative_check.py`, `sm_stability_check.py` and matching scenario + fixtures cover replay stability, convergence, scope expansion, and the + below-threshold gate boundary. + +### Changed +- **`update_skills` signature** — `source` is now optional; `SkillbookView` + was dropped from the parameter list (callers pass the real `Skillbook` + directly). +- **Hard removal cap removed** — SM no longer auto-removes skills whose + `harmful_count >= 3`. Heavily-used skills can legitimately accumulate + harmful tags without being net-negative; REMOVE now requires explicit + reflection evidence. +- **TauBench evaluator** — `evaluation_type=ALL_WITH_NL_ASSERTIONS` on both + `run_task` and `run_tasks` call sites in + `ace-eval/src/ace_eval/e2e/benchmarks/tau_bench.py`. Retail (and any future + benchmark with `NL_ASSERTION` in `reward_basis`) now produces real reward + numbers instead of crashing on every task during reward computation. + +### Removed +- **Skillbook v1 legacy aliases** on `Skill` and `UpdateOperation` — v2 schema + is now the only schema. + +## [0.11.0] - 2026-04-29 + +### Added +- **`RecursiveAgent` core abstraction** — extracted from RR into `ace/core/recursive_agent.py`; provides a generic recursive PydanticAI agent with sandbox, microcompaction, default tool set, and depth-aware sub-agent registration. Reusable across roles beyond the Reflector. +- **Skillbook v2 schema** — full rewrite of `ace/core/skillbook.py` with section-grouped storage, richer `InsightSource` provenance, and BM25-backed retrieval (`rank-bm25` runtime dependency). +- **Agentic SkillManager** — `SkillManager` rewritten as a tool-calling loop (`ace/implementations/sm_tools.py`). Provenance is now populated by the SkillManager agent directly rather than a dedicated step. +- **RR skillbook tools for the Reflector** — Reflector can introspect and propose updates to the skillbook from inside the recursive loop. +- **Anthropic prompt caching enabled by default** for RR agents; `cache_read_tokens` and `cache_write_tokens` are forwarded in run metadata for cost accounting. +- **Logfire spans around recursive agent sessions** for end-to-end observability of nested RR runs. +- **Online / offline mode** in the ACE runner. +- **`nest-asyncio`** added to the dev extra to support nested loops in notebooks and live test scripts. + +### Changed +- **RR collapsed into a single `RRStep`** — the orchestrator/worker split, batch machinery, and `AttachInsightSourcesStep` have been removed. RR now runs as a true recursive loop with depth-bounded sub-agent delegation and microcompaction of stale tool results. +- **Reflector prompts** simplified, deduplicated, and made input-agnostic; added early-skillbook-skim and parallel-tool guidance. +- **`record_observation` tool renamed to `think`** to clarify it is a scratch reasoning channel, not persistent storage. +- **Native evidence summaries** are produced inside RR before final synthesis. +- **Skillbook prompt format is now markdown** — `Skillbook.as_prompt()` returns a section-grouped markdown list instead of TOON. The `python-toon` dependency has been dropped. +- **`metered_model` and `sandbox`** moved from `ace/rr/` into `ace/core/` to reflect their cross-role use. +- **Pytest defaults** — `uv run pytest` now excludes `integration` and `requires_api` markers by default; coverage flags removed from `addopts` (run with `--cov` explicitly when needed). +- **Observability** — `tool_arguments` and `tool_response` are no longer scrubbed by the Logfire callback so tool I/O remains inspectable. + +### Removed +- `ace/rr/` legacy package layout (`agent.py`, `runner.py`, `trace_context.py`, `message_trimming.py`, batch helpers). Functionality is now in `ace/core/recursive_agent.py` and `ace/implementations/rr/`. +- `AttachInsightSourcesStep` and its pipeline wiring — provenance is attached by the SkillManager agent. +- `python-toon` runtime dependency. +- TAG handling from the SkillManager. +- Citation scanning from the Reflector. + +## [0.10.0] - 2026-04-13 + +### Added +- **Usage metering hook** — `RecursiveConfig.usage_callback: (RequestUsage, model_id) -> None` fires once per pydantic-ai model request (orchestrator turns, sub-agent runs, tool-call follow-ups). Implemented via `ace.rr.MeteredModel`, a `pydantic_ai.models.wrapper.WrapperModel` subclass, so metering lives at the framework's own model boundary — no per-call-site plumbing. Callback exceptions are caught and logged so metering never crashes the pipeline. +- **Pre-built model instance support** — `RRStep`, `create_rr_agent`, `create_sub_agent`, and `RecursiveConfig.subagent_model` now accept either a model-id string or a pre-built `pydantic_ai.models.Model` instance. Enables callers that need a custom provider (e.g. cross-account Bedrock with STS-assumed credentials) to inject a fully-configured model rather than resolving from a string. +- **Sub-agent `model_settings`** — `create_sub_agent` now threads an explicit `ModelSettings` parameter into its `PydanticAgent` constructor. + +### Notes +- Back-compat: existing `RRStep(model="...")` callers continue to work unchanged. The widened type signature is additive. + +## [0.9.4] - 2026-04-11 + +### Added +- **Kayba tracing SDK** — `ace.tracing` module wraps MLflow tracing with Kayba-native configuration, folder organization, and input sanitization (`pip install ace-framework[tracing]`) + +## [0.9.3] - 2026-04-01 + +### Added +- **Structured design docs** — split ACE_DESIGN.md into architecture, reference, and decisions docs under docs/design/ +- **Simplified Skill model** — removed unused tag counters (helpful/harmful/neutral) and TagStep from the pipeline +- **Cleaner InsightSource provenance** — restored error_identification and learning_text fields + +## [0.9.2] - 2026-03-31 + +### Added +- **Insight source provenance** — `InsightSource` typed model captures the origin of each skillbook update (trace ID, sample question, epoch/step, reflection summary, integration metadata); provenance is now populated by the SkillManager agent directly +- **Claude SDK step** — `ClaudeSDKStep` integration for running Claude Code sub-agents from within ACE pipelines +- **RR sub-agent code execution** — Recursive Reflector can now delegate to code-execution sub-agents at runtime +- **RR raw trace batch helpers** — `build_raw_trace_batches` and related runtime utilities for feeding raw traces directly into the RR pipeline + +### Fixed +- **Logfire scrubbing** — added scrubbing callback to stop Logfire over-redacting trace content (reasoning, answers, messages now visible in Logfire UI) +- **RR combined-batch normalization** — fixed ordering/deduplication of combined task batches in multi-sample runs + +### Docs +- Logfire query API guide clarifications +- MCP client setup guide and compatibility tests +- Design docs updated to reflect insight source provenance model + +## [0.9.1] - 2026-03-26 + +### Fixed +- **CLI packaging** — include .md data files in wheel so `kayba setup` and skill install work on pip/uv-installed packages + +## [0.9.0] - 2026-03-26 + +### Added +- **PydanticAI migration** — ACE roles (Agent, Reflector, SkillManager) rebuilt on PydanticAI agents with structured output, replacing the legacy role system +- **Recursive Reflector** — PydanticAI-powered trace analysis agent with sandboxed code execution, sub-agent delegation, and working memory (`save_notes` tool) +- **Kayba CLI** — full hosted API client with trace upload/management, interactive run, insights, prompts, batch processing, materialization, and integration commands (`kayba` entry point) + +## [0.8.8] - 2026-03-17 + +### Added +- **Pipeline hooks & cancellation** — `PipelineHook` protocol and `CancellationToken` for observing and controlling pipeline execution +- **Kayba pipeline skills for Claude Code** — 7-stage dynamic evaluation pipeline that generates custom benchmarks tailored to your agent's domain. Instead of static test suites, the skills analyze your API, build domain-aware metrics and rubrics, create action plans, and run human-in-the-loop validation — all as composable Claude Code skills +- **`kayba setup` command** — one command to install the full evaluation skill pipeline into your `.claude/skills/` directory, ready to use inside Claude Code out of the box + +### Docs +- Documented `kayba setup` skills installation + +### Try it free +**7-day free trial** — Try the full Kayba evaluation pipeline on our hosted solution with zero setup. Sign up at [kayba.ai](https://kayba.ai) and run `kayba setup` to start building dynamic evals for your agents today. + +## [0.8.7] - 2026-03-17 + +### Added +- **Improved Opik trace naming** — traces now display the question text (first 80 chars) instead of generic names like "ace_pipeline" or "rr_reflect" +- **Thread ID support for Opik** — `OpikStep` and `RROpikStep` accept an optional `thread_id` parameter for grouping related traces + +## [0.8.5] - 2026-03-04 + +### Added +- **Self-contained RR module** (`ace/rr/`) — sandbox, subagent, trace_context, config, code_extraction, message_trimming extracted from `ace/reflector/` into a standalone package +- **v5.6 prompt promoted as default** — new prompt evolution (v4 → v5.1–v5.6) for the `ace` RR pipeline +- **`build_steps()` API** — all runners gain a `build_steps()` classmethod for pipeline customization +- **Shared `CallBudget`** — single budget instance shared across RR pipeline steps +- **ACE MCP server (optional)** — stdio MCP server in `ace.integrations.mcp` with tools: `ace.ask`, `ace.learn.sample`, `ace.learn.feedback`, `ace.skillbook.get`, `ace.skillbook.save`, `ace.skillbook.load` +- **Session-scoped state management** — in-memory `session_id` registry with TTL cleanup and per-session async locking +- **MCP packaging + CLI** — optional `mcp` extra and `ace-mcp` entrypoint +- **MCP docs and demo client** — integration guide and stdio client example +- **Composing pipelines guide** — new `docs/guides/composing-pipelines.md` +- **RR examples** — `rr_demo.py`, `rr_opik_demo.py`, `compose_custom_pipeline.py` + +### Changed +- **RR backward-compat shims** — original `ace/reflector/` files now re-export from `ace.rr` (no duplication) +- **`RRStep` dual protocol** — implements both `StepProtocol` and `ReflectorLike` +- **Sandbox hardening** — hardened `getattr` in sandbox execution environment +- **Opik made opt-in** — moved `opik` from hard dependency to `observability` extra +- **Safety controls** — runtime request limits (`max_prompt_chars`, `max_samples_per_call`) and optional root-bound path enforcement for save/load via `ACE_MCP_SKILLBOOK_ROOT` +- **Schema-driven validation** — MCP request/response models aligned to `specs/002-ace-mcp-server/contracts/tool-schemas.md` +- **`learn_from_feedback` routed through pipeline** — feedback learning now uses the pipeline engine + +### Testing +- Added MCP test suite: models, registry, handlers, and server registration/startup smoke tests +- Added optional-dependency boundary checks for the MCP integration +- RR steps at 94%, sandbox at 92%, runner at 74%, MCP models at 100% + +## [0.8.4] - 2026-02-27 + +### Added +- **OpenClaw integration** — learn from OpenClaw session transcripts (JSONL) via new `OpenClawToTraceStep` and `LoadTracesStep` pipeline steps (#86) +- **ExportSkillbookMarkdownStep** — export skillbook to markdown file +- OpenClaw example script and integration docs + +## [0.8.3] - 2026-02-21 + +### Added +- **Pipeline engine** — generic pipeline framework with branching, async boundaries, and parallel execution (#78) +- **Trace passthrough** — `_build_traces()` helper and raw trace data passed to RecursiveReflector sandbox + +## [0.8.2] - 2026-02-18 + +### Added +- **RecursiveReflector None-response guard** — gracefully handles empty/None LLM responses (e.g. from Gemini) with retry prompt instead of crashing +- **`LiteLLMClient.complete_messages()`** — native multi-turn completion that preserves structured message lists + +## [0.8.1] - 2026-02-18 + +### Added +- **Insight source tracing** — `InsightSource` dataclass tracks skill provenance (epoch, sample, trace refs, error identification, learning text) +- **Sample.id** promoted to first-class field with UUID auto-generation +- **Skillbook query API** — `source_map()`, `source_summary()`, `source_filter()` for skill lineage +- Insight sources wired through `OfflineACE`, `OnlineACE`, and async learning pipelines +- `UpdateOperation.learning_index` for linking operations to reflector learnings +- Bedrock e2e example (`examples/litellm/bedrock_insight_source_test.py`) +- `docs/INSIGHT_SOURCES.md` guide + +## [0.8.0] - 2026-02-17 + +### Added +- **Recursive reflector** with sandboxed code execution for validation +- **TAU-bench integration** with config-driven YAML profiles, prompt sweep, capture/replay, and label support +- **v3 prompt templates** for agent, reflector, and skill manager roles +- **Trace context module** exposing agent system prompt and execution context to reflector + +### Fixed +- Opik cloud mode support when `OPIK_API_KEY` is set +- Bedrock/SageMaker API key lookup skipped for managed providers +- Reflector trace quality improvements (user messages, turn separators) + +### Changed +- v3 prompts set as default prompt version +- Reflector now includes agent system prompt in trace context + +## [0.7.3] - 2026-02-04 + +### Added +- ACE learning for Claude Code via `/ace-learn` (transcript-based learning and skillbook updates). +- CLI patching to minimize Claude Code system prompt overhead for learning runs. + +### Fixed +- Claude Code transcript parsing for feedback and last-prompt extraction edge cases. + +### Changed +- Unified agent guidance into `AGENTS.md` with `CLAUDE.md` symlink. + +## [0.7.0] - 2025-12-04 + +### ⚠️ Breaking Changes +- **Complete terminology rename** - Playbook → Skillbook, Bullet → Skill + - `Playbook` → `Skillbook` + - `Bullet` → `Skill` + - `Generator` → `Agent` + - `Curator` → `SkillManager` + - `OfflineAdapter` → `OfflineACE` + - `OnlineAdapter` → `OnlineACE` + - `DeltaOperation` → `UpdateOperation` + - `DeltaBatch` → `UpdateBatch` + - **Migration**: Update imports and method calls to use new names + - **JSON files**: Change `"bullets"` key to `"skills"` in saved skillbooks + +### Added +- **Deduplication consolidation_operations field** - SkillManagerOutput now properly captures consolidation operations from LLM responses + +### Fixed +- **Deduplication not working** - Added `consolidation_operations` field to SkillManagerOutput Pydantic model. Previously, Instructor was silently dropping these operations. + +## [0.5.0] - 2025-11-20 + +### ⚠️ Breaking Changes +- **Playbook format changed to TOON (Token-Oriented Object Notation)** + - `Playbook.as_prompt()` now returns TOON format instead of markdown + - **Reason**: 16-62% token savings for improved scalability and reduced inference costs + - **Migration**: No action needed if using playbook with Generator/Curator/Reflector + - **Debugging**: Use `playbook._as_markdown_debug()` or `str(playbook)` for human-readable output + - **Details**: Uses tab delimiters and excludes internal metadata (created_at, updated_at) + +### Added +- **ACELiteLLM integration** - Simple conversational agent with automatic learning +- **ACELangChain integration** - Wrap LangChain Runnables with ACE learning +- **Custom integration pattern** - Wrap ANY agentic system with ACE learning + - Base utilities in `ace/integrations/base.py` with `wrap_playbook_context()` helper + - Complete working example in `examples/custom_integration_example.py` + - Integration Pattern: Inject playbook → Execute agent → Learn from results +- **Integration exports** - Import ACEAgent, ACELiteLLM, ACELangChain from `ace` package root +- **TOON compression for playbooks** - 16-62% token reduction vs markdown +- **Citation-based tracking** - Strategies cited inline as `[section-00001]`, auto-extracted from reasoning +- **Enhanced browser traces** - Full execution logs (2200+ chars) passed to Reflector +- **Test coverage** - Improved from 28% to 70% (241 tests total) + +### Changed +- **Renamed SimpleAgent → ACELiteLLM** - Clearer naming for conversational agent integration +- `Playbook.__str__()` returns markdown (TOON reserved for LLM consumption via `as_prompt()`) + +### Fixed +- **Browser-use trace integration** - Reflector now receives complete execution traces + - Fixed initial query duplication (task appeared in both question and reasoning) + - Fixed missing trace data (reasoning field now contains 2200+ chars vs 154 chars) + - Fixed screenshot attribute bug causing AttributeError on step.state.screenshot + - Fixed invalid bullet ID filtering - hallucinated/malformed citations now filtered out + - Added comprehensive regression tests to catch these issues + - Impact: Reflector can now properly analyze browser agent's thought process + - Test coverage improved: 69% → 79% for browser_use.py +- Prompt v2.1 test assertions updated to match current format +- All 206 tests now pass (was 189) + +## [0.4.0] - 2025-10-26 + +### Added +- **Production Observability** with Opik integration + - Enterprise-grade monitoring and tracing + - Automatic token usage and cost tracking for all LLM calls + - Real-time cost monitoring via Opik dashboard + - Graceful degradation when Opik is not installed +- **Browser Automation Demos** showing ACE vs baseline performance + - Domain checker demo with learning capabilities + - Form filler demo with adaptive strategies + - Side-by-side comparison of baseline vs ACE-enhanced automation +- Support for UV package manager (10-100x faster than pip) + - Added uv.lock for reproducible builds + - UV-specific installation and development instructions +- Improved documentation structure with multiple guides + - QUICK_START.md for 5-minute quickstart + - API_REFERENCE.md for complete API documentation + - PROMPT_ENGINEERING.md for advanced techniques + - SETUP_GUIDE.md for development setup + - TESTING_GUIDE.md for testing procedures +- Optional dependency groups for modular installation + - `observability` for Opik integration + - `demos` for browser automation examples + - `langchain` for LangChain support + - `transformers` for local model support + - `dev` for development tools + - `all` for all features combined + +### Changed +- **Replaced explainability module with observability** + - Removed empty ace/explainability directory + - Migrated to production-grade Opik monitoring + - Updated all documentation to reflect this change +- Improved Python version requirements consistency (3.12 everywhere) +- Enhanced README with clearer examples and installation options +- Reorganized examples directory for better discoverability +- Updated CLAUDE.md with comprehensive codebase guidance + +### Fixed +- Package configuration in pyproject.toml +- Documentation references to non-existent explainability module +- Python version inconsistencies across documentation files + +### Removed +- Empty ace/explainability module (replaced by observability) +- Outdated references to explainability features in documentation + +## [0.3.0] - 2025-10-16 + +### Added +- **Experimental v2 Prompts** with state-of-the-art prompt engineering + - Confidence scoring at bullet and answer levels + - Domain-specific variants for math and code generation + - Hierarchical structure with identity headers and metadata + - Concrete examples and anti-patterns for better guidance + - PromptManager for version control and A/B testing +- Comprehensive prompt engineering documentation (`docs/PROMPT_ENGINEERING.md`) +- Advanced examples demonstrating v2 prompts (`examples/advanced_prompts_v2.py`) +- Comparison script for v1 vs v2 prompts (`examples/compare_v1_v2_prompts.py`) +- Playbook persistence with `save_to_file()` and `load_from_file()` methods +- Example demonstrating playbook save/load functionality (`examples/playbook_persistence.py`) +- py.typed file for PEP 561 type hint support +- Mermaid flowchart visualization in README showing ACE learning loop + +### Changed +- Enhanced docstrings with comprehensive examples throughout codebase +- Improved README with v2 prompts section and visual diagrams +- Updated formatting to comply with Black code style + +### Fixed +- README incorrectly referenced non-existent docs/ directory +- Test badge URL in README (test.yml → tests.yml) +- Code formatting issues detected by GitHub Actions + +## [0.2.0] - 2025-10-15 + +### Added +- LangChain integration via `LangChainLiteLLMClient` for advanced workflows +- Router support for load balancing across multiple model deployments +- Comprehensive example for LangChain usage (`examples/langchain_example.py`) +- Optional installation group: `pip install ace-framework[langchain]` +- PyPI badges and Quick Links section in README +- CHANGELOG.md for version tracking + +### Fixed +- Parameter filtering in LiteLLM and LangChain clients (refinement_round, max_refinement_rounds) +- GitHub Actions workflow using deprecated artifact actions v3 → v4 + +### Changed +- Improved README with better structure and badges +- Updated .gitignore to exclude build artifacts and development files + +### Removed +- Unnecessary development files from repository + +## [0.1.1] - 2025-10-15 + +### Fixed +- GitHub Actions workflow for PyPI publishing +- Updated artifact upload/download actions from v3 to v4 + +## [0.1.0] - 2025-10-15 + +### Added +- Initial release of ACE Framework +- Core ACE implementation based on paper (arXiv:2510.04618) +- Three-role architecture: Generator, Reflector, and Curator +- Playbook system for storing and evolving strategies +- LiteLLM integration supporting 100+ LLM providers +- Offline and Online adaptation modes +- Async and streaming support +- Example scripts for quick start +- Comprehensive test suite +- PyPI packaging and GitHub Actions CI/CD + +### Features +- Self-improving agents that learn from experience +- Delta operations for incremental playbook updates +- Support for OpenAI, Anthropic, Google, and more via LiteLLM +- Type hints and modern Python practices +- MIT licensed for open source use + +[0.9.4]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.9.3...v0.9.4 +[0.9.3]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.9.2...v0.9.3 +[0.9.2]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.9.1...v0.9.2 +[0.9.1]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.9.0...v0.9.1 +[0.9.0]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.8.9...v0.9.0 +[0.8.8]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.8.7...v0.8.8 +[0.8.7]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.8.6...v0.8.7 +[0.8.5]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.8.4...v0.8.5 +[0.8.4]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.8.3...v0.8.4 +[0.8.3]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.8.2...v0.8.3 +[0.8.2]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.8.1...v0.8.2 +[0.8.1]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.8.0...v0.8.1 +[0.8.0]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.7.3...v0.8.0 +[0.7.3]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.7.0...v0.7.3 +[0.7.0]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.6.0...v0.7.0 +[0.6.0]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.5.0...v0.6.0 +[0.5.0]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.4.0...v0.5.0 +[0.4.0]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.1.1...v0.2.0 +[0.1.1]: https://github.com/Kayba-ai/agentic-context-engine/compare/v0.1.0...v0.1.1 +[0.1.0]: https://github.com/Kayba-ai/agentic-context-engine/releases/tag/v0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000000000000000000000000000000000..08cbe1c527f5e66f64603e12a3a5c20ba436b741 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,99 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Guidelines + +### Pipeline-First Development (MANDATORY) +**All new functionality MUST be implemented as pipeline Steps composed via the Pipeline engine.** Do NOT write standalone scripts, ad-hoc loops, or inline logic that bypasses the pipeline. Before writing any code: + +1. Read `docs/design/PIPELINE_DESIGN.md` to understand the Step → Pipeline → Branch model. +2. Implement logic as a `Step` class with `requires`/`provides` declarations and a `__call__(self, ctx) -> ctx` method. +3. Compose steps using `Pipeline().then(...)` and `.branch(...)` — never manual for-loops or direct function chaining. +4. Use `StepContext.replace()` for immutable context updates — never mutate context directly. +5. Put integration-specific data in `metadata`, not new context fields, unless the field is shared across multiple pipelines. + +**Anti-patterns to reject:** +- Writing a function that calls multiple steps manually instead of composing them in a Pipeline +- Inline reflection/evaluation logic instead of creating a ReflectStep or EvaluateStep +- Ad-hoc `ThreadPoolExecutor` usage instead of `async_boundary` and `max_workers` on steps +- Standalone scripts that duplicate pipeline functionality without using the pipeline engine +- Bypassing `requires`/`provides` contracts by accessing context fields not declared in `requires` + +If a task seems like it cannot fit the pipeline model, explain why to the user before proceeding — do not silently circumvent it. + +### Core Code Protection +**Do NOT modify core modules (`ace/core/`, `pipeline/`) without explicit user approval.** Before proposing any change to these directories: +1. Read the relevant design docs (`docs/design/ACE_ARCHITECTURE.md`, `docs/design/PIPELINE_DESIGN.md`) thoroughly. +2. Evaluate whether the change is truly required or if it can be achieved outside the core (e.g., in an integration, step, or example). +3. Clearly explain the proposed change and its justification to the user **before** making any edits. +4. Wait for the user to explicitly accept before proceeding. + +### Documentation Maintenance +Before working on code in `ace/`, read `docs/design/ACE_ARCHITECTURE.md` to understand the current architecture. +Before working on code in `pipeline/` or `ace/core/`, read `docs/design/PIPELINE_DESIGN.md` to understand the pipeline engine. + +**Docs MUST be kept in sync with code.** Any change that alters a public API, renames a concept, adds/removes a module, or changes execution flow **requires** a corresponding update to the relevant docs. Do not merge code changes that make the documentation inaccurate. + +Key design docs: +- `docs/design/ACE_ARCHITECTURE.md` — ACE architecture: layers, core concepts, roles, steps, runners, integrations +- `docs/design/ACE_REFERENCE.md` — ACE code reference: full implementations, API signatures, usage examples +- `docs/design/ACE_DECISIONS.md` — design decisions and rejected alternatives (ACE, pipeline, migration) +- `docs/design/PIPELINE_DESIGN.md` — pipeline engine: steps, StepProtocol, Pipeline, Branch, concurrency +- If you need to work with collected traces from Logfire, read `agent-guides/logfire.md` + +### Project Structure +- `ace/` — core library: roles (PydanticAI-backed), skillbook, steps, runners, providers, RR, integrations, observability +- `pipeline/` — generic pipeline engine that `ace` is built on (see `docs/design/PIPELINE_DESIGN.md`) +- `ace-eval/` — evaluation framework (submodule, separate repo) +- `tests/` — unit/integration tests (pytest) +- `examples/` — runnable demos grouped by integration +- `agent-guides/` — internal development guides for LLM agents; not part of the public docs site +- `docs/` — guides and reference material + - `docs/design/ACE_ARCHITECTURE.md` — architecture and concepts (keep in sync with code) + - `docs/design/ACE_REFERENCE.md` — code reference and examples (keep in sync with code) + - `docs/design/ACE_DECISIONS.md` — design decisions and rejected alternatives + - `docs/design/PIPELINE_DESIGN.md` — pipeline engine design doc (keep in sync with code) + +### Commands +- `uv sync` — install all dependencies +- `uv run pytest` — run tests (excludes `integration` and `requires_api` markers by default) +- `uv run pytest -m unit` / `-m integration` / `-m slow` — run by marker +- `uv run black ace/ tests/ examples/` — format code +- `uv run mypy ace/` — type check + +### Coding Style +- PEP 8 with Black formatting (line length 88) +- Type hints and docstrings for public APIs +- Python 3.12 target +- Test files: `tests/test_*.py`; functions: `test_*`; classes: `Test*` + +### Testing +- Pytest is the primary runner +- Add tests for new features; include regression tests for bug fixes + +### Commits +- Conventional Commits: `feat(scope): subject`, `fix(scope): subject` +- Do NOT add `Co-Authored-By` trailers to commit messages +- PRs should include description, test results, and relevant docs updates + +### ACE Roles (quick reference) + +| Role | Responsibility | Key Class | +|------|---------------|-----------| +| **Agent** | Executes tasks using skillbook strategies | `Agent` | +| **Reflector** | Analyzes execution results | `Reflector` | +| **SkillManager** | Updates the skillbook with new strategies | `SkillManager` | + +### Integration Runners + +| Runner | Framework | Use Case | +|--------|-----------|----------| +| `ACELiteLLM` | LiteLLM (100+ providers) | Simple self-improving agent | +| `ACELangChain` | LangChain | Wrap chains/agents with learning | +| `ACEBrowserUse` | browser-use | Browser automation with learning | +| `ACEClaudeCode` | Claude Code CLI | Coding tasks with learning | + +NEVER USE FALLBACKS OR IMPLEMENT THINGS I NEVER ASKED FOR. + +Keep your answers concise and to the point. If you don't know something, say you don't know instead of making assumptions or fabricating information. Always ask clarifying questions if the user's request is ambiguous or lacks necessary details. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..cf938f2202e6b389656b428516f39e47070450d6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,183 @@ +**By submitting a pull request to this repository, you agree to the terms below.** + +## Contributor Terms + +(a) The contribution is your original work and you have the right to submit it. +(b) You license your contribution under the project's current license (Apache-2.0). +(c) You grant the maintainers the right to relicense your contribution as part of the project under any future open-source or commercial license. + +--- + +# Contributing to ACE Framework + +Thank you for your interest in contributing to the Agentic Context Engine! We welcome contributions from the community. + +## How to Contribute + +### Reporting Bugs + +Before creating bug reports, please check existing issues to avoid duplicates. When creating a bug report, include: + +- A clear and descriptive title +- Steps to reproduce the issue +- Expected behavior vs actual behavior +- Environment details (OS, Python version, package versions) +- Any relevant error messages or logs + +### Suggesting Enhancements + +Enhancement suggestions are welcome! Please provide: + +- A clear description of the enhancement +- Use cases and benefits +- Possible implementation approach (optional) +- Any potential drawbacks or considerations + +### Pull Requests + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Run tests to ensure nothing breaks +5. Commit your changes using conventional commits (see below) +6. Push to your branch +7. Open a Pull Request + +## Branch Naming Convention + +Use consistent prefixes for branch names: + +| Prefix | Purpose | Example | +|--------|---------|---------| +| `feature/` | New features | `feature/john/benchmarks` | +| `fix/` | Bug fixes | `fix/jane/memory-leak` | +| `docs/` | Documentation changes | `docs/john/api-reference` | +| `refactor/` | Code refactoring | `refactor/jane/llm-client` | +| `test/` | Test additions/fixes | `test/john/integration-suite` | +| `chore/` | Maintenance tasks | `chore/jane/update-deps` | + +**Format:** `<type>/<developer>/<description>` + +**Rules:** +- Use lowercase with hyphens (kebab-case) +- Use your GitHub username or first name as developer identifier +- Keep descriptions short but descriptive +- Include issue number if applicable: `fix/john/123-login-error` +- Never push directly to `main` - always use feature branches + +## Worktree Workflow + +We use git worktrees to work on multiple branches simultaneously without switching. Each branch gets its own directory. + +### Claude Code Commands + +| Command | Description | Example | +|---------|-------------|---------| +| `/create-branch` | Create branch + worktree | `/create-branch feature add-caching` | +| `/checkout-branch` | Switch to branch (creates worktree if needed) | `/checkout-branch add-caching` | +| `/list-branches` | List branches with worktree status | `/list-branches` or `/list-branches feature` | +| `/remove-branch` | Remove branch + worktree | `/remove-branch feature/john/add-caching` | + +### Worktree Path Convention + +Worktrees are created as siblings to the main worktree: +- Branch: `feature/john/add-caching` +- Worktree: `../feature-john-add-caching` + +### Manual Worktree Commands + +```bash +# List all worktrees +git worktree list + +# Add worktree for existing branch +git worktree add ../path-name branch-name + +# Add worktree with new branch +git worktree add -b new-branch ../path-name + +# Remove worktree +git worktree remove ../path-name + +# Prune stale worktree references +git worktree prune +``` + +### Benefits + +- **Parallel development**: Work on multiple features without stashing +- **Faster context switching**: No need to rebuild dependencies +- **Cleaner git history**: No accidental commits to wrong branch +- **IDE-friendly**: Open each worktree in separate IDE windows + +## Development Setup + +```bash +# Clone your fork +git clone https://github.com/your-username/agentic-context-engine.git +cd agentic-context-engine + +# Install all dependencies (uses UV - 10-100x faster than pip) +uv sync + +# Run tests +uv run pytest + +# Run linting and formatting +uv run black ace/ tests/ examples/ +uv run mypy ace/ + +# Run specific test files +uv run pytest tests/test_skillbook.py +uv run pytest -m unit # Only unit tests +uv run pytest -m integration # Only integration tests +``` + +## Commit Message Format + +We use [Conventional Commits](https://www.conventionalcommits.org/) for clear commit history and automatic changelog generation. + +Format: `<type>(<scope>): <subject>` + +Types: +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation only +- `style`: Code style changes (formatting, etc.) +- `refactor`: Code refactoring +- `test`: Adding tests +- `chore`: Maintenance tasks + +Examples: +``` +feat(llm): add support for new LLM provider +fix(adapter): resolve memory leak in online mode +docs(readme): update installation instructions +``` + +## Code Style + +- Follow PEP 8 +- Use type hints where possible +- Add docstrings to all public functions and classes +- Keep line length under 100 characters +- Use Black for automatic formatting + +## Testing + +- Write tests for new features +- Ensure all tests pass before submitting PR +- Aim for good test coverage +- Use meaningful test names + +## Documentation + +- Update README.md if adding new features +- Add docstrings to new code +- Update CHANGELOG.md following Keep a Changelog format +- Include examples for new functionality + +## Questions? + +Feel free to open an issue for any questions or join the discussion in [GitHub Discussions](https://github.com/Kayba-ai/agentic-context-engine/discussions). + diff --git a/Dockerfile b/Dockerfile index 4014a51c4ad2c200d4ed3e5b16dcaee667d377ac..93ee57a8facecd8ca714cc5426087176e7ce9c29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,20 @@ -FROM mcr.microsoft.com/playwright:v1.44.0-jammy +FROM python:3.10-slim WORKDIR /app -# Install python and pip -RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/* +# Install uv for fast dependency resolution as recommended by ACE +RUN pip install uv -COPY requirements.txt . -RUN pip3 install --no-cache-dir -r requirements.txt +# Copy the ACE repository and our FastAPI files +COPY . /app -# Install playwright browsers -RUN pip3 install playwright -RUN playwright install chromium - -COPY . . +# Install ACE framework using uv (referencing the local pyproject.toml in the cloned repo) +# and install FastAPI components +RUN uv pip install --system fastapi uvicorn pydantic litellm +# Since the cloned directory has pyproject.toml, we can install the local package +RUN uv pip install --system -e . EXPOSE 7860 +# We need the user to pass API keys in Space Secrets (e.g. OPENAI_API_KEY, GROQ_API_KEY) CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..29f81d812f3e768fa89638d1f72920dbfd1413a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 173ac81519b8fecf2d70d632a74b9a4866a46680..fe8ed3459ad2fa4d876739456bfcd1e5e114de55 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,224 @@ ---- -title: Logic Engine -emoji: 🏃 -colorFrom: indigo -colorTo: yellow -sdk: docker -pinned: false ---- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +<a href="https://kayba.ai"><img src="assets/kayba-banner.png" alt="Kayba - Stop fixing agents by hand" width="1080"/></a> + +# Agentic Context Engine (ACE) + +[![GitHub stars](https://img.shields.io/github/stars/kayba-ai/agentic-context-engine)](https://github.com/kayba-ai/agentic-context-engine/stargazers) +[![Kayba Website](https://img.shields.io/badge/kayba.ai-6B8BA8?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAIpElEQVR42q1XbWwU1xU9d2Z29sPe2V3jyHVwDNixkiBiqKKixA1IBloKCRJSAaU4ip0PhaiJE0JSqSU/WqlpihpXiaJGmJBGSiGOEj4MjsuHhDApENzEjYHaxTbGJUUuBsvaHXt3vbuzM6c/1l6MvW7+5EpPu/t23rvnnvfm3HsFgArAzsvLu1/TtF+SrAYQBKDguzUHgCkipyyRHfFI5AIAVQDA7/evE5E9IhIgp64hAJmxkwggItmRWUOQhENmliHXPpx4nmMiUmuaZrPk5+cvVBSlQ0S8JNMZRkRy7AIRgaqqsG0HyWQCqVQKjuMAABRFhdutw+32QASwbWcagOwXW0Q0kilFUX6giai/FsGkc23agqypqoZ02oJpmvB6vZi/YAHmlZaisLAQiqJgeHgYl/v7MTAwADoODMOAoigTAGVyTwGgkUyLiO44zm/EMIxhEZlDZh+YGvOEcxWmGUEwGMRjjz2GjRs2YunSpfDl+QAAdtrG+Pg4TNPEpUuX0PTxxzh48ADGx8fh9XpBTg9IKAI4pCmGYSQAceekHAJFVRCJRLB27Vq8/tvXseT7SwAAHR0dOHz4MDo6OnD9+nVYloX8vHyUlZdhcWUlUpaF5uZm9Pf3Q1XV20BM3AMASMMwjIRhGMw1CgoKCIAvvPACU8kkSXJgYIBPPPEEjUCAE7zmHHPvnMuqqioWFRXR7/czEAjk8mHlYCBzXpqmIRwOY9OmTfho70eZqP/RgdraWvT29kJEcN999+Ghhx7C3eV3w+P1IBwOo7u7G+3t7RgcHITH44HH44HjOFOjnmqTDExFF2AwGKTP5+OCBQt47T/XmE6n2dXVxbvuuosAWFRUxIaGBl7/73VONytlsedSD7dt20afz8f8/HwGg0HOwrKV8wgmqX/zD2+SJGPRGNesWUMALCkpYVtbW8ZZ0mLn151sOdzC5uZmfvXlV4xFY1kwB/YfoGEY/w/ETACBQID5+fksKvoeey71kCT/2tpKXdfpdru55y97SJKdX3dy3bp1LCwspNvtpsfjYUFBAVetXMVjx44xmcjcmU8++YS6rjMQCOS6BzMBhEIhqqrKVatWMRaN0bEdPv/z5wmA1dXVTCaT7LnUw7KysuyF03WdiqJQRKhpGl0uF3fu3EkrZZEkt2/fTgAsKCiYAUDJpXa2baOsrAxerxfxeBy9vb0AgJUrV0LXdTT8sQEDAwNYvXo1PvjgA5xqO4VDzYewefNmuN1ueDwe1NfX48jRIwCBl7e+jIULFyIej0NRbnc5A8DkTfX7/RBFkEgmYI6aAICysjKMjY7h+PHjCAaD8Pv9OHToEHa9twv33HMP9u7di3f/9C5EFOi6jtdeew1DQ0MovKMQGzdsRCKR+BYAItB1HQBgmqMgCU3VsnNpK40rV67gxtANkMT+/fvR0tKCDz/8EFU/rMKXf/8StXW12PLss4jH4+jq6kJ7ezsIoqqqCrquZ1/JaQAyE4oIysvLoaoqLvf1IRaNId+fj/KycgDAyZMnYaUtpO00HMfBli1b8NRTTyMUCmFkZASNjY2wbRt1dXV46623cPGfF7Fi5Qok4gnMmz8PwWAQ6XQ6NwMiAsuyUFFRgVAohPMXzqPvch9UVcVP1qyBoggOHDyIM2fOYHHlYgQCATQ2NuLP77+PQCCQAd1/GclEEqXzSrF582ZcvHARra2tUDUNXo8XPq8vmz1nAFBVBfF4HEVFRaioqIBpmmhqagIAPPLIWvxo1Y8xNjaKhoYGpFIpjIyMYPuvtuPVX7yKSCQCkiicUwiPx4NIOILly5bh8ccfR9vJNui6C8lkElbauo1+AMgqYSiUEZ8n657kG797I/vanD1zliTZ19vHBx54gACy77yqqnS5XPR6vdQ0jUePHKVlWezu6mZxcTEB8J133iFJfvHFOfr9fhq360FGBwJGZtLn87G0tJSdX3eyoqKCIsJFixbx6r+vkiSvXbvG+vp6FhcXU9O07CiZO5d79+xlOp1mdCzK8fFxfvPNN3zmmWfYfq6dJLl79+4pWjAdwASqSQne1biLx48dnxAZN5csWcLzneezEtvb08sD+/fzvV3vsampiVf6rzCVSDEei7OpqYnLli1jS0sLk8kkw+Ew01aa69evp6IoDIVCMwFkJiaSUF4eS0pKaEZM7vj9DgKg2+1mcXEx3377bd68cZO5LBaNcWRkhPfee282ZwxcGaBt2zz9t9P0+Xy55PgWA4ZxiwUR4aZNm0iSO3bsoMfjoaIoVFWVlfdX8qWXXmLjzkZevXqVfX193Lp1Kx999FFGo1Hu3r2bJXNL2H6unalkhpXq6mpqmjYRvTEbAzOz4SvbXiFJtp1s44MPPkiXy3Vb0XH69GmeOHEi+/u5555jMplkV1cXU4kU6ZAv1r84Wx6YPR0bgQBDoRABsK62jqlkiiS579N9rKmp4eLFi+n3+/n5qc957tw5lpeXs6amhvs+3cdwOELHdhiPxTPORaaf+1QWcgEITMmMGSYqKyv5Wctn2fMeGxtjd3c3BwcHOTw8zBtDN7L/2WmbZ8+e5YoVKwhgFuczSjK4Z2tnNE1DNBqF4zhYvnw5Nvx0Ax5++GHMnz8fLpcLJGFZFgYHB3HhwgUcOnwYra2tSCTGEQgEZkjv9JJMDMMYBmTOlLp9Sh2f+VQUFQAxNjYG27YRDIZw553FKCgogCIKRsdGMXR9CDeHb8JxCMMwoKoKbNuezTEBAemMimEY+0Rkw63GRHI2JoBAVRVABGkrDctKZaNTFBW67oLL5co0gY6TqwCdaraIqCRbJC8vb5Gqqh0i4r7Vmk1nYmbRckvTJdsXfovTyZbLmXBuichSJRaLdYnIz0hGRUTL9IXIycAkLpKgQziOA8exp0Q8y9Jb8zLhPCYiNaZpnlcBqMlk8l8ej+cYyTkA7gCgT6L9DocNIAzgqIg8bZrmCQDq/wBcV6BSGdN3ewAAAABJRU5ErkJggg==&logoColor=white)](https://kayba.ai) +[![Discord](https://img.shields.io/discord/1429935408145236131?label=Discord&logo=discord&logoColor=white&color=5865F2)](https://discord.gg/mqCqH7sTyK) +[![Twitter Follow](https://img.shields.io/twitter/follow/kaybaai?style=social)](https://twitter.com/kaybaai) +[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://kayba-ai.github.io/agentic-context-engine/latest/) + +> [!TIP] +> ACE is the open-source engine behind [Kayba](https://kayba.ai). If you'd rather have the whole loop managed for you, from failure investigation to fixes shipped as PRs, [get a demo](https://kayba.ai). + +--- + +**AI agents don't learn from experience.** They repeat the same mistakes every session, forget what worked, and ignore what failed. ACE is the open-source engine that adds a persistent learning loop. It also powers [Kayba](https://kayba.ai), the managed service that does this for your production agents automatically. + +<img src="examples/seahorse-emoji-ace.gif" alt="ACE learns from mistakes in real time" width="70%"/> + +> The agent claims a seahorse emoji exists. ACE reflects on the error, and on the next attempt, the agent responds correctly — without human intervention. + +--- + +## Proven Results + +| Metric | Result | Context | +|:-------|:-------|:--------| +| **2x consistency** | Doubles pass^4 on Tau2 airline benchmark | 15 learned strategies, no reward signals | +| **49% token reduction** | Browser automation costs cut nearly in half | 10-run learning curve | +| **$1.50 learning cost** | Claude Code translated 14k lines to TypeScript | Zero build errors, all tests passing | + +--- + +## Quick Start + +```bash +uv add ace-framework +``` + +**Option A** — Interactive setup (recommended): + +```bash +ace setup # Walks you through model selection, API keys, and connection validation +``` + +**Option B** — Manual configuration: + +```bash +export OPENAI_API_KEY="your-key" # or ANTHROPIC_API_KEY, or any of 100+ supported providers +``` + +Then use it: + +```python +from ace import ACELiteLLM + +agent = ACELiteLLM(model="gpt-4o-mini") + +# First attempt — the agent may hallucinate +answer = agent.ask("Is there a seahorse emoji?") + +# Feed a correction — ACE extracts a strategy and updates the Skillbook +agent.learn_from_feedback("There is no seahorse emoji in Unicode.") + +# Subsequent calls benefit from the learned strategy +answer = agent.ask("Is there a seahorse emoji?") + +# Inspect what the agent has learned +print(agent.get_strategies()) +``` + +No fine-tuning, no training data, no vector database. + +[-> Quick Start Guide](https://kayba-ai.github.io/agentic-context-engine/latest/getting-started/quick-start/) | [-> Setup Guide](https://kayba-ai.github.io/agentic-context-engine/latest/getting-started/setup/) | [-> Hosted API: Where Do Traces Come From?](https://kayba-ai.github.io/agentic-context-engine/latest/integrations/hosted-api/#where-do-traces-come-from) + +--- + +## How It Works + +ACE maintains a **Skillbook** — a persistent collection of strategies that evolves with every task. Three specialized roles manage the learning loop: + +| Role | Responsibility | +|:-----|:---------------| +| **Agent** | Executes tasks, enhanced with Skillbook strategies | +| **Reflector** | Analyzes execution traces to extract what worked and what failed | +| **SkillManager** | Curates the Skillbook — adds, refines, and removes strategies | + +The **Recursive Reflector** is the key innovation: instead of summarizing traces in a single pass, it writes and executes Python code in a sandboxed environment to programmatically search for patterns, isolate errors, and iterate until it finds actionable insights. + +```mermaid +flowchart LR + Skillbook[(Skillbook)] + Start([Task]) --> Agent[Agent] + Agent <--> Environment[Environment] + Environment -- Trace --> Reflector[Reflector] + Reflector --> SkillManager[SkillManager] + SkillManager -- Updates --> Skillbook + Skillbook -. Strategies .-> Agent +``` + +All roles are backed by [PydanticAI](https://ai.pydantic.dev/) agents with structured output validation. PydanticAI routes to 100+ LLM providers through its LiteLLM integration, with native support for OpenAI, Anthropic, Google, Bedrock, Groq, and more. + +*Based on the [ACE paper](https://arxiv.org/abs/2510.04618) (Stanford & SambaNova) and [Dynamic Cheatsheet](https://arxiv.org/abs/2504.07952).* + +--- + +## Runners + +| Runner | Class | Description | +|:-------|:------|:------------| +| **LiteLLM** | `ACELiteLLM` | Batteries-included agent with `.ask()`, `.learn()`, `.save()` — accepts any [LiteLLM model string](https://docs.litellm.ai/docs/providers) | +| **Core** | `ACE` | Full learning loop with batch epochs and evaluation | +| **Trace Analyser** | `TraceAnalyser` | Learn from pre-recorded traces without re-running tasks | +| **browser-use** | `BrowserUse` | Browser automation that improves with each run | +| **LangChain** | `LangChain` | Wrap any LangChain chain or agent with learning | +| **Claude Code** | `ClaudeCode` | Claude Code CLI tasks with learning | + +```bash +uv add 'ace-framework[browser-use]' # Browser automation +uv add 'ace-framework[langchain]' # LangChain +uv add 'ace-framework[logfire]' # Observability (auto-instruments PydanticAI) +uv add 'ace-framework[mcp]' # MCP server for IDE integration +uv add 'ace-framework[deduplication]' # Embedding-based skill deduplication +``` + +Have existing agent logs? Extract strategies from them directly: + +```python +from ace import ACELiteLLM + +agent = ACELiteLLM(model="gpt-4o-mini") +agent.learn_from_traces(your_existing_traces) +print(agent.get_strategies()) +``` + +[-> Examples](examples/) + +--- + +## Benchmarks + +### Tau2 — Multi-Step Agentic Tasks + +[tau2-bench](https://github.com/sierra-research/tau2-bench) by Sierra Research: airline domain tasks requiring tool use and policy adherence. Claude Haiku 4.5 agent, strategies learned on the train split with no reward signals, evaluated on the held-out test split. + +<img src="benchmarks/tasks/tau_bench/Tau2Benchmark Result Haiku4.5.png" alt="Tau2 Benchmark — ACE doubles consistency at pass^4" width="35%"/> + +*pass^k = probability all k independent attempts succeed. ACE doubles consistency at pass^4 with 15 learned strategies.* + +### Claude Code — Autonomous Translation + +ACE + Claude Code translated this library from Python to TypeScript with zero supervision: + +| Metric | Result | +|:-------|:-------| +| Duration | ~4 hours | +| Commits | 119 | +| Lines written | ~14,000 | +| Build errors | 0 | +| Tests | All passing | +| Learning cost | ~$1.50 | + +--- + +## Pipeline Architecture + +ACE is built on a composable pipeline engine. Each step declares what it requires and what it produces: + +``` +AgentStep -> EvaluateStep -> ReflectStep -> UpdateStep -> DeduplicateStep +``` + +Use `learning_tail()` for the standard learning sequence, or compose custom pipelines: + +```python +from ace import Pipeline, AgentStep, EvaluateStep, learning_tail + +steps = [AgentStep(agent, skillbook), EvaluateStep(env)] + learning_tail(reflector, skill_manager, skillbook) +pipeline = Pipeline(steps) +``` + +The pipeline engine ([`pipeline/`](pipeline/)) is framework-agnostic with `requires`/`provides` contracts, immutable context, and error isolation. See [Pipeline Design](docs/design/PIPELINE_DESIGN.md) and [Architecture](docs/design/ACE_ARCHITECTURE.md). + +--- + +## CLI + +| Command | Description | +|:--------|:------------| +| `ace setup` | Interactive setup — model selection, API keys, connection validation | +| `ace models <query>` | Search available models with pricing | +| `ace validate <model>` | Test a model connection | +| `ace config` | Show current configuration | +| `kayba` | Cloud CLI — upload traces, fetch insights, manage prompts | +| `ace-mcp` | MCP server for IDE integration | + +--- + +## Documentation + +- [Full Documentation](https://kayba-ai.github.io/agentic-context-engine/latest/) — Guides, API reference, examples +- [Quick Start](https://kayba-ai.github.io/agentic-context-engine/latest/getting-started/quick-start/) — 5-minute setup +- [Setup Guide](https://kayba-ai.github.io/agentic-context-engine/latest/getting-started/setup/) — Configuration and providers +- [Hosted API Guide](https://kayba-ai.github.io/agentic-context-engine/latest/integrations/hosted-api/) — Hosted CLI, trace upload, prompt install +- [Architecture](docs/design/ACE_ARCHITECTURE.md) — Core concepts and system design +- [Code Reference](docs/design/ACE_REFERENCE.md) — Implementations, API, usage examples +- [Design Decisions](docs/design/ACE_DECISIONS.md) — Rejected alternatives and rationale +- [Pipeline Engine](docs/design/PIPELINE_DESIGN.md) — Step composition and context flow +- [Examples](examples/) — Runnable demos +- [Changelog](CHANGELOG.md) — Version history + +--- + +## Contributing + +Contributions are welcome. See [Contributing Guidelines](CONTRIBUTING.md). + +--- + +<div align="center"> + +**Built by [Kayba](https://kayba.ai) and the open-source community.** + +</div> diff --git a/ace.toml b/ace.toml new file mode 100644 index 0000000000000000000000000000000000000000..ecde022420cdd137c928942cce942c054ec16198 --- /dev/null +++ b/ace.toml @@ -0,0 +1,2 @@ +[default] +model = "us.writer.palmyra-x5-v1:0" diff --git a/ace/__init__.py b/ace/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..69d166d87bcd157575279777743f64a6ae14f025 --- /dev/null +++ b/ace/__init__.py @@ -0,0 +1,197 @@ +"""ACE — Agentic Context Engine. + +All public symbols are lazily imported to keep ``import ace`` fast. +Direct attribute access (``ace.ACE``, ``from ace import ACE``) +works — the underlying module is loaded on first use. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Static analysis / IDE autocomplete — never executed at runtime. + from pipeline import Branch, MergeStrategy, Pipeline, SampleResult, StepProtocol + + from .core import ( + ACEStepContext, + EnvironmentResult, + InsightSource, + Sample, + SimpleEnvironment, + Skill, + Skillbook, + SkillbookView, + TaskEnvironment, + TraceIdentity, + UpdateBatch, + UpdateOperation, + ) + from .deduplication import DeduplicationManager, SimilarityDetector + from .implementations import Agent, Reflector, SkillManager + from .integrations import wrap_skillbook_context + from .protocols import DeduplicationConfig + from .providers import ACEModelConfig, ModelConfig + from .steps.rr_step import RRConfig, RRStep + from .tracing import configure as configure_tracing + from .runners import ( + ACE, + ACELiteLLM, + ACERunner, + BrowserUse, + ClaudeCode, + LangChain, + TraceAnalyser, + ) + from .steps import ( + AgentStep, + CheckpointStep, + DeduplicateStep, + EvaluateStep, + ExportSkillbookMarkdownStep, + LoadTracesStep, + ObservabilityStep, + PersistStep, + ReflectStep, + UpdateStep, + learning_tail, + ) + +# ---- lazy import mapping: name -> (module_path, attribute) ---------------- + +_LAZY_IMPORTS: dict[str, tuple[str, str]] = { + # Pipeline engine (re-exported from pipeline/) + "Pipeline": ("pipeline", "Pipeline"), + "Branch": ("pipeline", "Branch"), + "MergeStrategy": ("pipeline", "MergeStrategy"), + "StepProtocol": ("pipeline", "StepProtocol"), + "SampleResult": ("pipeline", "SampleResult"), + # ACE context + "ACEStepContext": ("ace.core", "ACEStepContext"), + "SkillbookView": ("ace.core", "SkillbookView"), + # Core data types + "InsightSource": ("ace.core", "InsightSource"), + "Skill": ("ace.core", "Skill"), + "Skillbook": ("ace.core", "Skillbook"), + "TraceIdentity": ("ace.core", "TraceIdentity"), + "UpdateOperation": ("ace.core", "UpdateOperation"), + "UpdateBatch": ("ace.core", "UpdateBatch"), + "Sample": ("ace.core", "Sample"), + "EnvironmentResult": ("ace.core", "EnvironmentResult"), + "TaskEnvironment": ("ace.core", "TaskEnvironment"), + "SimpleEnvironment": ("ace.core", "SimpleEnvironment"), + # Implementations + "Agent": ("ace.implementations", "Agent"), + "Reflector": ("ace.implementations", "Reflector"), + "SkillManager": ("ace.implementations", "SkillManager"), + # Deduplication + "DeduplicationConfig": ("ace.protocols", "DeduplicationConfig"), + "DeduplicationManager": ("ace.deduplication", "DeduplicationManager"), + "SimilarityDetector": ("ace.deduplication", "SimilarityDetector"), + # Integrations + "wrap_skillbook_context": ("ace.integrations", "wrap_skillbook_context"), + # Config + "ModelConfig": ("ace.providers", "ModelConfig"), + "ACEModelConfig": ("ace.providers", "ACEModelConfig"), + # Runners + "ACE": ("ace.runners", "ACE"), + "ACELiteLLM": ("ace.runners", "ACELiteLLM"), + "ACERunner": ("ace.runners", "ACERunner"), + "BrowserUse": ("ace.runners", "BrowserUse"), + "ClaudeCode": ("ace.runners", "ClaudeCode"), + "LangChain": ("ace.runners", "LangChain"), + "TraceAnalyser": ("ace.runners", "TraceAnalyser"), + # Steps + "AgentStep": ("ace.steps", "AgentStep"), + "EvaluateStep": ("ace.steps", "EvaluateStep"), + "ReflectStep": ("ace.steps", "ReflectStep"), + "UpdateStep": ("ace.steps", "UpdateStep"), + "DeduplicateStep": ("ace.steps", "DeduplicateStep"), + "CheckpointStep": ("ace.steps", "CheckpointStep"), + "LoadTracesStep": ("ace.steps", "LoadTracesStep"), + "ExportSkillbookMarkdownStep": ("ace.steps", "ExportSkillbookMarkdownStep"), + "ObservabilityStep": ("ace.steps", "ObservabilityStep"), + "PersistStep": ("ace.steps", "PersistStep"), + "learning_tail": ("ace.steps", "learning_tail"), + # Recursive Reflector + "RRStep": ("ace.steps.rr_step", "RRStep"), + "RRConfig": ("ace.steps.rr_step", "RRConfig"), + # Tracing + "configure_tracing": ("ace.tracing", "configure"), +} + + +def __getattr__(name: str) -> object: + if name in _LAZY_IMPORTS: + module_path, attr = _LAZY_IMPORTS[name] + import importlib + + module = importlib.import_module(module_path) + value = getattr(module, attr) + # Cache on the module so __getattr__ is only called once per name. + globals()[name] = value + return value + raise AttributeError(f"module 'ace' has no attribute {name!r}") + + +__all__ = [ + # Pipeline composition + "Pipeline", + "Branch", + "MergeStrategy", + "StepProtocol", + "SampleResult", + # ACE context + "ACEStepContext", + "SkillbookView", + # Core data types + "InsightSource", + "Skill", + "Skillbook", + "TraceIdentity", + "UpdateOperation", + "UpdateBatch", + # Environments + "Sample", + "EnvironmentResult", + "TaskEnvironment", + "SimpleEnvironment", + # Implementations + "Agent", + "Reflector", + "SkillManager", + # Config + "ModelConfig", + "ACEModelConfig", + # Runners + "ACE", + "ACELiteLLM", + "ACERunner", + "BrowserUse", + "ClaudeCode", + "LangChain", + "TraceAnalyser", + # Steps + "AgentStep", + "EvaluateStep", + "ReflectStep", + "UpdateStep", + "DeduplicateStep", + "CheckpointStep", + "LoadTracesStep", + "ExportSkillbookMarkdownStep", + "ObservabilityStep", + "PersistStep", + "learning_tail", + # Recursive Reflector + "RRStep", + "RRConfig", + # Deduplication + "DeduplicationConfig", + "DeduplicationManager", + "SimilarityDetector", + # Tracing + "configure_tracing", + # Utilities + "wrap_skillbook_context", +] diff --git a/ace/cli/__init__.py b/ace/cli/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6f3a7b75d7dca65b102f6ff58bbd7a2fd49fe94e --- /dev/null +++ b/ace/cli/__init__.py @@ -0,0 +1,39 @@ +"""Kayba CLI.""" + +import click + +from ace.cli.cloud import ( + upload, + traces, + run, + insights, + prompts, + status, + materialize, + batch, + setup, + integrations, +) + + +@click.group() +@click.version_option(package_name="ace-framework") +def cli(): + """Kayba CLI.""" + pass + + +cli.add_command(upload) +cli.add_command(traces) +cli.add_command(run) +cli.add_command(insights) +cli.add_command(prompts) +cli.add_command(status) +cli.add_command(materialize) +cli.add_command(batch) +cli.add_command(setup) +cli.add_command(integrations) + + +def main(): + cli() diff --git a/ace/cli/client.py b/ace/cli/client.py new file mode 100644 index 0000000000000000000000000000000000000000..c3674f77ba1e2c3b4ff7f3abfc0ad1ffd1a08c61 --- /dev/null +++ b/ace/cli/client.py @@ -0,0 +1,310 @@ +"""HTTP client for the Kayba hosted API.""" + +from __future__ import annotations + +import json +import os +import re +from typing import Any, Dict, List, Optional + + +class KaybaAPIError(Exception): + """Structured error from the Kayba API.""" + + def __init__(self, code: str, message: str, status_code: int = 0): + self.code = code + self.message = message + self.status_code = status_code + super().__init__(f"[{code}] {message}") + + +DEFAULT_BASE_URL = "https://use.kayba.ai/api" +MAX_TRACE_UPLOAD_BODY_BYTES = 900_000 + + +def _chunk_trace_uploads( + traces: List[Dict[str, Any]], +) -> List[List[Dict[str, Any]]]: + """Split uploads into request-sized batches under the body size cap.""" + batches: List[List[Dict[str, Any]]] = [] + current: List[Dict[str, Any]] = [] + current_size = len('{"traces":[]}') + + for trace in traces: + trace_size = len( + json.dumps(trace, ensure_ascii=False, separators=(",", ":")).encode( + "utf-8" + ) + ) + separator_size = 1 if current else 0 + candidate_size = current_size + separator_size + trace_size + + if current and candidate_size > MAX_TRACE_UPLOAD_BODY_BYTES: + batches.append(current) + current = [trace] + current_size = len('{"traces":[]}') + trace_size + continue + + current.append(trace) + current_size = candidate_size + + if current: + batches.append(current) + + return batches + + +class KaybaClient: + """HTTP client for the Kayba hosted API. + + Args: + api_key: Kayba API key. Falls back to KAYBA_API_KEY env var. + base_url: API base URL. Falls back to KAYBA_API_URL env var, + then to https://use.kayba.ai/api. + """ + + def __init__( + self, + api_key: Optional[str] = None, + base_url: Optional[str] = None, + ): + try: + import requests + except ImportError as exc: + raise KaybaAPIError( + "DEPENDENCY_MISSING", + "The hosted Kayba CLI requires the cloud extra. Install with " + "`uv add \"ace-framework[cloud]\"` or " + "`pip install 'ace-framework[cloud]'`.", + ) from exc + + self.api_key = api_key or os.environ.get("KAYBA_API_KEY", "") + if not self.api_key: + raise KaybaAPIError( + "AUTH_MISSING", + "No API key provided. Set KAYBA_API_KEY or pass --api-key.", + ) + self.base_url = ( + base_url or os.environ.get("KAYBA_API_URL") or DEFAULT_BASE_URL + ).rstrip("/") + self.session: Any = requests.Session() + self.session.headers["Authorization"] = f"Bearer {self.api_key}" + + @staticmethod + def _summarize_http_body(body: str, limit: int = 240) -> str: + """Collapse whitespace so raw HTML and proxy errors stay readable.""" + snippet = re.sub(r"\s+", " ", body or "").strip() + if not snippet: + return "Unexpected non-JSON error from the Kayba API." + if len(snippet) <= limit: + return snippet + return snippet[: limit - 3] + "..." + + def _request( + self, + method: str, + path: str, + *, + json: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, str]] = None, + ) -> Any: + """Send a request and return parsed JSON, raising on API errors.""" + url = f"{self.base_url}{path}" + resp = self.session.request(method, url, json=json, params=params) + + if resp.status_code >= 400: + try: + body = resp.json() + err = body.get("error", {}) + if isinstance(err, str): + raise KaybaAPIError( + code="API_ERROR", + message=err, + status_code=resp.status_code, + ) + message = err.get("message", resp.text) + if ( + resp.status_code == 413 + or "maximum content size" in message.lower() + or "too large" in message.lower() + ): + raise KaybaAPIError( + code="PAYLOAD_TOO_LARGE", + message=message, + status_code=resp.status_code, + ) + raise KaybaAPIError( + code=err.get("code", "UNKNOWN"), + message=message, + status_code=resp.status_code, + ) + except (ValueError, KeyError, AttributeError): + message = self._summarize_http_body(resp.text) + if resp.status_code == 413: + message = ( + "Upload rejected because the request body is too large. " + "Try smaller traces or upload fewer files at once." + ) + elif resp.status_code in (401, 403): + message = "Authentication failed; check KAYBA_API_KEY" + else: + message = f"HTTP {resp.status_code} from Kayba API: {message}" + raise KaybaAPIError( + code="HTTP_ERROR", + message=message, + status_code=resp.status_code, + ) + + if resp.status_code == 204: + return {} + return resp.json() + + # -- Traces -- + + def upload_traces(self, traces: List[Dict[str, Any]]) -> Dict[str, Any]: + """Upload trace files. + + Args: + traces: List of dicts with keys: filename, content, fileType. + """ + batches = _chunk_trace_uploads(traces) + if len(batches) == 1: + return self._request("POST", "/traces", json={"traces": traces}) + + combined: Dict[str, Any] = {"count": 0, "traces": []} + for batch in batches: + result = self._request("POST", "/traces", json={"traces": batch}) + uploaded = result.get("traces", []) + combined["count"] += result.get("count", len(uploaded) or len(batch)) + combined["traces"].extend(uploaded) + for key, value in result.items(): + if key not in {"count", "traces"} and key not in combined: + combined[key] = value + return combined + + def list_traces(self) -> Dict[str, Any]: + """List all traces (metadata only, no content).""" + return self._request("GET", "/traces") + + def get_trace(self, trace_id: str) -> Dict[str, Any]: + """Get a single trace with full content.""" + return self._request("GET", f"/traces/{trace_id}") + + def get_traces(self, trace_ids: List[str]) -> Dict[str, Any]: + """Batch get traces by IDs (with content).""" + return self._request("POST", "/traces/batch", json={"ids": trace_ids}) + + def delete_trace(self, trace_id: str) -> Dict[str, Any]: + """Delete a single trace.""" + return self._request("DELETE", f"/traces/{trace_id}") + + def delete_traces(self, trace_ids: List[str]) -> Dict[str, Any]: + """Delete multiple traces.""" + results = [] + errors = [] + for tid in trace_ids: + try: + self.delete_trace(tid) + results.append(tid) + except KaybaAPIError as e: + errors.append({"id": tid, "error": str(e)}) + return {"deleted": results, "errors": errors} + + # -- Insights -- + + def generate_insights( + self, + *, + trace_ids: Optional[List[str]] = None, + model: Optional[str] = None, + epochs: Optional[int] = None, + reflector_mode: Optional[str] = None, + anthropic_key: Optional[str] = None, + ) -> Dict[str, Any]: + """Start async insight generation.""" + body: Dict[str, Any] = {} + if trace_ids: + body["traceIds"] = trace_ids + if model: + body["model"] = model + if epochs is not None: + body["epochs"] = epochs + if reflector_mode: + body["reflectorMode"] = reflector_mode + if anthropic_key: + body["anthropicApiKey"] = anthropic_key + return self._request("POST", "/insights/generate", json=body) + + def list_insights( + self, + *, + status: Optional[str] = None, + section: Optional[str] = None, + ) -> Dict[str, Any]: + """List insights, optionally filtered.""" + params: Dict[str, str] = {} + if status: + params["status"] = status + if section: + params["section"] = section + return self._request("GET", "/insights", params=params or None) + + def triage_insight( + self, + insight_id: str, + status: str, + note: Optional[str] = None, + ) -> Dict[str, Any]: + """Accept or reject a single insight.""" + body: Dict[str, Any] = {"status": status} + if note: + body["note"] = note + return self._request("PATCH", f"/insights/{insight_id}", json=body) + + # -- Jobs -- + + def get_job(self, job_id: str) -> Dict[str, Any]: + """Get job status.""" + return self._request("GET", f"/jobs/{job_id}") + + def materialize_job(self, job_id: str) -> Dict[str, Any]: + """Materialize completed job results into the skillbook.""" + return self._request("POST", f"/jobs/{job_id}") + + # -- Prompts -- + + def generate_prompt( + self, + *, + insight_ids: Optional[List[str]] = None, + label: Optional[str] = None, + ) -> Dict[str, Any]: + """Generate a prompt from accepted insights.""" + body: Dict[str, Any] = {} + if insight_ids: + body["insightIds"] = insight_ids + if label: + body["label"] = label + return self._request("POST", "/prompts/generate", json=body) + + def list_prompts(self) -> Dict[str, Any]: + """List all prompt versions.""" + return self._request("GET", "/prompts") + + def get_prompt(self, prompt_id: str) -> Dict[str, Any]: + """Get a specific prompt by ID.""" + return self._request("GET", f"/prompts/{prompt_id}") + + # -- Integrations -- + + def get_integrations(self) -> Dict[str, Any]: + """Get current integration settings.""" + return self._request("GET", "/integrations") + + def update_integration(self, name: str, config: Dict[str, Any]) -> Dict[str, Any]: + """Update an integration's config.""" + return self._request("PUT", f"/integrations/{name}", json=config) + + def test_integration(self, name: str) -> Dict[str, Any]: + """Test an integration connection.""" + return self._request("POST", f"/integrations/{name}/test") diff --git a/ace/cli/cloud.py b/ace/cli/cloud.py new file mode 100644 index 0000000000000000000000000000000000000000..d599c219767922f50de91efe7814503e5932ce8d --- /dev/null +++ b/ace/cli/cloud.py @@ -0,0 +1,1463 @@ +"""Kayba CLI — commands for the Kayba hosted API.""" + +from __future__ import annotations + +import importlib.resources +import json +import re +import sys +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +import click + +from ace.cli.client import KaybaClient, KaybaAPIError + +# Shared options applied to every command. +_api_key_option = click.option( + "--api-key", + envvar="KAYBA_API_KEY", + help="Kayba API key (or set KAYBA_API_KEY).", +) +_base_url_option = click.option( + "--base-url", + envvar="KAYBA_API_URL", + help="API base URL (default: https://use.kayba.ai/api).", +) + +MAX_TRACE_CHARS = 350_000 +PROMPT_BLOCK_START = "<!-- KAYBA:PROMPT:START -->" +PROMPT_BLOCK_END = "<!-- KAYBA:PROMPT:END -->" +PROMPT_INSTALL_TARGETS = { + "universal": ("AGENTS.md", "most coding agents"), + "codex": ("AGENTS.md", "Codex"), + "windsurf": ("AGENTS.md", "Windsurf"), + "claude-code": ("CLAUDE.md", "Claude Code"), + "cursor": (".cursorrules", "Cursor"), +} + + +def _client(api_key: Optional[str], base_url: Optional[str]) -> KaybaClient: + """Build a KaybaClient, surfacing auth errors as click failures.""" + try: + return KaybaClient(api_key=api_key, base_url=base_url) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + +def _detect_file_type(filename: str) -> str: + """Infer fileType from extension.""" + ext = Path(filename).suffix.lower() + return {"md": "md", "markdown": "md", "json": "json", "jsonl": "json", "toon": "json"}.get( + ext.lstrip("."), + "txt", + ) + + +# --------------------------------------------------------------------------- +# upload +# --------------------------------------------------------------------------- + + +@click.command() +@click.argument("paths", nargs=-1) +@click.option( + "--type", + "file_type", + type=click.Choice(["md", "json", "txt"]), + default=None, + help="Force file type (auto-detected from extension by default).", +) +@_api_key_option +@_base_url_option +def upload(paths, file_type, api_key, base_url): + """Upload trace files to Kayba. + + PATHS can be files, directories, or '-' for stdin. + Directories are walked recursively. + """ + client = _client(api_key, base_url) + traces = _collect_upload_traces(paths, file_type) + + if not traces: + raise click.ClickException("No traces to upload.") + + _warn_large_trace_batch(traces) + + try: + result = client.upload_traces(traces) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + count = result.get("count", len(result.get("traces", []))) + click.echo(f"Uploaded {count} trace(s).") + for t in result.get("traces", []): + click.echo(f" {t['id']} {t['filename']}") + + +def _add_file(traces: list, path: Path, forced_type: Optional[str]): + content = path.read_text(encoding="utf-8", errors="replace") + if len(content) > MAX_TRACE_CHARS: + click.echo( + f"Skipping {path.name}: {len(content)} chars exceeds the Kayba API " + f"limit of {MAX_TRACE_CHARS}. Split or trim the trace and re-upload.", + err=True, + ) + return False + ft = forced_type or _detect_file_type(path.name) + traces.append({"filename": path.name, "content": content, "fileType": ft}) + return True + + +def _collect_upload_traces( + paths: tuple[str, ...], + forced_type: Optional[str], +) -> list[dict[str, str]]: + """Collect uploadable traces from files, directories, or stdin.""" + traces: list[dict[str, str]] = [] + items = list(paths) if paths else ["-"] + + for item in items: + if item == "-": + content = sys.stdin.read() + if len(content) > MAX_TRACE_CHARS: + click.echo( + f"Skipping stdin.txt: {len(content)} chars exceeds the Kayba API " + f"limit of {MAX_TRACE_CHARS}. Split or trim the trace and re-upload.", + err=True, + ) + continue + ft = forced_type or "txt" + traces.append({"filename": "stdin.txt", "content": content, "fileType": ft}) + continue + + p = Path(item) + if p.is_dir(): + for child in sorted(p.rglob("*")): + if child.is_file(): + _add_file(traces, child, forced_type) + elif p.is_file(): + _add_file(traces, p, forced_type) + else: + click.echo(f"Warning: skipping {item} (not found)", err=True) + + return traces + + +def _warn_large_trace_batch(traces: list[dict[str, str]]) -> None: + """Warn once when a batch contains very large trace files.""" + oversized = sum(1 for trace in traces if len(trace["content"]) > MAX_TRACE_CHARS) + if oversized: + click.echo( + "Warning: " + f"{oversized} trace(s) exceed {MAX_TRACE_CHARS} chars; the CLI will chunk uploads " + "into smaller requests, but very large individual files may still be " + "rejected.", + err=True, + ) + + +# --------------------------------------------------------------------------- +# traces +# --------------------------------------------------------------------------- + + +@click.group() +def traces(): + """List, view, upload, and delete traces.""" + pass + + +@traces.command("list") +@click.option("--json", "as_json", is_flag=True, help="Output raw JSON.") +@_api_key_option +@_base_url_option +def traces_list(as_json, api_key, base_url): + """List uploaded traces.""" + client = _client(api_key, base_url) + try: + result = client.list_traces() + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + items = result.get("traces", []) + + if as_json: + click.echo(json.dumps(items, indent=2)) + return + + if not items: + click.echo(_no_traces_message()) + return + + # Table header + click.echo( + f" {'ID':<36} {'Filename':<40} {'Type':<6} {'Size':>8} {'Uploaded'}" + ) + click.echo(f" {'-' * 36} {'-' * 40} {'-' * 6} {'-' * 8} {'-' * 10}") + for t in items: + tid = t.get("id", "?") + fname = t.get("filename", "?") + ftype = t.get("fileType", t.get("type", "?")) + size = _format_size(t.get("size", 0)) + age = _format_age(t.get("uploadedAt", "")) + click.echo(f" {tid:<36} {fname:<40} {ftype:<6} {size:>8} {age}") + + click.echo(f"\n {len(items)} trace(s)") + + +@traces.command("show") +@click.argument("trace_id") +@click.option("--json", "as_json", is_flag=True, help="Output raw JSON.") +@click.option("--meta", is_flag=True, help="Show only metadata, no content.") +@_api_key_option +@_base_url_option +def traces_show(trace_id, as_json, meta, api_key, base_url): + """View a trace.""" + client = _client(api_key, base_url) + try: + result = client.get_trace(trace_id) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + if as_json: + click.echo(json.dumps(result, indent=2)) + return + + # Metadata header + click.echo(f"ID: {result.get('id', '?')}") + click.echo(f"Filename: {result.get('filename', '?')}") + click.echo(f"Type: {result.get('fileType', result.get('type', '?'))}") + click.echo(f"Size: {_format_size(result.get('size', 0))}") + click.echo(f"Uploaded: {result.get('uploadedAt', '?')}") + + if not meta: + content = result.get("content", "") + if content: + click.echo(f"\n{'─' * 60}") + click.echo(content) + + +@traces.command("delete") +@click.argument("trace_ids", nargs=-1, required=True) +@click.option("--force", is_flag=True, help="Skip confirmation prompt.") +@_api_key_option +@_base_url_option +def traces_delete(trace_ids, force, api_key, base_url): + """Delete one or more traces.""" + if not force and sys.stdin.isatty(): + count = len(trace_ids) + if not click.confirm(f"Delete {count} trace(s)?"): + click.echo("Aborted.") + return + + client = _client(api_key, base_url) + try: + result = client.delete_traces(list(trace_ids)) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + deleted = result.get("deleted", []) + errors = result.get("errors", []) + + for tid in deleted: + click.echo(f" Deleted {tid}") + for err in errors: + click.echo(f" Error deleting {err['id']}: {err['error']}", err=True) + + if errors: + raise click.ClickException(f"{len(errors)} deletion(s) failed.") + + +@traces.command("upload") +@click.argument("paths", nargs=-1) +@click.option( + "--type", + "file_type", + type=click.Choice(["md", "json", "txt"]), + default=None, + help="Force file type (auto-detected from extension by default).", +) +@_api_key_option +@_base_url_option +def traces_upload(paths, file_type, api_key, base_url): + """Upload trace files to Kayba. + + PATHS can be files, directories, or '-' for stdin. + Directories are walked recursively. + """ + client = _client(api_key, base_url) + trace_list = _collect_upload_traces(paths, file_type) + + if not trace_list: + raise click.ClickException("No traces to upload.") + + _warn_large_trace_batch(trace_list) + + try: + result = client.upload_traces(trace_list) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + count = result.get("count", len(result.get("traces", []))) + click.echo(f"Uploaded {count} trace(s).") + for t in result.get("traces", []): + click.echo(f" {t['id']} {t['filename']}") + + +# --------------------------------------------------------------------------- +# run +# --------------------------------------------------------------------------- + + +@click.command() +@click.option("--traces", "trace_ids", multiple=True, help="Trace IDs to analyze.") +@click.option("--all", "select_all", is_flag=True, help="Select all traces.") +@click.option( + "--model", + type=click.Choice(["claude-sonnet-4-6", "claude-opus-4-6"]), + default=None, + help="Model to use for analysis.", +) +@click.option("--epochs", type=int, default=None, help="Analysis epochs (default 1).") +@click.option( + "--reflector-mode", + type=click.Choice(["recursive", "standard"]), + default=None, + help="Reflector mode.", +) +@click.option( + "--anthropic-key", + envvar="ANTHROPIC_API_KEY", + default=None, + help="Anthropic API key (or set ANTHROPIC_API_KEY).", +) +@click.option("--wait", is_flag=True, help="Poll until the job completes.") +@click.option("--json", "as_json", is_flag=True, help="Output raw JSON.") +@_api_key_option +@_base_url_option +def run( + trace_ids, + select_all, + model, + epochs, + reflector_mode, + anthropic_key, + wait, + as_json, + api_key, + base_url, +): + """Run the analysis pipeline on selected traces. + + Interactive mode: shows a visual trace selector. + Programmatic mode: use --traces ID or --all. + """ + client = _client(api_key, base_url) + + # Fetch available traces + try: + result = client.list_traces() + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + available = result.get("traces", []) + + if not available: + raise click.ClickException(_no_traces_message()) + + if select_all: + selected_ids = [t["id"] for t in available] + elif trace_ids: + selected_ids = list(trace_ids) + elif sys.stdin.isatty() and not as_json: + # Interactive mode: visual checkbox selector + selected_ids = _interactive_trace_select(available) + if not selected_ids: + click.echo("No traces selected.") + return + else: + raise click.ClickException( + "Provide --traces ID, --all, or run interactively (TTY)." + ) + + # Start the pipeline + try: + result = client.generate_insights( + trace_ids=selected_ids, + model=model, + epochs=epochs, + reflector_mode=reflector_mode, + anthropic_key=anthropic_key, + ) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + job_id = result["jobId"] + + if as_json: + click.echo(json.dumps({"jobId": job_id, "traces": len(selected_ids)})) + else: + click.echo(f"Job started: {job_id} ({len(selected_ids)} traces)") + + if wait: + _poll_job(client, job_id) + + +# --------------------------------------------------------------------------- +# insights +# --------------------------------------------------------------------------- + + +@click.group() +def insights(): + """Generate, list, and triage insights.""" + pass + + +@insights.command("generate") +@click.option("--traces", "trace_ids", multiple=True, help="Trace IDs to analyze.") +@click.option( + "--model", + type=click.Choice(["claude-sonnet-4-6", "claude-opus-4-6"]), + default=None, + help="Model to use for analysis.", +) +@click.option("--epochs", type=int, default=None, help="Analysis epochs (default 1).") +@click.option( + "--reflector-mode", + type=click.Choice(["recursive", "standard"]), + default=None, + help="Reflector mode.", +) +@click.option( + "--anthropic-key", + envvar="ANTHROPIC_API_KEY", + default=None, + help="Anthropic API key (or set ANTHROPIC_API_KEY).", +) +@click.option("--wait", is_flag=True, help="Poll until the job completes.") +@_api_key_option +@_base_url_option +def insights_generate( + trace_ids, model, epochs, reflector_mode, anthropic_key, wait, api_key, base_url +): + """Trigger insight generation from uploaded traces.""" + client = _client(api_key, base_url) + try: + result = client.generate_insights( + trace_ids=list(trace_ids) or None, + model=model, + epochs=epochs, + reflector_mode=reflector_mode, + anthropic_key=anthropic_key, + ) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + job_id = result["jobId"] + click.echo(f"Job started: {job_id}") + + if wait: + _poll_job(client, job_id) + + +@insights.command("list") +@click.option( + "--status", + type=click.Choice(["pending", "new", "accepted", "rejected"]), + default=None, + help="Filter by review status.", +) +@click.option("--section", default=None, help="Filter by skillbook section.") +@click.option("--json", "as_json", is_flag=True, help="Output raw JSON.") +@_api_key_option +@_base_url_option +def insights_list(status, section, as_json, api_key, base_url): + """List insights.""" + client = _client(api_key, base_url) + try: + result = client.list_insights(status=status, section=section) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + items = result.get("insights", []) + if as_json: + click.echo(json.dumps(items, indent=2)) + return + + if not items: + click.echo("No insights found.") + return + + for ins in items: + status_str = ins.get("status", "?") + click.echo(f" [{status_str:>8}] {ins['id']} {ins.get('section', '')}") + click.echo(f" {ins.get('content', '')[:120]}") + + +@insights.command("triage") +@click.option("--accept", "accept_ids", multiple=True, help="Insight IDs to accept.") +@click.option("--reject", "reject_ids", multiple=True, help="Insight IDs to reject.") +@click.option("--accept-all", is_flag=True, help="Accept all pending insights.") +@click.option("--note", default=None, help="Optional triage note.") +@_api_key_option +@_base_url_option +def insights_triage(accept_ids, reject_ids, accept_all, note, api_key, base_url): + """Accept or reject insights.""" + client = _client(api_key, base_url) + + if accept_all: + try: + result = client.list_insights(status="pending") + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + accept_ids = tuple(ins["id"] for ins in result.get("insights", [])) + if not accept_ids: + click.echo("No pending insights to accept.") + return + + if not accept_ids and not reject_ids: + raise click.ClickException("Provide --accept, --reject, or --accept-all.") + + errors = [] + for iid in accept_ids: + try: + client.triage_insight(iid, "accepted", note=note) + click.echo(f" Accepted {iid}") + except KaybaAPIError as exc: + errors.append(str(exc)) + click.echo(f" Error accepting {iid}: {exc}", err=True) + + for iid in reject_ids: + try: + client.triage_insight(iid, "rejected", note=note) + click.echo(f" Rejected {iid}") + except KaybaAPIError as exc: + errors.append(str(exc)) + click.echo(f" Error rejecting {iid}: {exc}", err=True) + + if errors: + raise click.ClickException(f"{len(errors)} triage operation(s) failed.") + + +# --------------------------------------------------------------------------- +# prompts +# --------------------------------------------------------------------------- + + +@click.group() +def prompts(): + """Generate, list, and pull prompts.""" + pass + + +@prompts.command("generate") +@click.option( + "--insights", "insight_ids", multiple=True, help="Insight IDs to include." +) +@click.option("--label", default=None, help="Label for the generated prompt.") +@click.option("-o", "--output", "output_path", default=None, help="Save to file.") +@_api_key_option +@_base_url_option +def prompts_generate(insight_ids, label, output_path, api_key, base_url): + """Generate a prompt from accepted insights.""" + client = _client(api_key, base_url) + try: + result = client.generate_prompt( + insight_ids=list(insight_ids) or None, + label=label, + ) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + prompt_id = result.get("promptId", "?") + version = result.get("version", "?") + text = result.get("content", {}).get("text", "") + + click.echo(f"Prompt {prompt_id} (v{version}) generated.") + + if output_path: + Path(output_path).write_text(text, encoding="utf-8") + click.echo(f"Saved to {output_path}") + else: + click.echo(text) + + +@prompts.command("list") +@_api_key_option +@_base_url_option +def prompts_list(api_key, base_url): + """List prompt versions.""" + client = _client(api_key, base_url) + try: + result = client.list_prompts() + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + items = result if isinstance(result, list) else result.get("prompts", []) + if not items: + click.echo("No prompts found.") + return + + for p in items: + pid = p.get("id", p.get("promptId", "?")) + label = p.get("label", "") + click.echo(f" {pid} {label}") + + +@prompts.command("pull") +@click.option("--id", "prompt_id", default=None, help="Prompt ID (default: latest).") +@click.option("-o", "--output", "output_path", default=None, help="Save to file.") +@click.option("--pretty", is_flag=True, help="Pretty-print JSON output.") +@_api_key_option +@_base_url_option +def prompts_pull(prompt_id, output_path, pretty, api_key, base_url): + """Download a prompt.""" + client = _client(api_key, base_url) + + if prompt_id: + try: + result = client.get_prompt(prompt_id) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + else: + # Get latest by listing and picking first + try: + listing = client.list_prompts() + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + items = listing if isinstance(listing, list) else listing.get("prompts", []) + if not items: + raise click.ClickException("No prompts available.") + first = items[0] + pid = first.get("id", first.get("promptId")) + try: + result = client.get_prompt(pid) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + text = result.get("content", {}).get("text", "") + + if pretty: + output = json.dumps(result, indent=2) + else: + output = text + + if output_path: + Path(output_path).write_text(output, encoding="utf-8") + click.echo(f"Saved to {output_path}") + else: + click.echo(output) + + +@prompts.command("install") +@click.option("--id", "prompt_id", default=None, help="Prompt ID (default: latest).") +@click.option( + "-i", + "--input", + "input_path", + type=click.Path(exists=True, dir_okay=False), + default=None, + help="Read prompt text from a local file instead of the API.", +) +@click.option( + "--target", + type=click.Choice(sorted(PROMPT_INSTALL_TARGETS)), + default="universal", + show_default=True, + help="Agent to install the prompt for.", +) +@click.option( + "--file", + "target_path", + type=click.Path(dir_okay=False), + default=None, + help="Override the destination file path.", +) +@_api_key_option +@_base_url_option +def prompts_install(input_path, prompt_id, target, target_path, api_key, base_url): + """Install a generated prompt into an agent instruction file.""" + if input_path and prompt_id: + raise click.ClickException("Use either --input or --id, not both.") + + if input_path: + prompt_ref = input_path + text = Path(input_path).read_text(encoding="utf-8") + else: + client = _client(api_key, base_url) + prompt_ref, text = _fetch_prompt_text(client, prompt_id) + + if not text.strip(): + raise click.ClickException("Prompt content is empty.") + + default_filename, target_label = PROMPT_INSTALL_TARGETS[target] + destination = Path(target_path) if target_path else Path(default_filename) + _upsert_prompt_block(destination, _build_prompt_block(text)) + click.echo(f"Installed Kayba prompt ({prompt_ref}) into {destination}") + click.echo( + f"Target: {target_label}. Start a new agent session so it reloads the file." + ) + + +# --------------------------------------------------------------------------- +# status +# --------------------------------------------------------------------------- + + +@click.command() +@click.argument("job_id") +@click.option("--wait", is_flag=True, help="Poll until the job completes.") +@click.option( + "--interval", type=int, default=5, help="Poll interval in seconds (default 5)." +) +@_api_key_option +@_base_url_option +def status(job_id, wait, interval, api_key, base_url): + """Check the status of an analysis job.""" + client = _client(api_key, base_url) + + if wait: + _poll_job(client, job_id, interval=interval) + else: + try: + job = client.get_job(job_id) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + _print_job(job) + + +# --------------------------------------------------------------------------- +# materialize +# --------------------------------------------------------------------------- + + +@click.command() +@click.argument("job_id") +@_api_key_option +@_base_url_option +def materialize(job_id, api_key, base_url): + """Materialize completed job results into the skillbook.""" + client = _client(api_key, base_url) + try: + result = client.materialize_job(job_id) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + click.echo( + f"Materialized {result.get('skillsGenerated', '?')} skill(s) " + f"from job {result.get('jobId', job_id)}." + ) + + +# --------------------------------------------------------------------------- +# batch +# --------------------------------------------------------------------------- + +DEFAULT_BATCH_PROMPT = """\ +You are a trace classification system. Analyze the trace metadata below and group +them into coherent batches for analysis by a Recursive Reflector. + +Constraints: +{constraints} + +Instructions: +1. Group traces by semantic similarity (similar tasks, tools, domains). +2. Respect min/max batch size constraints. +3. Every trace must be assigned to exactly one batch. +4. Use descriptive batch names (lowercase-with-hyphens). + +Output only valid JSON matching this schema: +{{"batches": {{"name": {{"description": "...", "trace_files": [...]}}}}, "summary": {{"total_traces": N, "num_batches": N, "batch_sizes": {{"name": N}}}}}} + +Trace metadata: +{traces_json} +""" + + +def _extract_trace_metadata(filename: str, content: str, file_type: str) -> dict: + """Extract compact metadata from a trace for prompt context.""" + meta: dict = { + "filename": filename, + "type": file_type, + "size": len(content), + } + + if file_type == "json": + try: + data = json.loads(content) + if isinstance(data, dict): + if "task_id" in data: + meta["task_id"] = data["task_id"] + if "user_request" in data: + meta["user_request"] = str(data["user_request"])[:200] + if "tools" in data: + meta["tools"] = data["tools"] + steps = data.get("steps") or data.get("events") or [] + if isinstance(steps, list): + meta["step_count"] = len(steps) + except (json.JSONDecodeError, TypeError): + pass + elif file_type == "md": + lines = content.split("\n") + meta["summary"] = "\n".join(lines[:10]) + headings = [ln for ln in lines if ln.startswith("#")] + if headings: + meta["headings"] = headings[:20] + else: + lines = content.split("\n") + meta["summary"] = "\n".join(lines[:5]) + + return meta + + +def _build_classification_prompt( + traces_metadata: list[dict], + constraints: str, + custom_prompt: Optional[str] = None, +) -> str: + """Build the classification prompt with metadata and constraints.""" + traces_json = json.dumps(traces_metadata, indent=2) + template = custom_prompt if custom_prompt else DEFAULT_BATCH_PROMPT + return template.format(traces_json=traces_json, constraints=constraints) + + +def _validate_batch_plan( + plan: dict, + all_filenames: list[str], + min_size: int, + max_size: int, +) -> list[str]: + """Validate a batch plan. Returns list of error strings (empty = valid).""" + errors: list[str] = [] + + batches = plan.get("batches") + if not isinstance(batches, dict): + errors.append("Missing or invalid 'batches' key (expected dict).") + return errors + + assigned: set[str] = set() + for name, batch in batches.items(): + files = batch.get("trace_files", []) + if not isinstance(files, list): + errors.append(f"Batch '{name}': trace_files must be a list.") + continue + + if len(files) < min_size: + errors.append(f"Batch '{name}' has {len(files)} traces (min {min_size}).") + if len(files) > max_size: + errors.append(f"Batch '{name}' has {len(files)} traces (max {max_size}).") + for f in files: + if f in assigned: + errors.append(f"Trace '{f}' assigned to multiple batches.") + assigned.add(f) + + missing = set(all_filenames) - assigned + if missing: + errors.append(f"Traces not assigned: {sorted(missing)}") + + extra = assigned - set(all_filenames) + if extra: + errors.append(f"Unknown traces in plan: {sorted(extra)}") + + return errors + + +def _upload_batches( + plan: dict, + traces_by_name: dict[str, dict[str, str]], + client: KaybaClient, +) -> None: + """Upload each batch to the Kayba API.""" + batches = plan.get("batches", {}) + for name, batch in batches.items(): + files = batch.get("trace_files", []) + batch_traces = [traces_by_name[f] for f in files if f in traces_by_name] + if not batch_traces: + click.echo(f" Skipping empty batch '{name}'.", err=True) + continue + try: + _warn_large_trace_batch(batch_traces) + result = client.upload_traces(batch_traces) + count = result.get("count", len(batch_traces)) + click.echo(f" Uploaded batch '{name}': {count} trace(s).") + except KaybaAPIError as exc: + click.echo(f" Error uploading batch '{name}': {exc}", err=True) + + +@click.command() +@click.argument("paths", nargs=-1) +@click.option( + "--prompt", + "prompt_file", + type=click.Path(exists=True), + default=None, + help="Custom classification prompt file (should contain {traces_json} and {constraints}).", +) +@click.option( + "-o", + "--output", + "output_file", + default="batches.json", + show_default=True, + help="Output batch plan file.", +) +@click.option( + "--apply", + "apply_file", + type=click.Path(exists=True), + default=None, + help="Apply an existing batch plan (skip prompt generation).", +) +@click.option( + "--upload", + "do_upload", + is_flag=True, + help="Upload each batch to the API (requires --apply).", +) +@click.option("--max-batch-size", type=int, default=30, show_default=True) +@click.option("--min-batch-size", type=int, default=10, show_default=True) +@_api_key_option +@_base_url_option +def batch( + paths, + prompt_file, + output_file, + apply_file, + do_upload, + max_batch_size, + min_batch_size, + api_key, + base_url, +): + """Pre-batch traces for the Recursive Reflector. + + Two modes: + + Prepare (default): collect traces, extract metadata, print a classification + prompt to stdout for Claude Code to process. + + Apply (--apply FILE): validate a batch plan JSON and optionally upload. + + PATHS can be files or directories (walked recursively). + """ + if not paths: + raise click.ClickException("Provide at least one path.") + + # ---- Collect traces ---- + traces: list[dict[str, str]] = [] + for item in paths: + p = Path(item) + if p.is_dir(): + for child in sorted(p.rglob("*")): + if child.is_file(): + _add_file(traces, child, None) + elif p.is_file(): + _add_file(traces, p, None) + else: + click.echo(f"Warning: skipping {item} (not found)", err=True) + + if not traces: + raise click.ClickException("No trace files found.") + + all_filenames = [t["filename"] for t in traces] + + # ---- Mode 2: Apply ---- + if apply_file: + plan_text = Path(apply_file).read_text(encoding="utf-8") + try: + plan = json.loads(plan_text) + except json.JSONDecodeError as exc: + raise click.ClickException(f"Invalid JSON in {apply_file}: {exc}") + + errors = _validate_batch_plan( + plan, all_filenames, min_batch_size, max_batch_size + ) + if errors: + for err in errors: + click.echo(f" Error: {err}", err=True) + raise click.ClickException("Batch plan validation failed.") + + num_batches = len(plan.get("batches", {})) + click.echo( + f"Batch plan valid: {num_batches} batch(es), {len(traces)} trace(s)." + ) + + if do_upload: + client = _client(api_key, base_url) + traces_by_name = {t["filename"]: t for t in traces} + _upload_batches(plan, traces_by_name, client) + click.echo("Upload complete.") + return + + # ---- Mode 1: Prepare ---- + if do_upload: + raise click.ClickException("--upload requires --apply.") + + metadata = [ + _extract_trace_metadata(t["filename"], t["content"], t["fileType"]) + for t in traces + ] + + constraints = f"min_batch_size={min_batch_size}, max_batch_size={max_batch_size}" + custom_prompt = None + if prompt_file: + custom_prompt = Path(prompt_file).read_text(encoding="utf-8") + prompt_text = _build_classification_prompt(metadata, constraints, custom_prompt) + + # Write metadata to output file as starting point + starter = { + "batches": {}, + "summary": {"total_traces": len(traces), "num_batches": 0, "batch_sizes": {}}, + } + out_path = Path(output_file) + out_path.write_text(json.dumps(starter, indent=2), encoding="utf-8") + click.echo(f"Wrote metadata to {out_path}", err=True) + click.echo(f"Found {len(traces)} trace(s).", err=True) + + # Print prompt to stdout for Claude Code + click.echo(prompt_text) + + +# --------------------------------------------------------------------------- +# integrations +# --------------------------------------------------------------------------- + + +def _mask_token(token: str) -> str: + """Mask a token/key for display, showing first 4 + last 4 chars.""" + if not token or len(token) <= 8: + return "****" + return f"{token[:4]}...{token[-4:]}" + + +@click.group() +def integrations(): + """Manage platform integrations (MLflow, LangSmith).""" + pass + + +@integrations.command("list") +@click.option("--json", "as_json", is_flag=True, help="Output raw JSON.") +@_api_key_option +@_base_url_option +def integrations_list(as_json, api_key, base_url): + """Show configured integrations.""" + client = _client(api_key, base_url) + try: + result = client.get_integrations() + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + if as_json: + click.echo(json.dumps(result, indent=2)) + return + + for name in ("mlflow", "langsmith"): + config = result.get(name, {}) + enabled = config.get("enabled", False) + status_str = "enabled" if enabled else "disabled" + + click.echo(f"\n {name}") + click.echo(f" Status: {status_str}") + + if name == "mlflow": + uri = config.get("trackingUri", "") + auth = config.get("authType", "none") + experiment = config.get("experimentName", "") + if uri: + click.echo(f" Tracking URI: {uri}") + click.echo(f" Auth type: {auth}") + if config.get("token"): + click.echo(f" Token: {_mask_token(config['token'])}") + if config.get("username"): + click.echo(f" Username: {config['username']}") + if experiment: + click.echo(f" Experiment: {experiment}") + elif name == "langsmith": + api_url = config.get("apiUrl", "") + project = config.get("projectName", "") + if api_url: + click.echo(f" API URL: {api_url}") + if config.get("apiKey"): + click.echo(f" API key: {_mask_token(config['apiKey'])}") + if project: + click.echo(f" Project: {project}") + + click.echo() + + +@integrations.command("configure") +@click.argument("name", type=click.Choice(["mlflow", "langsmith"])) +@_api_key_option +@_base_url_option +def integrations_configure(name, api_key, base_url): + """Interactively configure an integration.""" + client = _client(api_key, base_url) + + # Fetch current config for defaults + try: + current = client.get_integrations() + except KaybaAPIError: + current = {} + + existing = current.get(name, {}) + + if name == "mlflow": + tracking_uri = click.prompt( + "MLflow tracking URI", + default=existing.get("trackingUri", ""), + ) + auth_type = click.prompt( + "Auth type", + type=click.Choice(["none", "basic", "bearer", "databricks"]), + default=existing.get("authType", "none"), + ) + + token = "" + username = "" + if auth_type in ("basic", "bearer", "databricks"): + token = click.prompt( + "Token / password", + default="", + hide_input=True, + show_default=False, + ) + if auth_type == "basic": + username = click.prompt( + "Username", + default=existing.get("username", ""), + ) + + experiment_name = click.prompt( + "Experiment name (optional filter)", + default=existing.get("experimentName", ""), + ) + + config = { + "enabled": True, + "trackingUri": tracking_uri, + "authType": auth_type, + "token": token, + "username": username, + "experimentName": experiment_name, + } + + elif name == "langsmith": + api_url = click.prompt( + "LangSmith API URL", + default=existing.get("apiUrl", "https://api.smith.langchain.com"), + ) + langsmith_key = click.prompt( + "LangSmith API key", + default="", + hide_input=True, + show_default=False, + ) + project_name = click.prompt( + "Project name (optional filter)", + default=existing.get("projectName", ""), + ) + + config = { + "enabled": True, + "apiUrl": api_url, + "apiKey": langsmith_key, + "projectName": project_name, + } + + try: + client.update_integration(name, config) + click.echo(f"\n {name} configuration saved.") + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + # Auto-test the connection + click.echo(f" Testing {name} connection...") + try: + test_result = client.test_integration(name) + if test_result.get("connected"): + click.echo(f" Connected successfully.") + if name == "mlflow" and test_result.get("mlflowVersion"): + click.echo(f" MLflow version: {test_result['mlflowVersion']}") + else: + click.echo(f" Warning: connection test returned unexpected result.") + except KaybaAPIError as exc: + click.echo(f" Warning: connection test failed: {exc}", err=True) + + +@integrations.command("test") +@click.argument("name", type=click.Choice(["mlflow", "langsmith"])) +@_api_key_option +@_base_url_option +def integrations_test(name, api_key, base_url): + """Test an integration connection.""" + client = _client(api_key, base_url) + try: + result = client.test_integration(name) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + if result.get("connected"): + click.echo(f" {name}: connected") + if name == "mlflow" and result.get("mlflowVersion"): + click.echo(f" MLflow version: {result['mlflowVersion']}") + if name == "mlflow" and result.get("experimentCount") is not None: + click.echo(f" Experiments found: {result['experimentCount']}") + else: + error = result.get("error", "Unknown error") + raise click.ClickException(f"{name}: connection failed — {error}") + + +# --------------------------------------------------------------------------- +# setup +# --------------------------------------------------------------------------- + + +@click.command() +@click.option( + "--append-to", + type=click.Path(), + default=None, + help="Append instructions to this file (default: print to stdout). " + "Recommended: AGENTS.md (universal), CLAUDE.md, .cursorrules.", +) +@click.option( + "--skills/--no-skills", + default=True, + help="Install Claude Code skills (default: enabled).", +) +@click.option( + "--project-dir", + type=click.Path(exists=True, file_okay=False), + default=".", + help="Project root directory (default: current directory).", +) +def setup(append_to, skills, project_dir): + """Print or install Kayba CLI instructions and skills for coding agents.""" + snippet = ( + importlib.resources.files("ace.cli.commands") + .joinpath("kayba-agent-instructions.md") + .read_text(encoding="utf-8") + ) + + if append_to: + path = Path(append_to) + mode = "a" if path.exists() else "w" + with path.open(mode, encoding="utf-8") as f: + if mode == "a": + f.write("\n\n") + f.write(snippet) + click.echo(f"Appended Kayba CLI instructions to {path}") + else: + click.echo(snippet) + + if skills: + target = Path(project_dir) / ".claude" / "skills" + _install_skills(target) + + +def _install_skills(target_dir: Path) -> None: + """Copy bundled skill files to the target .claude/skills/ directory.""" + skills_pkg = importlib.resources.files("ace.cli.skills") + installed = [] + + for skill_dir in skills_pkg.iterdir(): + if skill_dir.name.startswith("_") or not skill_dir.is_dir(): + continue + + dest = target_dir / skill_dir.name + dest.mkdir(parents=True, exist_ok=True) + + # Copy top-level SKILL.md + skill_file = skill_dir / "SKILL.md" + if skill_file.is_file(): + (dest / "SKILL.md").write_bytes(skill_file.read_bytes()) + + # Copy stage subdirectories + for sub in skill_dir.iterdir(): + if sub.is_dir() and not sub.name.startswith("_"): + sub_dest = dest / sub.name + sub_dest.mkdir(parents=True, exist_ok=True) + sub_skill = sub / "SKILL.md" + if sub_skill.is_file(): + (sub_dest / "SKILL.md").write_bytes(sub_skill.read_bytes()) + + stages = [d.name for d in dest.iterdir() if d.is_dir()] + installed.append((skill_dir.name, len(stages))) + + if installed: + click.echo(f"\nInstalled skills to {target_dir}/:") + for name, stage_count in installed: + click.echo(f" {name} ({stage_count} stages)") + else: + click.echo("\nNo skills found to install.") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _interactive_trace_select(traces: list[dict]) -> list[str]: + """Visual checkbox selector for traces.""" + try: + import questionary + except ImportError: + raise click.ClickException( + "Interactive mode requires 'questionary'. " + "Install with: pip install 'ace-framework[cloud]'" + ) + + choices = [] + for t in traces: + size = _format_size(t.get("size", 0)) + age = _format_age(t.get("uploadedAt", "")) + label = f"{t['filename']:<40} {size:>8} {age}" + choices.append(questionary.Choice(title=label, value=t["id"])) + + selected = questionary.checkbox( + "Select traces (space to toggle, a to toggle all, enter to confirm):", + choices=choices, + ).ask() + + if selected is None: # Ctrl-C + return [] + return selected + + +def _no_traces_message() -> str: + """Explain why a new hosted account often has no traces yet.""" + return ( + "No traces found in your Kayba account.\n" + "Kayba does not auto-import local agent transcripts yet.\n" + "If you're using Claude Code, upload its local .jsonl files first, for example:\n" + " kayba traces upload ~/.claude/projects/<project>/*.jsonl\n" + "Docs: https://kayba.ai/docs/integrations/hosted-api/#where-do-traces-come-from" + ) + + +def _fetch_prompt_text(client: KaybaClient, prompt_id: Optional[str]) -> tuple[str, str]: + """Load a prompt body from the API.""" + if prompt_id: + result = client.get_prompt(prompt_id) + prompt_ref = prompt_id + else: + listing = client.list_prompts() + items = listing if isinstance(listing, list) else listing.get("prompts", []) + if not items: + raise click.ClickException( + "No prompts available. Generate one first with `kayba prompts generate`." + ) + first = items[0] + prompt_ref = str(first.get("id", first.get("promptId", "latest"))) + result = client.get_prompt(prompt_ref) + + text = result.get("content", {}).get("text", "") + if not text.strip(): + raise click.ClickException(f"Prompt {prompt_ref} is empty.") + return prompt_ref, text + + +def _build_prompt_block(prompt_text: str) -> str: + """Wrap prompt text in a managed block so repeated installs replace cleanly.""" + body = prompt_text.strip() + return ( + f"{PROMPT_BLOCK_START}\n" + "## Kayba Prompt\n" + "_Managed by `kayba prompts install`. Re-run the command to update this block._\n\n" + f"{body}\n" + f"{PROMPT_BLOCK_END}\n" + ) + + +def _upsert_prompt_block(path: Path, block: str) -> None: + """Replace an existing managed prompt block or append a new one.""" + existing = path.read_text(encoding="utf-8") if path.exists() else "" + pattern = re.compile( + rf"{re.escape(PROMPT_BLOCK_START)}.*?{re.escape(PROMPT_BLOCK_END)}\n?", + re.DOTALL, + ) + + if pattern.search(existing): + updated = pattern.sub(block, existing, count=1) + elif existing.strip(): + updated = existing.rstrip() + "\n\n" + block + else: + updated = block + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(updated, encoding="utf-8") + + +def _format_size(size_bytes: int) -> str: + """Format bytes as human-readable.""" + if size_bytes < 1024: + return f"{size_bytes} B" + elif size_bytes < 1024 * 1024: + return f"{size_bytes / 1024:.1f} KB" + else: + return f"{size_bytes / (1024 * 1024):.1f} MB" + + +def _format_age(iso_str: str) -> str: + """Format ISO datetime as relative time.""" + if not iso_str: + return "" + try: + dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00")) + now = datetime.now(timezone.utc) + diff = now - dt + seconds = diff.total_seconds() + if seconds < 60: + return "just now" + elif seconds < 3600: + return f"{int(seconds / 60)}m ago" + elif seconds < 86400: + return f"{int(seconds / 3600)}h ago" + else: + return f"{int(seconds / 86400)}d ago" + except (ValueError, TypeError): + return iso_str + + +def _poll_job(client: KaybaClient, job_id: str, *, interval: int = 5): + """Poll a job until it reaches a terminal state.""" + terminal = {"completed", "failed"} + while True: + try: + job = client.get_job(job_id) + except KaybaAPIError as exc: + raise click.ClickException(str(exc)) + + st = job.get("status", "unknown") + click.echo(f" {job_id} {st}") + + if st in terminal: + _print_job(job) + if st == "completed": + click.echo(f"\nRun: kayba materialize {job_id}") + return + + time.sleep(interval) + + +def _print_job(job: dict): + """Pretty-print a job status dict.""" + click.echo(f"Job: {job.get('jobId', '?')}") + click.echo(f"Status: {job.get('status', '?')}") + if job.get("startedAt"): + click.echo(f"Started: {job['startedAt']}") + if job.get("completedAt"): + click.echo(f"Completed: {job['completedAt']}") + if job.get("error"): + click.echo(f"Error: {job['error']}") + result = job.get("result") + if result: + click.echo(f"Skills generated: {result.get('skillsGenerated', '?')}") + if result.get("summary"): + click.echo(f"Summary: {result['summary']}") + click.echo(f"Materialized: {result.get('materialized', False)}") diff --git a/ace/cli/commands/__init__.py b/ace/cli/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ace/cli/commands/kayba-agent-instructions.md b/ace/cli/commands/kayba-agent-instructions.md new file mode 100644 index 0000000000000000000000000000000000000000..bf4cc2334bcd61b145f295a1032417b5abf10814 --- /dev/null +++ b/ace/cli/commands/kayba-agent-instructions.md @@ -0,0 +1,70 @@ +## Kayba CLI + +The `kayba` CLI interacts with the Kayba hosted API (https://use.kayba.ai). +Auth: set `KAYBA_API_KEY` env var or pass `--api-key` to every command. + +### Commands + +``` +kayba traces list [--json] List uploaded traces +kayba traces show <id> [--meta] [--json] View a trace +kayba traces upload <paths...> Upload trace files/dirs (or - for stdin) + --type [md|json|txt] Force file type (auto-detected by default) +kayba traces delete <ids...> [--force] Delete traces + +kayba run Run pipeline (interactive trace selector) + --traces ID --all --model MODEL --epochs N + --reflector-mode [recursive|standard] --anthropic-key KEY + --wait --json + +kayba insights generate Trigger insight generation + --traces ID --model MODEL --epochs N --reflector-mode [recursive|standard] + --anthropic-key KEY --wait + +kayba insights list List insights + --status [pending|new|accepted|rejected] --section NAME --json + +kayba insights triage Accept/reject insights + --accept ID --reject ID --accept-all --note TEXT + +kayba prompts generate Generate prompt from accepted insights + --insights ID --label NAME -o FILE + +kayba prompts list List prompt versions + +kayba prompts pull Download a prompt + --id ID -o FILE --pretty +kayba prompts install Install a generated prompt into an agent file + --target TARGET --file PATH --id ID --input FILE + +kayba status <job-id> Check job status + --wait --interval N + +kayba materialize <job-id> Materialize results into skillbook + +kayba integrations list [--json] Show configured integrations +kayba integrations configure <name> Configure mlflow or langsmith +kayba integrations test <name> Test integration connection + +kayba batch <paths...> Pre-batch traces for Recursive Reflector + --apply FILE --upload --min-batch-size N --max-batch-size N +``` + +### Typical workflow + +``` +kayba traces upload traces/ +kayba run --all --wait +kayba insights triage --accept-all +kayba prompts generate -o prompt.md +kayba prompts install --target claude-code +``` + +### Programmatic workflow (for agents) + +``` +kayba traces list --json +kayba run --traces ID1 --traces ID2 --json --wait +kayba insights triage --accept-all +kayba prompts generate -o prompt.md +``` diff --git a/ace/cli/setup.py b/ace/cli/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..5dba57591b01f0bbd961f1f91b5e31fc4de7cbb7 --- /dev/null +++ b/ace/cli/setup.py @@ -0,0 +1,522 @@ +"""``ace setup`` — interactive configuration wizard. + +Guides the user through: +1. Enter a model name (any LiteLLM model string) +2. Validate the connection — if it fails, prompt for keys +3. Optionally assign different models per ACE role +4. Save .env (secrets) and ace.toml (model config) +""" + +from __future__ import annotations + +import getpass +import os +import sys +from pathlib import Path + +from ..providers.config import ( + ACEModelConfig, + ModelConfig, + find_config, + load_config, + load_dotenv, + save_config, + save_env_var, +) +from ..providers.registry import ( + PROVIDER_KEY_ENV, + _PROVIDER_ALT_KEYS, + get_missing_keys, + get_provider, + search_models, + suggest_models, + validate_connection, +) + +# --------------------------------------------------------------------------- +# Terminal helpers +# --------------------------------------------------------------------------- + +_IS_TTY = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() + +BOLD = "\033[1m" if _IS_TTY else "" +DIM = "\033[2m" if _IS_TTY else "" +GREEN = "\033[32m" if _IS_TTY else "" +RED = "\033[31m" if _IS_TTY else "" +YELLOW = "\033[33m" if _IS_TTY else "" +CYAN = "\033[36m" if _IS_TTY else "" +RESET = "\033[0m" if _IS_TTY else "" + + +def _ok(msg: str) -> None: + print(f" {GREEN}\u2713{RESET} {msg}") + + +def _warn(msg: str) -> None: + print(f" {YELLOW}!{RESET} {msg}") + + +def _fail(msg: str) -> None: + print(f" {RED}\u2717{RESET} {msg}") + + +def _info(msg: str) -> None: + print(f" {DIM}{msg}{RESET}") + + +def _prompt(label: str, default: str = "") -> str: + suffix = f" [{default}]" if default else "" + try: + value = input(f" {label}{suffix}: ").strip() + except (EOFError, KeyboardInterrupt): + print() + sys.exit(1) + return value or default + + +def _prompt_secret(label: str) -> str: + try: + value = getpass.getpass(f" {label}: ").strip() + except (EOFError, KeyboardInterrupt): + print() + sys.exit(1) + return value + + +def _confirm(label: str, default: bool = True) -> bool: + suffix = "[Y/n]" if default else "[y/N]" + try: + value = input(f" {label} {suffix}: ").strip().lower() + except (EOFError, KeyboardInterrupt): + print() + sys.exit(1) + if not value: + return default + return value in ("y", "yes") + + +def _load_project_dotenv() -> None: + """Load .env from the project root (where ace.toml lives), not just CWD.""" + config_path = find_config() + if config_path is not None: + env_path = config_path.parent / ".env" + if env_path.exists(): + try: + from dotenv import load_dotenv as _load + + _load(env_path) + return + except ImportError: + pass + # Fallback: try CWD + load_dotenv() + + +# --------------------------------------------------------------------------- +# Model + key flow +# --------------------------------------------------------------------------- + + +def _detect_credential_source(provider: str) -> str | None: + """Return which credential env var is set for *provider*.""" + env_vars = PROVIDER_KEY_ENV.get(provider) + if env_vars is None: + candidates: list[str] = [] + elif isinstance(env_vars, str): + candidates = [env_vars] + else: + candidates = list(env_vars) + + # Include alternative auth (e.g. AWS_BEARER_TOKEN_BEDROCK) + alt = _PROVIDER_ALT_KEYS.get(provider) + if alt: + candidates.extend(alt) + + found = [v for v in candidates if os.environ.get(v)] + if not found: + return None + return ", ".join(found) + + +def _validate_and_prompt_keys( + model: str, + provider: str, + directory: Path, +) -> bool: + """Try to validate *model*. If auth fails, prompt for missing keys and retry. + + Returns True on success. On non-auth failures (model not found, etc.) + prints the error and returns False so the caller can re-prompt. + """ + # First: just try it — handles bearer tokens, ~/.aws/credentials, etc. + print(f" Validating...", end="", flush=True) + result = validate_connection(model) + + if result.success: + print( + f"\r {GREEN}\u2713{RESET} Connected! " + f"({model} via {result.provider}, {result.latency_ms}ms)" + ) + # Show which credential was used + cred_source = _detect_credential_source(result.provider or provider) + if cred_source: + _info(f"Using {cred_source}") + return True + + # Model not found — not recoverable by adding keys + if "not found" in result.error.lower(): + print(f"\r {RED}\u2717{RESET} {result.error} ") + suggestions = suggest_models(model) + if suggestions: + _info("Did you mean one of these?") + for s in suggestions: + _info(f" - {s}") + return False + + # Everything else (auth, connection, bad request, etc.) — offer to + # prompt for credentials since missing/wrong keys are the most common cause. + print(f"\r {YELLOW}!{RESET} {result.error} ") + _info(f"This may be a credentials issue for {provider}.") + + # Prefer our own mapping over LiteLLM's generic response, since + # LiteLLM often returns wrong keys (e.g. bedrock_converse gets + # generic bedrock keys instead of the bearer token alternative). + our_keys = PROVIDER_KEY_ENV.get(provider) + if our_keys is not None: + missing = [our_keys] if isinstance(our_keys, str) else list(our_keys) + else: + missing = get_missing_keys(model) + if not missing: + missing = [f"{provider.upper()}_API_KEY"] + + # Env vars that are not secrets — prompt with visible input + _NON_SECRET_VARS = {"AWS_REGION_NAME", "GOOGLE_APPLICATION_CREDENTIALS"} + + provided_keys: dict[str, str] = {} + for env_var in missing: + if env_var in _NON_SECRET_VARS: + value = _prompt(env_var) + else: + value = _prompt_secret(f"{env_var}") + if value: + provided_keys[env_var] = value + os.environ[env_var] = value + + if not provided_keys: + _fail("No credentials provided.") + return False + + # Retry validation + print(f" Validating...", end="", flush=True) + result = validate_connection(model) + + if result.success: + print( + f"\r {GREEN}\u2713{RESET} Connected! " + f"({model} via {result.provider}, {result.latency_ms}ms)" + ) + # Persist keys to .env + for env_var, value in provided_keys.items(): + save_env_var(env_var, value, directory) + _ok(f"Saved credentials to .env") + return True + else: + print(f"\r {RED}\u2717{RESET} {result.error} ") + # Roll back + for env_var in provided_keys: + os.environ.pop(env_var, None) + return False + + +def _setup_model( + role_label: str, + directory: Path, + *, + default_model: str = "", +) -> str: + """Prompt for model, validate connection. Return the validated model string. + + Loops until validation succeeds or the user quits (Ctrl-C). + """ + while True: + model = _prompt(f"{role_label} model", default=default_model) + if not model: + continue + + provider = get_provider(model) + + if provider == "unknown": + _fail(f"Could not detect a provider for '{model}'.") + _info("Use the format: provider/model-name (e.g. groq/llama-3.1-70b)") + suggestions = suggest_models(model) + if suggestions: + _info("Did you mean one of these?") + for s in suggestions[:5]: + _info(f" - {s}") + print() + continue + + if _validate_and_prompt_keys(model, provider, directory): + return model + + print() # blank line before retry + + +# --------------------------------------------------------------------------- +# Main setup flow +# --------------------------------------------------------------------------- + + +def run_setup(directory: str | Path = ".") -> ACEModelConfig: + """Run the interactive setup wizard. Returns the saved config.""" + directory = Path(directory).resolve() + + print() + print(f"{BOLD}ACE Setup{RESET}") + print() + + # Load existing .env if present + load_dotenv() + + # Check for existing config + existing = find_config(directory) + if existing: + try: + old = load_config(existing.parent) + _info(f"Found existing config: {existing}") + _info(f" Default model: {old.default.model}") + for role in ("agent", "reflector", "skill_manager"): + cfg = getattr(old, role) + if cfg: + _info(f" {role}: {cfg.model}") + print() + if not _confirm("Reconfigure?"): + print() + _ok("Keeping existing config.") + return old + print() + except Exception: + pass # corrupted config — just reconfigure + + # Step 1: Default model + print(f"{BOLD}Step 1: Choose your model{RESET}") + print() + _info("Examples: gpt-4o-mini, claude-sonnet-4-20250514, ollama/llama2") + _info(f"Search models: {CYAN}ace models <query>{RESET}") + print() + + default_model = _setup_model("Default", directory) + print() + + # Step 2: Per-role assignment + print(f"{BOLD}Step 2: Role assignment{RESET}") + print() + _info("ACE uses three roles. You can assign a different model to each,") + _info("or use the same model for all (recommended to start).") + print() + + use_same = _confirm("Use this model for all roles?") + + agent_cfg: ModelConfig | None = None + reflector_cfg: ModelConfig | None = None + skill_manager_cfg: ModelConfig | None = None + + if not use_same: + print() + _info("Press Enter to keep the default for any role.") + print() + + for role_name, label in [ + ("agent", "Agent (executes tasks)"), + ("reflector", "Reflector (analyses results)"), + ("skill_manager", "Skill Manager (updates skillbook)"), + ]: + model = _prompt(label, default=default_model) + if model != default_model: + model = _setup_model(label, directory, default_model=model) + if role_name == "agent": + agent_cfg = ModelConfig(model=model) + elif role_name == "reflector": + reflector_cfg = ModelConfig(model=model) + else: + skill_manager_cfg = ModelConfig(model=model) + + # Build and save config + config = ACEModelConfig( + default=ModelConfig(model=default_model), + agent=agent_cfg, + reflector=reflector_cfg, + skill_manager=skill_manager_cfg, + ) + + config_path = save_config(config, directory) + print() + _ok(f"Saved model config to {config_path.name}") + + # Summary + print() + print(f" {BOLD}Configuration summary:{RESET}") + _info(f" default: {default_model}") + for role in ("agent", "reflector", "skill_manager"): + cfg = getattr(config, role) + if cfg: + _info(f" {role + ':':<16}{cfg.model}") + print() + print(f" {BOLD}Ready!{RESET} Use in code:") + print() + print(f" {CYAN}from ace import ACELiteLLM{RESET}") + print(f" {CYAN}ace = ACELiteLLM.from_setup(){RESET}") + print() + + return config + + +# --------------------------------------------------------------------------- +# CLI entry point +# --------------------------------------------------------------------------- + + +def main() -> None: + """Entry point for ``ace`` CLI.""" + import argparse + + parser = argparse.ArgumentParser( + prog="ace", + description="ACE Framework CLI", + ) + subparsers = parser.add_subparsers(dest="command") + + # ace setup + setup_parser = subparsers.add_parser("setup", help="Configure models and API keys") + setup_parser.add_argument( + "--dir", + default=".", + help="Directory to save config files (default: current directory)", + ) + + # ace models + models_parser = subparsers.add_parser("models", help="Search available models") + models_parser.add_argument( + "query", nargs="*", default=[], help="Search query (multiple terms = match all)" + ) + models_parser.add_argument("--provider", default=None, help="Filter by provider") + models_parser.add_argument( + "--limit", type=int, default=20, help="Max results (default: 20)" + ) + + # ace validate + validate_parser = subparsers.add_parser( + "validate", help="Validate a model connection" + ) + validate_parser.add_argument("model", help="Model name to validate") + + # ace config + subparsers.add_parser("config", help="Show current configuration") + + args = parser.parse_args() + + if args.command == "setup": + run_setup(args.dir) + elif args.command == "models": + _cmd_models(" ".join(args.query), args.provider, args.limit) + elif args.command == "validate": + _cmd_validate(args.model) + elif args.command == "config": + _cmd_config() + else: + parser.print_help() + + +# --------------------------------------------------------------------------- +# Subcommands +# --------------------------------------------------------------------------- + + +def _cmd_models(query: str, provider: str | None, limit: int) -> None: + """``ace models [query]`` — search available models.""" + if not query and not provider: + print(f"Usage: {CYAN}ace models <query>{RESET}") + print() + print("Examples:") + print(f" {CYAN}ace models claude{RESET} All Claude models") + print(f" {CYAN}ace models gpt 4o{RESET} GPT-4o variants") + print(f" {CYAN}ace models haiku us{RESET} US-region Haiku models") + print(f" {CYAN}ace models --provider openai{RESET} All OpenAI models") + return + + _load_project_dotenv() + results, total = search_models(query=query, provider=provider, limit=limit) + + if not results: + print(f"No models matching '{query}'.") + print(f"Try: {CYAN}ace models gpt-4o{RESET} or {CYAN}ace models claude{RESET}") + return + + print( + f"{'Model':<45} {'Provider':<15} {'Input $/M':<10} {'Output $/M':<11} {'Key'}" + ) + print("-" * 90) + + for m in results: + in_cost = f"${m.input_cost_per_m:.2f}" if m.input_cost_per_m else "-" + out_cost = f"${m.output_cost_per_m:.2f}" if m.output_cost_per_m else "-" + key_status = f"{GREEN}\u2713{RESET}" if m.key_found else f"{RED}\u2717{RESET}" + print( + f"{m.model:<45} {m.provider:<15} {in_cost:<10} {out_cost:<11} {key_status}" + ) + + if total > limit: + print() + print( + f"{DIM}Showing {limit} of {total} models. " + f"Narrow your search: {CYAN}ace models <query>{RESET}" + f"{DIM} or use {CYAN}--limit {total}{RESET}" + ) + + +def _cmd_validate(model: str) -> None: + """``ace validate <model>`` — test a model connection.""" + _load_project_dotenv() + + print(f"Validating {model}...", end="", flush=True) + result = validate_connection(model) + + if result.success: + print( + f"\r{GREEN}\u2713{RESET} Connected! " + f"({model} via {result.provider}, {result.latency_ms}ms)" + ) + else: + print(f"\r{RED}\u2717{RESET} {result.error}") + suggestions = suggest_models(model) + if suggestions: + print("Did you mean:") + for s in suggestions: + print(f" - {s}") + sys.exit(1) + + +def _cmd_config() -> None: + """``ace config`` — show current configuration.""" + _load_project_dotenv() + + config_path = find_config() + if config_path is None: + print(f"No ace.toml found. Run {CYAN}ace setup{RESET} to create one.") + sys.exit(1) + + try: + config = load_config(config_path.parent) + except Exception as e: + _fail(f"Error reading {config_path}: {e}") + sys.exit(1) + + print(f"{BOLD}Configuration{RESET} ({config_path})") + print() + print(f" {'Role':<16} {'Model':<45}") + print(f" {'-' * 16} {'-' * 45}") + print(f" {'default':<16} {config.default.model}") + for role in ("agent", "reflector", "skill_manager"): + cfg = getattr(config, role) + model = cfg.model if cfg else f"{DIM}(default){RESET}" + print(f" {role:<16} {model}") diff --git a/ace/cli/skills/__init__.py b/ace/cli/skills/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ace/cli/skills/kayba-pipeline/SKILL.md b/ace/cli/skills/kayba-pipeline/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..7e88e52021c7debaa84d0f5c5767ead28d4fc12d --- /dev/null +++ b/ace/cli/skills/kayba-pipeline/SKILL.md @@ -0,0 +1,145 @@ +--- +name: kayba-pipeline +description: End-to-end agent evaluation and improvement pipeline. Takes a traces folder and optional HITL flag, then orchestrates sub-agents through 7 stages — each stage is its own skill invoked by a dedicated sub-agent. Trigger when the user says "run the pipeline", "kayba pipeline", "evaluate and fix", "full eval", "analyze traces and fix", or provides a traces folder with intent to improve their agent. +--- + +# kayba-pipeline + +End-to-end pipeline: analyze traces → define metrics → build rubric → plan fixes → implement fixes. + +Each stage is a separate skill file that can be run independently or as part of this pipeline. + +## Inputs + +The user provides two things: + +1. **`TRACES_FOLDER`** — path to a directory containing trace JSON files +2. **`HITL`** — `true` or `false` — whether to pause for human review before implementing fixes + +If the user doesn't specify HITL, default to `true` (safe default). + +--- + +## Pipeline overview + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Stage 1: Kayba API Analysis → skill: kayba-pipeline:stage-1-api-analysis │ +│ Stage 2: Domain Context Gathering → skill: kayba-pipeline:stage-2-domain-context │ +│ ─── stages 1 & 2 run in parallel ─── │ +│ Stage 3: Metrics & Analysis → skill: kayba-pipeline:stage-3-metrics │ +│ Stage 4: Rubric Definition → skill: kayba-pipeline:stage-4-rubric │ +│ Stage 5: Action Plan → skill: kayba-pipeline:stage-5-action-plan │ +│ Stage 6: HITL Gate → skill: kayba-pipeline:stage-6-hitl │ +│ Stage 7: Fix Implementation → skill: kayba-pipeline:stage-7-fixer │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Orchestration instructions + +You are the orchestrator. Your job is to: +1. Create the `eval/` directory and `eval/pipeline_log.md` +2. Spawn sub-agents that invoke stage skills via the Skill tool +3. Coordinate stage ordering and handle the HITL gate + +### Setup + +Create `eval/` directory and initialize `eval/pipeline_log.md`: + +```markdown +# Pipeline Log + +| Stage | Name | Status | Started | Completed | Notes | +|-------|------|--------|---------|-----------|-------| +| 1 | Kayba API Analysis | pending | | | | +| 2 | Domain Context | pending | | | | +| 3 | Metrics & Analysis | pending | | | | +| 4 | Rubric Definition | pending | | | | +| 5 | Action Plan | pending | | | | +| 6 | HITL Gate | pending | | | | +| 7 | Fix Implementation | pending | | | | +``` + +### Stages 1 & 2 — run in parallel + +Spawn two sub-agents in parallel using the Agent tool: + +**Agent 1:** +- Name: `api-analyst` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-1-api-analysis" using the Skill tool. The traces folder is: {TRACES_FOLDER}. Follow the skill instructions completely.` + +**Agent 2:** +- Name: `domain-scout` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-2-domain-context" using the Skill tool. The traces folder is: {TRACES_FOLDER}. Follow the skill instructions completely.` + +Wait for both to complete before proceeding. + +### Stage 3 — sequential + +Spawn one sub-agent after stages 1 & 2 complete: + +- Name: `metric-engineer` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-3-metrics" using the Skill tool. The traces folder is: {TRACES_FOLDER}. Follow the skill instructions completely — this includes iterating on the metrics until you're satisfied.` + +### Stage 4 — sequential + +Spawn one sub-agent after stage 3 completes: + +- Name: `rubric-builder` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-4-rubric" using the Skill tool. Follow the skill instructions completely.` + +### Stage 5 — sequential + +Spawn one sub-agent after stage 4 completes: + +- Name: `action-planner` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-5-action-plan" using the Skill tool. Follow the skill instructions completely.` + +### Stage 6 — HITL Gate + +**If `HITL` is `true`:** + +Spawn one sub-agent after stage 5 completes: + +- Name: `hitl-reviewer` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-6-hitl" using the Skill tool. Follow the skill instructions completely. Present the full review to the user and collect their decision before proceeding.` + +Wait for the sub-agent to complete. Check `eval/stage6_decision.md` for the outcome: +- If decision is "Approve all" or "Approve with modifications" — proceed to Stage 7 +- If decision is "Reject" — re-run Stage 5 with the user feedback recorded in `eval/stage6_decision.md`, then re-run Stage 6 +- Only proceed to Stage 7 after a clear approval is recorded + +**If `HITL` is `false`:** +- Skip to Stage 7 +- Log "HITL skipped" in `eval/pipeline_log.md` + +### Stage 7 — sequential + +Spawn one sub-agent after stage 6 completes (or is skipped): + +- Name: `fixer` +- Type: `general-purpose` +- Prompt: `Invoke the skill "kayba-pipeline:stage-7-fixer" using the Skill tool. Follow the skill instructions completely.` + +--- + +## Error handling + +- If any stage fails, log the failure in `eval/pipeline_log.md` with the stage number and error +- Do not proceed to dependent stages if a prerequisite failed +- If Stage 1 fails (kayba CLI issues), ask the user whether to proceed without API insights — if yes, skip Stage 1 and have Stage 3 work from domain context + raw traces only + +## After completion + +Update `eval/pipeline_log.md` with final status for all stages. Report to the user: +- How many stages completed successfully +- Summary of metrics (from rubric) +- Summary of fixes applied (from changes log) diff --git a/ace/cli/skills/kayba-pipeline/stage-1-api-analysis/SKILL.md b/ace/cli/skills/kayba-pipeline/stage-1-api-analysis/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..34b14aba1f0bdfcf16e601a8a8ffdcb014160a22 --- /dev/null +++ b/ace/cli/skills/kayba-pipeline/stage-1-api-analysis/SKILL.md @@ -0,0 +1,84 @@ +--- +name: kayba-stage-1-api-analysis +description: Fetch pre-computed insights from the Kayba API and build a structured summary. Does NOT upload traces or trigger generation — analysis is assumed to already exist. Trigger when the user says "run stage 1", "get insights", "fetch skills", "kayba analyze", or when invoked by the kayba-pipeline orchestrator. Requires the kayba CLI to be installed and KAYBA_API_KEY to be set. +--- + +# Stage 1: Kayba API Analysis (Fetch-Only Mode) + +Fetch pre-computed insights from the Kayba API. Traces have already been uploaded and analyzed — this stage only pulls results. + +## Inputs + +- **`TRACES_FOLDER`** — passed by the orchestrator but **ignored** in this stage. Traces are already uploaded and analyzed on the Kayba side. Do NOT upload, validate, or read trace files. + +## Process + +### Step 1: Setup + +Ensure `eval/` directory exists at the project root. + +### Step 2: Fetch insights + +``` +kayba insights list --json > eval/insights.json +``` + +If `kayba` is not found in PATH, search common locations (`.venv/bin/kayba`, project virtualenvs). If found, use the full path. If not found anywhere, report the error and stop. + +If `KAYBA_API_KEY` is not set, report the error and stop. + +### Step 3: Insight quality gate + +Read `eval/insights.json` and run quality checks before building the summary: + +1. **Empty check**: if the insights array is empty (0 insights returned), report this as a warning. Write a minimal summary noting "0 insights generated" and stop — downstream stages cannot proceed without insights. +2. **Duplicate detection**: compare insight `content` fields pairwise. If two insights cover substantially the same behavior (same section, overlapping evidence traces, similar corrective action), flag them as potential duplicates in the summary. Do not remove them — just annotate. +3. **Evidence coverage**: for each insight, check if the `evidence` field references specific traces (e.g., "task_7 turn 4"). Insights with no trace-specific evidence are lower quality — flag as "low-evidence" in the summary. +4. **Vote signal**: insights with `status: "accepted"` and `helpful > 0` have been human-validated. Insights with `status: "new"` and `helpful: 0, harmful: 0` are unvalidated — note this distinction in the summary. + +Log the quality gate result: `"Insight quality: {total} insights, {accepted} accepted, {new_unvalidated} unvalidated, {duplicates} potential duplicate pairs, {low_evidence} low-evidence"` + +### Step 4: Build structured summary + +Extract a structured summary of each insight: +- Insight ID and title/summary (use the `section` field as the title) +- Status +- Evidence citations — specific trace references, error strings, behavioral patterns the reflector identified +- Justification / reasoning chain — the reflector's full analysis of why this is a real pattern +- Confidence score if available +- Helpful/harmful counts if available +- Quality flags from Step 3 (potential duplicate, low-evidence, unvalidated) + +Write the structured summary to `eval/stage1_insights_summary.md` using this format: + +```markdown +# Kayba Insights Summary + +Generated from: Kayba API (pre-computed analysis) +Total insights: N +Quality: {accepted} accepted, {unvalidated} unvalidated, {duplicate_pairs} potential duplicate pairs, {low_evidence} low-evidence + +## Insight: [ID] — [section title] +**Status:** [status] [quality flags if any, e.g., "[potential duplicate with ID]", "[low-evidence]", "[unvalidated]"] +**Confidence:** [score if available] +**Evidence:** +- [citation 1 — trace reference, error string, or behavioral pattern] +- [citation 2] +**Justification:** [reflector's reasoning for why this is a real pattern] +**Helpful/Harmful:** [counts if available] + +--- +[repeat for each insight] +``` + +## Error handling + +- If `kayba` is not found in PATH or common locations, report the error and stop +- If `KAYBA_API_KEY` is not set, report the error and stop +- If `kayba insights list` fails (network error, auth error), report the error and stop +- If 0 insights are returned, write a minimal summary and stop — downstream stages need insights + +## Outputs + +- `eval/insights.json` — raw API response +- `eval/stage1_insights_summary.md` — structured summary with quality annotations for downstream stages diff --git a/ace/cli/skills/kayba-pipeline/stage-2-domain-context/SKILL.md b/ace/cli/skills/kayba-pipeline/stage-2-domain-context/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..72791ddd80c0159a7e27f589a5f91469b122713c --- /dev/null +++ b/ace/cli/skills/kayba-pipeline/stage-2-domain-context/SKILL.md @@ -0,0 +1,166 @@ +--- +name: kayba-stage-2-domain-context +description: Gather domain context about the repository and agent — system prompt, tool definitions, domain docs, and behavior patterns from traces. Trigger when the user says "run stage 2", "gather context", "domain context", or when invoked by the kayba-pipeline orchestrator. +--- + +# Stage 2: Domain Context Gathering + +Understand the agent's world — what it does, what tools it has, and what "success" looks like. + +## Inputs + +- **`TRACES_FOLDER`** — path to directory containing trace JSON files + +## Process + +### 0. Detect trace format + +Before reading traces, identify the framework that produced them. Read 1 trace file and check: + +| Signal | Framework | +|--------|-----------| +| `info.agent_info.implementation`, `info.environment_info`, `simulation.messages[]` with `role`/`tool_calls`/`turn_idx` | **tau2-bench** | +| `runs[].steps[]` with `type: "tool"`, `lc_kwargs` | **LangChain / LangSmith** | +| `events[]` with `event_type`, `span_id`, `parent_id` | **LlamaIndex** | +| `choices[].message.tool_calls[]` at top level | **Raw OpenAI API logs** | +| `trace.spans[]` with `attributes`, `trace_id` | **OpenTelemetry / Arize / Langfuse** | + +Record the detected format in the output under **Trace Format**. All subsequent trace-reading steps use the field paths appropriate for that format. + +If the format is unrecognized, note the top-level keys and structure, then proceed best-effort with field names found in the data. + +### 1. Detect architecture + +Read 2-3 traces and determine if this is a single-agent or multi-agent system: + +- **Single agent**: one `agent_info` entry, one conversation thread, tool calls from one identity +- **Multi-agent / router**: look for multiple `agent_info` entries, routing tool calls (e.g., `transfer_to_*`, `delegate_to_*`), sub-conversation arrays, or distinct system prompts per agent identity + +If multi-agent: document each agent separately (name, role, tools, handoff triggers) and note the routing logic. The remaining steps apply per-agent. + +### 2. Find the system prompt + +Use a fallback chain — stop at the first hit: + +1. **Config files** — grep for keys: `system_prompt`, `system_message`, `instructions`, `AGENT_INSTRUCTION`, `SYSTEM_PROMPT` in YAML/JSON/TOML/Python/JS files +2. **Source code** — search for prompt template strings, f-strings, or `.format()` calls that build the system message (look in agent implementation files) +3. **Trace extraction** — read 3 trace files from `{TRACES_FOLDER}`: + - Check `info.environment_info.policy` (tau2-bench format) + - Check first message with `role: "system"` in the messages array + - Check `raw_data` fields for system-level content +4. **Not found** — if none of the above yields a system prompt, explicitly record `SYSTEM_PROMPT_STATUS: NOT_FOUND` in the output and flag this for the orchestrator. Do not fabricate or guess. + +When found, record both the prompt content and its **source location** (file path + line, or trace field path). + +### 3. Extract tool definitions + +Two-pass approach: source code first (ground truth), then traces (usage evidence). + +**Pass 1 — Source code discovery:** +- Search for tool/function definition patterns: `@tool`, `@is_tool`, `def tool_`, function schema arrays, OpenAPI specs, `tools=[]` arguments +- For each tool, extract from source: + - Name + - Input parameters with types and defaults + - Return type / output schema (document the structure, not just "returns a dict") + - Side effects: READ (no state change), WRITE (mutates state), GENERIC (neither) + - Validation rules the tool does NOT enforce (critical — grep for comments like "API does not check", "agent must enforce") + +**Pass 2 — Trace usage evidence:** +- Read ALL traces (if <= 20) or a stratified sample (see step 4 for sampling) +- Extract every unique `tool_calls[].name` from assistant messages +- Extract every `role: "tool"` response to document actual output shapes +- For each tool, record one example input/output pair from traces + +**Reconcile the two passes:** +- Tools in source but NOT in traces = "available but unused" — flag these; they may be relevant for edge cases the agent should handle +- Tools in traces but NOT in source = possible dynamic tools or external APIs — investigate + +Output the full tool inventory as a table with columns: Name, Category, Input Schema, Output Schema, Observed in Traces (Y/N), Unvalidated Rules. + +### 4. Find domain documentation + +- READMEs, product docs, wiki links +- Policy files (e.g., `data/*/policy.md`, domain-specific docs) +- Inline code comments explaining business logic +- Test files that describe expected behavior +- Anything that explains what the agent does and what "success" means for its users + +### 5. Catalogue agent behavior patterns + +**Trace selection — stratified sampling** (do not just grab "5-10 random traces"): + +1. Count total traces in `{TRACES_FOLDER}`. If <= 20, read ALL of them. +2. If > 20, select a stratified sample: + - Sort by `termination_reason` — include at least 2 per unique reason + - Sort by conversation length (message count) — include shortest, longest, and 2 median + - Sort by tool call count — include lowest and highest + - If task outcomes are available (pass/fail), include at least 3 of each + - Target: ~15 traces total, or 30% of the corpus, whichever is larger + +For each selected trace, document: +- **Function call frequency** — which tools are called most, in what order +- **Tool call sequences** — common tool chains (e.g., get_user -> get_reservation -> cancel) +- **Success patterns** — what does a thread that accomplishes its goal look like? +- **Failure patterns** — what does a thread that fails or gets stuck look like? +- **Error patterns** — what error strings appear in tool outputs? Group by root cause +- **Policy violation patterns** — where does the agent break its own rules? (e.g., multiple tool calls per turn, acting without confirmation) +- **User feedback signals** — reverts, ratings, explicit corrections, escalations, stop tokens, transfer tokens + +### 6. Write findings + +Write all findings to `eval/stage2_domain_context.md`: + +```markdown +# Domain Context + +## Trace Format +- Framework: [detected framework name] +- Key field paths: [e.g., simulation.messages[], info.environment_info.policy] + +## Architecture +- Type: [single-agent | multi-agent] +- [If multi-agent: agent roster with roles and handoff triggers] + +## Agent Purpose +[1-2 sentence summary of what this agent does] + +## System Prompt +- **Source**: [file path + line, or trace field path, or NOT_FOUND] +- **Status**: [verbatim | reconstructed | not_found] + +[The system prompt content, or "NOT_FOUND — downstream stages should account for missing system prompt"] + +## Tools +| Tool | Category | Input Schema | Output Schema | In Traces? | Unvalidated Rules | +|------|----------|-------------|---------------|------------|-------------------| +| tool_name | READ/WRITE/GENERIC | `{param: type}` | `{field: type}` | Y/N | "API does not check X" | + +### Tools available but never called in traces +- [tool_name — why it matters] + +## Domain Rules +[Key business rules, constraints, policies the agent must follow] + +## Behavior Patterns + +### Success patterns +- [pattern 1] + +### Failure patterns +- [pattern 1] + +### Policy violation patterns +- [violation with frequency: N/M turns] + +### Error patterns +| Error | Frequency | Root cause | +|-------|-----------|------------| +| error string | N traces | cause | + +### User feedback signals +- [signal 1] +``` + +## Outputs + +- `eval/stage2_domain_context.md` diff --git a/ace/cli/skills/kayba-pipeline/stage-3-metrics/SKILL.md b/ace/cli/skills/kayba-pipeline/stage-3-metrics/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..a6ea98c0c84c7632c3140de9a0cad2ef4c6b8fce --- /dev/null +++ b/ace/cli/skills/kayba-pipeline/stage-3-metrics/SKILL.md @@ -0,0 +1,180 @@ +--- +name: kayba-stage-3-metrics +description: Define metrics from Kayba insights, implement them as Python measurement code, run against traces, and iterate until the metrics are clean and meaningful. Trigger when the user says "run stage 3", "define metrics", "build metrics", "compute baselines", or when invoked by the kayba-pipeline orchestrator. Requires eval/stage1_insights_summary.md and eval/stage2_domain_context.md to exist. +--- + +# Stage 3: Metrics and Programmatic Analysis + +Define metrics from insights, implement as code, run, review, iterate. + +## Inputs + +- **`TRACES_FOLDER`** — path to directory containing trace JSON files +- **`eval/stage1_insights_summary.md`** — output from Stage 1 +- **`eval/stage2_domain_context.md`** — output from Stage 2 + +Read both input files before starting. + +## Process + +This stage is iterative. You cycle through define → implement → run → review, with a hard cap of **3 iterations**. A metric set is "clean" when ALL of the following hold: + +1. **No small-sample metrics in the priority set** — every metric used for priority ranking has denominator >= 5. Metrics with denominator < 5 are kept but labeled `"confidence": "directional-only"` and excluded from priority sorting. +2. **No unexplained extremes** — no metric reads 0% or 100% unless you can write a one-sentence justification (e.g., "0% is correct because the agent never calls send_certificate anywhere in the dataset"). Record the justification in the metric's `"extreme_justification"` field. +3. **No redundant pairs** — no two metrics share > 70% of their denominator events. Check this: for each pair, compute `|events_A ∩ events_B| / min(|events_A|, |events_B|)`. If > 0.70, merge or drop one. +4. **Script runs without errors** on the full trace set. + +If after 3 iterations the set is not fully clean, ship what you have and log remaining issues in `eval/baseline_metrics.json` under a top-level `"warnings"` key. + +### Step 1: Define metrics + +For each insight from the Kayba analysis, use the evidence fields to identify observable signals in the traces: + +1. Read the insights summary — focus on evidence citations, error strings, behavioral patterns +2. For each valid insight, determine what trace signal would change if the agent followed the skill +3. Classify each metric by detector pattern type: + +**Recovery detectors** — consecutive calls to the same function where first has error, next succeeds +```python +def has_recovery(calls, function_name): + for i in range(len(calls) - 1): + if calls[i]['name'] == function_name and is_error(calls[i]['output']): + if calls[i+1]['name'] == function_name and is_success(calls[i+1]['output']): + return True + return False +``` + +**Loop detectors** — N+ consecutive calls to the same function (stuck agent) + +**Give-up detectors** — regex match agent output for abandonment phrases ("I'm unable to", "cannot complete", "beyond my capabilities") + +**Error classifiers** — match function outputs against domain-specific error patterns. Build a pattern table: +```python +ERROR_PATTERNS = { + 'pattern_name': r'regex matching the error', + # one entry per distinct error type +} +``` + +**Over-exploration detectors** — ratio of explore vs action calls. Use the tool categories from Stage 2. If explore ratio exceeds threshold AND task didn't complete → analysis paralysis + +**Ground-truth comparison detectors** — agent claims a value (dollar amount, flight number, policy rule) in natural language, and the preceding tool response contains the actual value. Extract candidate values from agent text via regex, then compare against structured fields in the tool response JSON. Examples: +```python +# Extract dollar amounts from agent text +DOLLAR_PATTERN = r'\$\s?([\d,]+(?:\.\d{2})?)' + +# Extract flight numbers (3 letters + 3 digits) +FLIGHT_PATTERN = r'\b([A-Z]{2,3}\d{3,4})\b' + +def check_agent_claims_against_tool(agent_text, preceding_tool_response): + """Compare values the agent states against the tool response ground truth.""" + claimed_amounts = re.findall(DOLLAR_PATTERN, agent_text) + actual_amounts = extract_amounts_from_json(preceding_tool_response) + # A claim is fabricated if it doesn't match any actual value + fabricated = [c for c in claimed_amounts if not any(matches(c, a) for a in actual_amounts)] + return len(fabricated) == 0, fabricated +``` +This pattern covers data accuracy (fabricated prices/flights), post-action verification (quoted vs actual cost), and policy accuracy (claimed restrictions vs policy text). These are NOT qualitative-only — regex + JSON comparison is noisy but produces a real signal. Build the detector even if it's imperfect; a noisy metric that produces a fix is better than a clean classification that produces nothing. + +**Ordering/sequencing detectors** — agent performs actions in the wrong order (e.g., searches for flights before checking if the reservation is even modifiable). Check whether tool call A appears before tool call B when B should come first. + +**Clean success** — threads where all tasks completed with no errors and no other tags + +4. **Validate each detector before coding it at scale.** Pick 2-3 traces where you already know the ground truth from Stage 1 evidence. Run your detector logic mentally (or in a scratch script) against those traces. If it misclassifies any of them, fix the logic before writing the full implementation. This catches regex and pattern bugs early — the Stage 3 trace showed multiple iterations wasted on broken confirmation-phrase matching that a quick manual check would have caught. + +### Step 2: Implement and run + +1. Write `eval/compute_baselines.py` with: + - CLI args: `--traces-dir` (required), `--output` (default: `eval/baseline_metrics.json`) + - `load_traces(traces_dir)` — loads all JSON trace files + - Error pattern table built from reading 20-30 traces + - `tag_thread(thread)` — combines all detectors, returns list of tags + - One measurement function per metric, computing `numerator / denominator` + - `compute_all_baselines(traces_dir)` — runs all metrics, returns dict + - Main block that runs everything and prints summary + +2. Run it: + ``` + python eval/compute_baselines.py --traces-dir {TRACES_FOLDER} --output eval/baseline_metrics.json + ``` + +### Step 3: Review and iterate + +Run these checks in order after every run. Each check either passes or produces a concrete fix action. + +**Check A — Script health.** Did the script error or produce `null` values? → fix and re-run. This is iteration 0-cost; don't count it toward the 3-iteration cap. + +**Check B — Small-sample guard.** For each metric, examine the denominator: +- denominator >= 5 → full-confidence metric, usable for priority ranking +- denominator 1-4 → label `"confidence": "directional-only"` in the output JSON. The metric stays in the report but is excluded from priority sorting in Stage 4. Do NOT drop it — small-sample metrics can still inform qualitative analysis. +- denominator 0 → the detector found no applicable events. Either the detector is broken (fix it) or the behavior genuinely doesn't occur in this trace set (log as `"confidence": "not-observed"` and move on). + +**Check C — Extreme-value triage.** For any metric at exactly 0% or 100%: +- Ask: "Is there a plausible trace where this metric would NOT be extreme?" If yes → detector is likely broken, fix it. +- If no (the behavior legitimately always/never happens in this dataset) → write a one-sentence justification and add it as `"extreme_justification"` in the output. Example: M5=0% is correct because both cancellations in the dataset were on ineligible reservations. +- Do NOT reflexively drop 0%/100% metrics. A metric that correctly reads 0% is a strong signal for Stage 5 action planning. +- **Ceiling/floor flag for 100% and 0% metrics:** If a metric baseline is already at 100% (or 0% where 0% is the desired direction), add `"at_ceiling": true` (or `"at_floor": true`) to its entry in the output JSON. This signals to Stage 4 (direction setting) and Stage 5 (action planning) that the metric is already optimal and should NOT be listed as needing improvement. Stage 4 must set its direction to `"↑ maintain"` or `"— already optimal"`, never bare `"↑"`. + +**Check D — Correlation / overlap audit.** For every pair of metrics, compute event overlap: `|denom_A ∩ denom_B| / min(|denom_A|, |denom_B|)`. If > 0.70: +- The two metrics are measuring overlapping populations. Keep the one with the sharper behavioral distinction (measures a more specific failure mode). Drop or merge the other. +- In the Stage 3 trace, M1 and M2 shared identical denominators (29 tool-calling turns) and were never flagged. They survived because they measure different *properties* of the same events — this is acceptable only if the numerator overlap is also checked. If both numerators move in lockstep (one is a strict subset of the other), merge them. + +**Check E — Coverage (strict).** For EVERY Stage 1 insight, verify it has a corresponding metric. If an insight has no metric: +- First, try harder to build one. Can you extract values from agent text and compare against tool responses? Can you detect the wrong tool-call ordering? Can you pattern-match the failure mode with keywords + JSON field checks? +- Only after a concrete failed attempt, classify as unmeasurable with a specific reason why the approach you tried doesn't work. +- An insight classified as unmeasurable means Stage 5 will NOT produce a fix for it. That is a real cost. Treat every unmeasurable classification as a missed fix. + +After checks, if any produced a fix action: apply fixes and re-run (counts as one iteration). If all checks pass → the metric set is clean. **Stop iterating.** + +### Design principles + +- **Target one metric per insight.** Every insight should have a metric unless it is genuinely unmeasurable (see above). If you end up with fewer metrics than insights, you are being too conservative. Directional-only metrics (denominator < 5) still count — they produce fixes in Stage 5. Only apply the redundancy check (Check D) to merge metrics that truly overlap; do not use the metric count as a reason to skip building detectors. +- **Express every metric as a ratio or percentage.** Absolute counts aren't comparable across trace sets. +- **Prefer per-event denominators over per-thread.** "% of EditScript calls with errors" is sharper than "% of threads with any EditScript error." Per-thread denominators compress information — a thread with 10 violations and a thread with 1 both count the same. +- **One metric per behavioral change.** If two would always move together, keep only the sharper one. Use Check D (overlap audit) to enforce this mechanically, not just by intuition. +- **Build a metric for EVERY insight. "Unmeasurable" is a last resort, not a default.** Before classifying an insight as unmeasurable, you MUST attempt to build a programmatic detector. The bar for "unmeasurable" is: you tried a concrete approach, it fundamentally cannot work (not just "it's noisy"), and you can explain why in one sentence. + + Specifically: + - **"Agent claims X but tool response says Y"** — this is ALWAYS measurable. Use regex to extract values (dollar amounts, IDs, flight numbers) from agent text, compare against structured JSON fields in the preceding tool response. Noisy matches are fine — a metric that catches 70% of fabrications is far more useful than a qualitative note that catches 0%. + - **"Agent violates policy rule Z"** — if the policy rule can be stated as a condition on trace data (tool call ordering, presence/absence of a call, argument values), build a detector. Only classify as qualitative-only if the rule requires understanding the *meaning* of free-text agent output beyond keyword/pattern matching. + - **"Insufficient data"** — if the detector logic is clear but n < 5, build the detector anyway and label it `"confidence": "directional-only"`. Do NOT skip building the metric. A directional-only metric still produces a fix in Stage 5. + + If after genuine effort an insight truly cannot be measured programmatically, classify it as: + - `"qualitative-only"` — requires semantic understanding that regex/JSON comparison cannot approximate. Must explain what specific semantic judgment is needed and why pattern matching fails. + - `"insufficient-data"` — detector exists but denominator is 0 (not just small — literally zero applicable events). Note what scenarios would need to appear in traces. + - `"needs-ground-truth"` — requires task-specific expected outcomes that aren't in the trace format. + + Record any remaining unmeasurable insights in the output JSON under a `"unmeasurable"` key. **The goal is for this list to be as short as possible — ideally empty.** + +## Outputs + +- `eval/compute_baselines.py` — runnable script with `--traces-dir` and `--output` CLI args +- `eval/baseline_metrics.json` — computed baseline values, structured as: + ```json + { + "M1": { + "name": "single_tool_call_compliance", + "value": 0.414, + "numerator": 12, + "denominator": 29, + "confidence": "full" + }, + "M5": { + "name": "cancellation_policy_compliance", + "value": 0.0, + "numerator": 0, + "denominator": 2, + "confidence": "directional-only", + "extreme_justification": "0% correct: both cancellations in dataset were on ineligible reservations" + }, + "warnings": ["M5 and M6 have denominator < 5; excluded from priority ranking"], + "unmeasurable": [ + { + "insight_id": "d7494740", + "name": "Cabin Change Constraints", + "classification": "insufficient-data", + "reason": "Only 1 update_reservation_flights call in dataset" + } + ] + } + ``` diff --git a/ace/cli/skills/kayba-pipeline/stage-4-rubric/SKILL.md b/ace/cli/skills/kayba-pipeline/stage-4-rubric/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..f41f5e7b3a1c3d5575ed3539f1c31f64311a778c --- /dev/null +++ b/ace/cli/skills/kayba-pipeline/stage-4-rubric/SKILL.md @@ -0,0 +1,163 @@ +--- +name: kayba-stage-4-rubric +description: Organize computed metrics into a tiered evaluation rubric with leading, lagging, and quality indicators. Trigger when the user says "run stage 4", "build rubric", "tier metrics", or when invoked by the kayba-pipeline orchestrator. Requires eval/baseline_metrics.json and eval/compute_baselines.py to exist. +--- + +# Stage 4: Rubric Definition + +Organize metrics into a tiered evaluation rubric. Detect and resolve redundancy quantitatively. Ensure every insight is accounted for. + +## Inputs + +- `eval/baseline_metrics.json` — computed baseline values from Stage 3 +- `eval/compute_baselines.py` — to understand what each metric measures +- `eval/stage1_insights_summary.md` — the original insights +- `eval/stage2_domain_context.md` — domain context + +Read all four files before starting. + +## Process + +### 1. Quantitative redundancy check + +Before tiering, check every pair of metrics for overlap. Two metrics are redundancy candidates if ANY of the following hold: + +- **Denominator overlap >70%**: compute `|denom_events(A) ∩ denom_events(B)| / min(|denom(A)|, |denom(B)|)`. If >0.70, they are candidates. To compute this, trace through the detector functions in `compute_baselines.py` and determine which trace events (turns, calls, threads) each denominator iterates over. When denominators are identical sets (same loop, same filter), overlap is 100%. +- **Same skill set**: the metrics map to the exact same set of insight/skill IDs from Stage 1. +- **Logical subsumption**: one metric's positive case is a strict subset of the other's (e.g., "turn has exactly 1 tool call" is a subset of "turn has no user-facing content alongside tool calls" only if every single-call turn also has no content — check this, don't assume it). + +For each candidate pair, make an explicit decision with reasoning: + +| Pair | Denom overlap | Skill overlap | Subsumption? | Decision | Reasoning | +|------|---------------|---------------|--------------|----------|-----------| +| M1/M2 | 100% (same 29 turns) | identical | No — can violate one without the other | **Keep both** | Independently actionable: batching vs. content leaking are distinct fixes | + +Valid decisions: **keep both** (with reasoning why they're independently actionable), **merge** (combine into one metric, specify how), or **drop** (specify which and why). "They feel different" is not sufficient reasoning — cite the specific behavior that one catches and the other misses. + +Final count target: 5-7 metrics after redundancy resolution. + +### 2. Tier each metric + +Use this decision flowchart for every metric: + +``` +Q1: Can a SINGLE skill/instruction change directly move this metric? + → If the agent follows one new instruction and the metric improves, + regardless of other behaviors: LEADING. + +Q2: Does moving this metric require MULTIPLE skills to be adopted together? + → If improvement depends on several upstream behaviors all working + (e.g., proper turn structure + confirmation flow + execution): + LAGGING. + +Q3: Does moving this metric require domain reasoning beyond following instructions? + → If the agent needs to correctly interpret policy rules, evaluate + eligibility criteria, or make judgment calls that can't be reduced + to a single instruction: QUALITY. +``` + +Apply the flowchart to each metric and record the Q1/Q2/Q3 answer that determined the tier. If a metric could arguably be two tiers, pick the lower one (Leading < Lagging < Quality) and note the ambiguity. + +Tier summary for reference: + +| Tier | Purpose | Moves when... | Diagnostic signal | +|------|---------|---------------|-------------------| +| **Leading** | Behaviors a single skill directly changes | Skill is adopted | If leading moves but lagging doesn't → skill adopted but not solving the right problem | +| **Lagging** | Aggregate outcomes requiring multiple skills | Multiple skills coordinate | If lagging moves but leading doesn't → something else improved, not your skills | +| **Quality** | Requires domain understanding, not just instruction-following | Agent reasons correctly | If quality moves but lagging doesn't → agent got lucky or metric is mis-tiered | + +### 3. Flag low-confidence baselines + +Any metric with denominator < 5 events is a **low-confidence baseline**. These metrics: +- ARE included in the rubric (they measure real behaviors) +- Are marked with `**Confidence: low** (n=X)` in the rubric +- Must NOT drive priority decisions in Stage 5 — they inform direction only +- Should note what denominator size would make them reliable (rule of thumb: n >= 10 for a rate metric to be meaningful, n >= 30 for statistical comparisons) + +### 4. Set direction + +For each metric, indicate whether it should go **up higher** or **down lower**. Don't set arbitrary numerical targets — baseline + direction is enough. + +**Ceiling guard:** If a metric's baseline is already 100%, its direction MUST be `"↑ maintain"` or `"— already optimal"`, never `"↑"` as if it needs to go higher. A 100% metric is at ceiling — the goal is to sustain it, not improve it. Similarly, if a metric is at 0% and the desired direction is `"↓"`, mark it `"↓ maintain"` or `"— already at floor"`. Do not let any downstream stage (Stage 5 action plan, Stage 7 fixes) list a ceiling/floor metric as needing improvement. + +### 5. Map insights to metrics (completeness check) + +For every insight from `eval/stage1_insights_summary.md`, assign it to one of three categories: + +1. **Mapped** — directly linked to one or more metrics. List which ones. +2. **Indirectly mapped** — supports a metric but isn't the primary driver. List the metric and explain the indirect relationship. +3. **Qualitative-only** — no programmatic metric captures this insight. Explicitly mark it and state why (e.g., "requires LLM-as-judge," "measures explanation quality," "efficiency pattern with no clear denominator"). + +Every insight MUST appear in exactly one category. If you find an insight that should have a metric but doesn't, note it as a gap for future Stage 3 iterations — but do not invent metrics at this stage. + +At the end, report: +- `X / N insights mapped to metrics` +- `Y / N insights indirectly mapped` +- `Z / N insights qualitative-only` + +### 6. Add invalidation notes + +For each metric, write one sentence answering: "What would make this tier assignment wrong?" + +Examples: +- M1 (Leading): "Wrong if fixing batching also requires the agent to change its confirmation flow — that would make it Lagging." +- M5 (Quality): "Wrong if cancellation compliance can be fixed by a single checklist instruction without requiring the agent to reason about policy — that would make it Leading." + +These notes exist so Stage 5 can catch tier errors. If Stage 5 finds evidence that a tier is wrong (e.g., a single skill would move a "Quality" metric), it should flag the conflict rather than silently inheriting the error. + +### 7. Write the rubric + +Write to `eval/baseline_metrics.md`: + +```markdown +# Eval Rubric — Baseline Metrics + +## Summary +| # | Metric | Tier | Baseline | Direction | Confidence | +|---|--------|------|----------|-----------|------------| +| M1 | First-call success rate | Leading | 37.6% | up | ok (n=29) | +| M2 | ... | ... | ... | ... | ... | + +## Tier Definitions + +- **Leading** — Single skill directly moves this. Should change first after deployment. +- **Lagging** — Multiple skills must coordinate. Improves as a consequence of adoption. +- **Quality** — Requires domain reasoning beyond instruction-following. Hardest to move. + +## Metric Details + +### M1: [name] +**Tier:** Leading +**Baseline:** 37.6% (685 / 1,821) +**Confidence:** ok (n=1821) | low (n=X) — needs n>=Y for reliable comparison +**Direction:** up higher is better +**What it measures:** [description] +**How it's computed:** [reference to function in compute_baselines.py] +**Skills that should move this:** [list insight/skill IDs from stage 1] +**Tier rationale:** [which flowchart question determined the tier] +**Invalidation note:** [what would make this tier wrong] + +### M2: [name] +... + +## Redundancy Analysis + +| Pair | Denom overlap | Skill overlap | Subsumption? | Decision | Reasoning | +|------|---------------|---------------|--------------|----------|-----------| +| ... | ... | ... | ... | ... | ... | + +## Insight Coverage + +### Mapped (X / N) +- `insight_id` — [title] → M1, M3 + +### Indirectly mapped (Y / N) +- `insight_id` — [title] → supports M5 via [explanation] + +### Qualitative-only (Z / N) +- `insight_id` — [title] — [why no metric: e.g., "requires LLM-as-judge"] +``` + +## Outputs + +- `eval/baseline_metrics.md` — human-readable tiered rubric with redundancy analysis, confidence flags, insight coverage, and invalidation notes diff --git a/ace/cli/skills/kayba-pipeline/stage-5-action-plan/SKILL.md b/ace/cli/skills/kayba-pipeline/stage-5-action-plan/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..7486e2ac0a954fc13ca7b51e70db1d8e749ee7b4 --- /dev/null +++ b/ace/cli/skills/kayba-pipeline/stage-5-action-plan/SKILL.md @@ -0,0 +1,201 @@ +--- +name: kayba-stage-5-action-plan +description: Triage each insight into discard/code-fix/prompt-fix and produce a prioritized action plan with specific recommendations. Trigger when the user says "run stage 5", "make action plan", "triage skills", or when invoked by the kayba-pipeline orchestrator. Requires eval outputs from stages 1-4. +--- + +# Stage 5: Action Plan + +Triage each insight and produce a concrete, prioritized action plan. + +## Inputs + +- `eval/stage1_insights_summary.md` — insights from Kayba +- `eval/stage2_domain_context.md` — domain context +- `eval/baseline_metrics.md` — the evaluation rubric +- `eval/baseline_metrics.json` — baseline values +- `eval/compute_baselines.py` — measurement code + +Read all files before starting. + +## Process + +### 1. Triage each insight + +For each insight/skill, answer three questions in order: Is it valid? Is it already handled? Is it a code fix or prompt fix? + +#### 1a. Validity check + +- Does it describe a real, recurring problem visible in traces — or noise from a one-off edge case? +- Is it actionable — can the agent actually change this behavior given its tools and context? +- If not valid → verdict: **discard** with a one-sentence reason. + +#### 1b. "Already handled" verification + +Do not rely on memory or assumption. Run these checks and cite what you find: + +1. **Grep the codebase** for 2-3 key terms from the insight (tool names, error strings, behavioral keywords). Example: for an insight about cancellation eligibility, grep for `cancel`, `eligibility`, `criteria`. +2. **Read the existing system prompt text** — check `AGENT_INSTRUCTION` in the agent file and the domain policy file. Quote any existing language that addresses this behavior. +3. **Verdict:** + - If existing text partially covers it → **keep** as a strengthening fix, note what's missing. + - If no existing coverage → **keep**. + - If existing prompt text already covers the behavior thoroughly AND the baseline metric is >= 95% → **discard** (cite the existing text and metric). A high baseline alone is NOT sufficient to discard — if the metric is below 95%, there are still failures to fix. An 87% baseline means 1 in 8 attempts still fails; that is worth fixing. + +#### 1c. Code-vs-prompt decision tree + +Walk through this tree for every non-discarded insight: + +``` +Q1: Can the agent fix this by following different instructions? + (Does it have the right tools, correct data in tool responses, + and sufficient context to behave correctly?) + │ + ├─ YES → PROMPT FIX + │ The agent has everything it needs but acts wrong. + │ A system prompt addition would fix it. + │ + └─ NO → Q2: What is the agent missing? + │ + ├─ Tool doesn't exist, schema is wrong, API returns + │ incomplete data, infrastructure drops information, + │ timeout/error not surfaced to agent + │ → CODE FIX + │ Name the file, function, and specific change. + │ + └─ The agent has partial information but the prompt + can't fully compensate (e.g., needs a new tool + but a heuristic prompt workaround exists) + → PROMPT FIX (primary) + CODE FIX (optional) + Note both. Mark the code fix as "optional" with + a one-sentence justification for why it's lower priority. +``` + +**Ambiguity default:** When genuinely uncertain, default to **prompt fix** and add a note: `"Classification uncertain — defaulting to prompt fix. Revisit if prompt change doesn't move metrics."` This is safer because prompt fixes are cheaper to test and revert, and Stage 7 handles prompt fixes and code fixes through different paths. + +Use the reflector's reasoning from Stage 1 insights — it often explicitly identifies root causes that clarify the code-vs-prompt distinction. + +### 2. Consolidate related insights + +Before writing recommendations, merge insights that are redundant. Two insights should merge when ALL three conditions hold: + +1. **Same target behavior** — they describe the agent doing (or failing to do) the same thing. +2. **Overlapping fix text** — the prompt instructions you'd write for each would share >50% of their content. +3. **Addressing one substantially addresses the other** — fixing insight A would fix >80% of the cases described by insight B. + +**When NOT to merge** — two insights about the same tool or domain area but different failure modes should remain separate. Example: "agent doesn't check cancellation eligibility" and "agent doesn't execute cancellation after user confirms" both involve `cancel_reservation` but are completely different behavioral failures with different prompt fixes. Keep them separate. + +For each merge, document: +- Which insight IDs are combined +- Which insight's framing is primary (use the one with stronger trace evidence) +- What, if anything, is lost from the secondary insight (add it as a sub-point) + +### 3. Write specific recommendations + +For each insight (after merging): + +- **Discards:** one sentence on why it's not valid or actionable. +- **Code fixes:** what code/schema/infrastructure to change. Name the file, the function, the specific change. If Stage 7 needs to find the right code location, give it enough to grep for. +- **Prompt fixes:** the exact instruction text to add to the system prompt, where it should go (e.g., appended to `AGENT_INSTRUCTION`, added to domain policy, or as a standalone skill block), and why this wording over alternatives. + +### 4. Assess risk per fix + +For each non-discarded fix, assess whether the change could break currently-working behaviors: + +| Risk | Definition | Example | +|------|-----------|---------| +| **None** | Change is additive; no existing behavior could be affected | Adding a new metric to compute_baselines.py | +| **Low** | Change targets a behavior that is currently failing; working cases are unrelated | Adding a cancellation checklist when current cancellation compliance is 0% | +| **Medium** | Change modifies a behavior where some cases already work correctly | Strengthening confirmation protocol when 28.6% already succeed — could the new wording break the working 28.6%? | +| **High** | Change rewrites or constrains a behavior that mostly works | Restricting tool-call patterns when 41.4% already comply — overly rigid wording could cause the agent to under-call tools | + +For Medium and High risk fixes, add a one-sentence mitigation: what to watch for, or how to word the prompt to preserve working cases. + +### 5. Handle qualitative-only insights — STILL PRODUCE FIXES + +Some insights from Stage 3 may be flagged as "unmeasurable." **These still get fixes.** An insight that the agent fabricates data or violates policy is a real problem whether or not we can measure it programmatically. Treat them the same as any other insight: + +- Run the same triage (validity → already-handled → code-vs-prompt) as every other insight. +- Include them in the **priority-ranked implementation list** alongside all other fixes. They are NOT second-class. +- Use the trace evidence from the insight (not the metric) to assess impact and priority. If the insight has strong trace evidence showing clear failures, rank it accordingly. +- For prioritization: since there is no metric denominator, use confidence = 0.5 and estimate impact from the severity described in the insight evidence. +- In the fix entry, note that this fix has no programmatic metric for automated before/after comparison, so improvement should be verified via manual trace review or LLM-as-judge after generating new traces. + +Only relegate an insight to a non-actionable "Monitor Items" section if the triage concludes it should be **discarded** (not valid or not actionable). Being unmeasurable is NOT a reason to skip fixing it. + +### 6. Link to metrics + +For each non-discarded fix, identify which metric(s) from the rubric would move if this fix is implemented. Use the metric IDs from `eval/baseline_metrics.md` (e.g., M1, M2). + +### 7. Prioritize + +Rank non-discarded fixes using this formula: + +``` +Priority Score = Impact × Confidence × Tier Bonus ÷ Risk Factor +``` + +Where: +- **Impact** = estimated metric delta. Use the gap between baseline and 100% as the ceiling. A fix expected to close 50% of that gap on M1 (baseline 41.4%) has impact = 0.5 × (1.0 - 0.414) = 0.293. +- **Confidence** = sample size reliability. Use the denominator from `baseline_metrics.json`: + - denominator >= 20: confidence = 1.0 + - denominator 10-19: confidence = 0.8 + - denominator 5-9: confidence = 0.6 + - denominator < 5: confidence = 0.3 +- **Tier Bonus** = leading metrics get a 1.5x multiplier (they validate adoption), lagging and quality get 1.0x. Rationale: leading metrics move first and tell you if your fix is even being adopted — you want those signals early. +- **Risk Factor** = None: 1.0, Low: 1.0, Medium: 1.5, High: 2.0 + +You do not need to compute exact scores to three decimal places. The formula is a tiebreaker and sanity check. The point is: +- High-impact, high-confidence, leading-metric fixes with low risk go first. +- Low-confidence fixes (small denominators) get deprioritized even if the metric is at 0%. +- High-risk fixes get deprioritized unless impact is overwhelming. + +After scoring, apply one manual adjustment pass: if a fix is a prerequisite for another fix (e.g., "confirmation protocol" must exist before "post-confirmation execution" can be measured), promote the prerequisite even if its standalone score is lower. + +## Output format + +Write to `eval/action_plan.md`: + +```markdown +# Action Plan + +## Summary +- Total insights: N +- Discarded: X (with reasons) +- Code fixes: Y +- Prompt fixes: Z +- Fixes without programmatic metric (verify manually): Q + +## Implementation Priority +| Rank | Fix | Type | Metrics | Risk | Score rationale | +|------|-----|------|---------|------|-----------------| +| 1 | [name] | prompt | M1, M2 | Low | [one-line: why this ranks here] | +| 2 | ... | ... | ... | ... | ... | + +--- + +## Skill: [insight ID(s)] — [title] +**Summary:** [one-line description of what the skill addresses] +**Verdict:** `prompt fix` | `code fix` | `discard` +**Classification path:** [which branch of the decision tree — e.g., "Agent has tools and data but acts wrong → prompt fix"] +**Rationale:** [why this verdict — reference specific trace evidence from insights] +**Risk:** None | Low | Medium | High — [one-sentence justification] +**Risk mitigation:** [for Medium/High only — what to watch for or how to preserve working cases] +**Recommendation:** [specific change to make] +**Files to modify:** [list of files, for code fixes] +**Metric link:** [which metrics would move, with baseline values] +**Already-handled check:** [what you grepped, what existing prompt text you found, verdict] + +--- +[repeat for each insight] + +## Consolidated Prompt Skills +[After all per-insight entries, list the final merged prompt skill texts in priority order, ready for Stage 7 to implement] + +## Monitor Items (Non-Actionable Only) +[Only insights that were triaged as genuinely non-actionable — e.g., the agent cannot change this behavior, or the insight is noise. Unmeasurable insights that are still real problems should appear in the priority list above, NOT here.] +``` + +Group related insights under cluster headings when they address the same underlying behavior. For merged insights, list all constituent insight IDs in the heading. + +## Outputs + +- `eval/action_plan.md` diff --git a/ace/cli/skills/kayba-pipeline/stage-6-hitl/SKILL.md b/ace/cli/skills/kayba-pipeline/stage-6-hitl/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..3f7820b0c257a3a5a153ae53aac16e183b0e80f3 --- /dev/null +++ b/ace/cli/skills/kayba-pipeline/stage-6-hitl/SKILL.md @@ -0,0 +1,258 @@ +--- +name: kayba-stage-6-hitl +description: Human-In-The-Loop gate that presents the action plan with full context, collects an informed approval/modification/rejection decision, and records the outcome. Trigger when the user says "run stage 6", "HITL review", "approve action plan", or when invoked by the kayba-pipeline orchestrator. Requires eval/action_plan.md and eval/baseline_metrics.md to exist. +--- + +# Stage 6: Human-In-The-Loop Gate + +Present the action plan with enough context for an informed decision, collect the user's approval, and record the outcome. + +The goal is not rubber-stamping. The user must receive enough information to genuinely evaluate, modify, or reject the plan -- even if they have not seen Stages 1-5. + +## Inputs + +- `eval/action_plan.md` -- the prioritized action plan from Stage 5 +- `eval/baseline_metrics.md` -- the evaluation rubric with baseline values +- `eval/baseline_metrics.json` -- raw metric data (for exact numerator/denominator counts) +- `eval/stage1_insights_summary.md` -- original insights (for trace evidence references) + +Read all four files before starting. + +## Process + +### 1. Build the executive summary + +Compute and present the following counts from the action plan: + +- Total insights analyzed (raw count before deduplication) +- Distinct actionable items after deduplication +- Breakdown: prompt fixes, code fixes, discarded +- Discard rate with one-line reason per discard (e.g., "5ac7f4ce: efficiency optimization, conflicts with turn discipline constraint") + +Format: + +``` +EXECUTIVE SUMMARY +----------------- +Insights analyzed: 19 (raw) -> 12 distinct after dedup +Actionable: 9 (8 prompt fixes, 1 code fix) +Discarded: 3 (reasons listed below) + +Discards: + - 5ac7f4ce (Upfront Info Collection): conflicts with higher-priority turn discipline + - fe2d51cb (Proactive Reservation Lookup): already default behavior, no failure evidence + - 1fa1b826 (Cancellation Denial Enumeration): subsumed into cancellation checklist +``` + +### 2. Present the top 3 highest-impact changes + +For each of the top 3 fixes by priority, present: + +**Before/after behavior** -- use concrete examples from actual traces referenced in the insights. Quote the specific agent behavior that was wrong (before) and describe what the agent should do instead (after). Reference the trace task ID. + +**Target metric delta** -- which metric(s) this fix targets, the current baseline value, and the expected direction. Do not fabricate precise target numbers. Use the format: "M1: 41.4% -> higher (target: 90%+)" only when the action plan provides a target; otherwise use "M1: 41.4% -> up". + +**Risk rating** -- assess each fix: +- `Low` -- additive prompt instruction, no behavioral side effects expected +- `Medium` -- changes existing behavior, could affect adjacent workflows +- `High` -- modifies code/infrastructure, or could degrade a metric while improving another + +Format each as a numbered block: + +``` +#1: Turn Discipline (covers 55c00c40, d9683144) + Type: prompt fix + Metrics: M1 (41.4% -> up), M2 (20.7% -> up) + Risk: Low + + BEFORE (task_1, task_5, task_7, ...): + Agent batches 2-3 tool calls per turn (e.g., get_reservation + get_flight_status + in a single response). Also includes user-facing text alongside tool calls. + + AFTER: + Exactly one tool call per response. No user-facing content in tool-call turns. + Agent processes each result before making the next call. +``` + +### 3. Present the full prioritized fix list + +Display all non-discarded fixes in a table: + +``` +| Priority | Fix Name | Type | Target Metrics | Risk | Effort | +|----------|-----------------------------------|------------|-----------------|--------|--------| +| 1 | Turn Discipline | prompt fix | M1, M2 | Low | Low | +| 2 | Post-Confirmation Execution | prompt fix | M3 | Low | Low | +| 3 | Cancellation Checklist | prompt fix | M5 | Low | Low | +| ... | ... | ... | ... | ... | ... | +``` + +Effort ratings: +- `Low` -- single prompt addition, under 5 lines +- `Medium` -- multiple prompt additions or minor code change +- `High` -- significant code changes, new metric implementation, or architectural changes + +### 4. Present "What we are NOT fixing and why" + +List every discarded insight with: +- Insight ID and name +- One-line reason for discard +- What would change your mind (under what conditions should this be revisited) + +This section exists so the user can override a discard if they disagree. + +### 5. Flag small-sample and low-confidence items + +Any metric with denominator < 5 must be explicitly called out: + +``` +LOW-CONFIDENCE METRICS (small sample size): + - M5 (Cancellation Policy Compliance): based on 2 observations -- directional only + - M6 (Compensation Execution Rate): based on 1 observation -- directional only + +Fixes targeting these metrics (Cancellation Checklist, Compensation Rules) are +still recommended because the policy violations are clear from trace evidence, +but the measured improvement may not be statistically meaningful until the +trace corpus grows. +``` + +Also flag any fix where the action plan notes uncertainty or partial evidence. + +### 6. Show the insight-to-fix traceability chain + +For each fix, present the chain: insight -> metric -> fix -> expected improvement. This can be a compact list or a table. The purpose is to let the user verify that nothing was lost or invented between stages. + +``` +TRACEABILITY: + 55c00c40 (Tool Call Discipline) -> M1, M2 -> Skill 1 (Turn Discipline) -> M1 up, M2 up + 6ea141e1 (Execution Discipline) -> M3 -> Skill 2 (Post-Confirmation) -> M3 up + 0f4a952b + 6ce88ebb (Cancellation) -> M5 -> Skill 3 (Cancellation Checklist) -> M5 up + ... +``` + +### 7. Collect the decision + +Present exactly three options: + +``` +OPTIONS: + [A] Approve all -- implement all 9 fixes as described + [B] Approve with modifications -- review each fix individually + [C] Reject -- return to Stage 5 with feedback +``` + +Use the appropriate mechanism to collect the user's choice (direct question or AskUserQuestion if available). + +#### If the user selects [A] Approve all + +Record the decision and proceed. No further interaction needed. + +#### If the user selects [B] Approve with modifications + +Walk through each fix individually, in priority order. For each fix, present: +- The fix name, type, and target metrics +- The recommended prompt/code change (quote the exact text from the action plan) +- Risk and effort ratings + +Then ask: "Approve / Skip / Modify?" + +- **Approve** -- keep as-is +- **Skip** -- remove from the plan, record reason +- **Modify** -- ask the user what to change, record the original and the modification + +After walking through all fixes, present a summary of changes: +- Fixes approved as-is: N +- Fixes skipped: M (list with reasons) +- Fixes modified: K (list with what changed) + +Ask for final confirmation: "Proceed with this modified plan?" + +Then update `eval/action_plan.md`: +- Remove skipped fixes (move to a "Skipped by HITL" section at the bottom with reasons) +- Update modified fixes with the user's changes, preserving the original recommendation in a "Original recommendation" sub-field +- Add a header note: "Modified during HITL review on [date]. See eval/stage6_decision.md for details." + +#### If the user selects [C] Reject + +Ask the user for specific feedback: +- What was wrong with the plan? +- Which insights or metrics should be reconsidered? +- Any new constraints or priorities? + +Record the feedback in `eval/stage6_decision.md` and signal that Stage 5 should be re-run with the user's feedback incorporated. + +## Output format + +### eval/stage6_decision.md + +Write this file regardless of which option was selected. + +```markdown +# Stage 6: HITL Decision Record + +## Date +[timestamp] + +## Decision +[Approve all | Approve with modifications | Reject] + +## What was presented +- Total insights: N (M distinct after dedup) +- Actionable fixes: X (Y prompt, Z code) +- Discarded: W +- Metrics: [list metric IDs and baselines] +- Low-confidence flags: [list metrics with small denominators] + +## Top 3 changes presented +1. [fix name] -- [type] -- targets [metrics] -- risk [rating] +2. ... +3. ... + +## Decision details + +### If Approve all: +User approved all N fixes without modification. +Reasoning: [any reasoning the user provided, or "No additional reasoning provided"] + +### If Approve with modifications: +| Fix | Original Status | Decision | Reason | +|-----|----------------|----------|--------| +| Turn Discipline | Priority 1 | Approved | -- | +| Compensation Rules | Priority 5 | Modified | User changed wording to... | +| Cabin Change Rules | Priority 8 | Skipped | User considers low priority | + +Modifications detail: +- [Fix name]: Original: "..." -> Modified: "..." -- User rationale: "..." + +### If Reject: +User feedback: [verbatim feedback] +Specific concerns: [list] +Re-run instructions for Stage 5: [what to change] + +## Traceability snapshot +[Copy of the traceability chain from step 6, so the decision record is self-contained] +``` + +### eval/action_plan.md (updated, only if modifications were made) + +If the user selected [B] and made changes: +- Add a modification header at the top of the file +- Update individual fix entries with user changes +- Move skipped fixes to a "Skipped by HITL" section +- Preserve original recommendations as sub-fields for auditability + +## Rules + +- Do NOT auto-approve. The entire point of this stage is human judgment. +- Do NOT summarize so aggressively that the user cannot evaluate. When in doubt, include more context. +- Do NOT proceed to Stage 7 until a clear approval (full or modified) is recorded. +- Do NOT modify `eval/action_plan.md` unless the user explicitly requests modifications. +- Do NOT skip the small-sample warnings. If M5 has denominator 2 and M6 has denominator 1, the user must know this. +- Do NOT fabricate target metric values. Use targets from the action plan when available; otherwise state direction only. +- Always present the "What we are NOT fixing" section. Omitting discards hides information the user needs. +- If the user asks clarifying questions, answer them fully before re-presenting the decision options. + +## Outputs + +- `eval/stage6_decision.md` -- full record of what was presented, decided, and why +- `eval/action_plan.md` -- updated only if the user selected "Approve with modifications" diff --git a/ace/cli/skills/kayba-pipeline/stage-7-fixer/SKILL.md b/ace/cli/skills/kayba-pipeline/stage-7-fixer/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..809adf043eadf4b9086838351d3fc5fbce8a72ca --- /dev/null +++ b/ace/cli/skills/kayba-pipeline/stage-7-fixer/SKILL.md @@ -0,0 +1,191 @@ +--- +name: kayba-stage-7-fixer +description: Implement the approved fixes from the action plan and log all changes. Trigger when the user says "run stage 7", "implement fixes", "apply action plan", or when invoked by the kayba-pipeline orchestrator. Requires eval/action_plan.md to exist. +--- + +# Stage 7: Fix Implementation + +Implement every non-discarded fix from the approved action plan. + +## Inputs + +- `eval/action_plan.md` -- the approved action plan from Stage 5 (possibly modified during HITL in Stage 6) +- `eval/stage6_decision.md` -- if it exists, the HITL decision record from Stage 6 (contains user modifications) +- `eval/baseline_metrics.json` -- the pre-fix baseline metrics from Stage 3 (for reference in changes log) + +Read the action plan and stage6 decision (if present) before starting. + +## Pre-flight: Git Safety Checkpoint + +Before making ANY changes to source files: + +1. Run `git status` to confirm the working tree state +2. Create a safety commit or stash: + ``` + git stash push -m "pre-pipeline-fixes-$(date +%Y%m%d-%H%M%S)" + ``` + If there are no uncommitted changes to stash, create a lightweight tag instead: + ``` + git tag pre-pipeline-fixes-$(date +%Y%m%d-%H%M%S) + ``` +3. Record the stash ref or tag name in `eval/changes_log.md` under a "Rollback" section so the user can restore if needed + +This ensures every fix is reversible with a single `git stash pop` or `git checkout`. + +## Pre-flight: HITL Modification Check + +If `eval/stage6_decision.md` exists: + +1. Read it and identify any items the user modified, added, or re-prioritized during Stage 6 +2. Build a set of `HITL_MODIFIED_IDS` -- the insight/skill IDs that the user changed +3. When logging each fix later, tag modified items with `[HITL-MODIFIED]` in the changes log so reviewers know which fixes reflect user judgment vs. the original pipeline output + +If the file does not exist, assume no HITL modifications were made. + +## Pre-flight: Conflict Scan + +Before implementing any fixes, scan the action plan for potential conflicts: + +1. Build a map of `file_path -> [fix IDs that touch it]` +2. If two or more fixes modify the same file, flag them as **co-located** +3. If two or more fixes modify the same section (within ~20 lines of each other), flag them as **overlapping** +4. For overlapping fixes: plan to apply them sequentially in priority order, re-reading the file between each edit to ensure the second fix still makes sense on top of the first +5. Log any detected conflicts at the top of `eval/changes_log.md` under a "Conflict Notes" section + +## Process + +Work through the action plan in priority order. For each non-discarded fix: + +### 1. Understand the fix + +- Read the recommendation carefully +- Read the referenced files in the codebase +- Understand the surrounding code before making changes +- Check if this fix was flagged as co-located or overlapping in the conflict scan. If overlapping with a previously-applied fix, re-read the target file to see the current state after prior edits + +### 2. Implement the change + +**For code fixes:** +- Find the relevant files +- Make the minimal, targeted change described in the recommendation +- Do not refactor surrounding code unless the fix obviously breaks without light adjacent cleanup (e.g., an import is missing, a variable was renamed). If you make adjacent cleanup, log it explicitly as "adjacent cleanup" in the change entry +- Do not add features beyond what was recommended + +**For prompt fixes:** +- Find the system prompt file (use domain context from Stage 2 if needed) +- Add the recommended instruction at the appropriate location +- Do not rewrite existing prompt text unless the recommendation explicitly says to + +### 3. Log the change + +Append to `eval/changes_log.md`: + +```markdown +## Fix N: [skill/insight name] [HITL-MODIFIED if applicable] +**Type:** code fix | prompt fix +**Verdict from action plan:** [quote the recommendation] +**Files modified:** +- `path/to/file.py` -- [what changed and why] +**Before:** +\``` +[relevant snippet before change] +\``` +**After:** +\``` +[relevant snippet after change] +\``` +**Linked metrics:** [which metrics this should improve] +**Conflict notes:** [if this fix overlapped with another, note it here; otherwise "none"] +``` + +### 4. Handle uncertainty (NEEDS REVIEW workflow) + +If a fix requires changes you are unsure about: + +1. Do NOT implement it +2. Log it as `NEEDS REVIEW` in the changes log with: + - What specifically is unclear + - What information would resolve the ambiguity + - The files and lines you examined +3. **Continue to the next fix** -- do not block the pipeline +4. At the end of all fixes, collect all NEEDS REVIEW items into a dedicated section (see Output format below). The pipeline does NOT stop; these items are presented to the user after all other fixes are applied. + +## Post-Fix: Next Steps (Do NOT Re-run Baselines) + +Do NOT re-run `compute_baselines.py` as part of this stage. The baseline metrics were computed against the original traces, which reflect old agent behavior. Re-running against the same traces will show zero movement for prompt-only fixes and is misleading. + +Instead, after all fixes are applied, include a **Next Steps** section in the changes log that tells the user: + +1. Generate new traces by running the agent with the updated prompts/code +2. Then re-run baselines against the new traces: + ```bash + python eval/compute_baselines.py --traces-dir <new_traces_folder> --output eval/post_fix_metrics.json + ``` +3. Compare `eval/post_fix_metrics.json` against `eval/baseline_metrics.json` to measure actual improvement + +## Rules + +- Do NOT modify trace files +- Do NOT make changes beyond what the action plan recommends (except adjacent cleanup logged explicitly) +- Make minimal, targeted changes -- don't clean up or refactor surrounding code +- If the action plan says "discard", skip that entry entirely +- You MAY write `eval/changes_log.md` as the primary output +- Do NOT run `eval/compute_baselines.py` -- baselines should only be re-computed after new traces are generated with the updated agent + +## Output format + +Write `eval/changes_log.md`: + +```markdown +# Changes Log + +## Rollback +- **Safety ref:** `git stash` ref or tag name +- **To undo all fixes:** `git stash pop` or `git checkout <tag>` + +## Conflict Notes +- [any file/region conflicts detected, or "No conflicts detected"] + +## Summary +- Code fixes applied: N +- Prompt fixes applied: M +- Skipped / needs review: K +- HITL-modified items: J + +--- + +## Fix 1: [skill name] +... + +## Fix 2: [skill name] +... + +--- + +## Needs Review +[Collected list of all NEEDS REVIEW items with context, or "None -- all fixes applied successfully"] + +For each NEEDS REVIEW item: +- **Fix N: [skill name]** +- **What is unclear:** [specific ambiguity] +- **What would resolve it:** [information needed] +- **Files examined:** [paths and lines] + +--- + +## Next Steps + +To measure actual improvement: +1. Generate new traces by running the agent with the updated prompts/code +2. Re-run baselines: +\```bash +python eval/compute_baselines.py --traces-dir <new_traces_folder> --output eval/post_fix_metrics.json +\``` +3. Compare `eval/post_fix_metrics.json` against `eval/baseline_metrics.json` to measure metric deltas +``` + +## Outputs + +- `eval/changes_log.md` -- full log of all changes, conflicts, NEEDS REVIEW items, and next steps +- The actual code/prompt changes in the repository +- A git stash or tag for rollback diff --git a/ace/core/__init__.py b/ace/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b530090af68d2999abdd107d0aead6f6d1b1a108 --- /dev/null +++ b/ace/core/__init__.py @@ -0,0 +1,51 @@ +"""Core data types for the ACE framework.""" + +from .context import ACESample, ACEStepContext, SkillbookView +from .environments import EnvironmentResult, Sample, SimpleEnvironment, TaskEnvironment +from .insight_source import ( + TRACE_IDENTITY_METADATA_KEY, + InsightSource, + TraceIdentity, +) +from .outputs import ( + AgentOutput, + ExtractedLearning, + ReflectorOutput, + SkillManagerOutput, +) +from .skillbook import ( + OperationType, + Skill, + Skillbook, + SimilarityDecision, + UpdateBatch, + UpdateOperation, +) + +__all__ = [ + # Skillbook types + "OperationType", + "Skill", + "Skillbook", + "SimilarityDecision", + "UpdateBatch", + "UpdateOperation", + # Outputs + "AgentOutput", + "ExtractedLearning", + "ReflectorOutput", + "SkillManagerOutput", + # Context + "ACESample", + "ACEStepContext", + "SkillbookView", + # Environments + "EnvironmentResult", + "Sample", + "SimpleEnvironment", + "TaskEnvironment", + # Provenance + "InsightSource", + "TraceIdentity", + "TRACE_IDENTITY_METADATA_KEY", +] diff --git a/ace/core/context.py b/ace/core/context.py new file mode 100644 index 0000000000000000000000000000000000000000..838616f6e50b77f2096cd33b965ce965cf271231 --- /dev/null +++ b/ace/core/context.py @@ -0,0 +1,131 @@ +"""Core types for the ACE pipeline: ACESample, SkillbookView, ACEStepContext.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Iterator, Literal, Protocol, runtime_checkable + +from pipeline import StepContext + +from .outputs import AgentOutput, ReflectorOutput +from .skillbook import Skill, Skillbook, UpdateBatch + +# --------------------------------------------------------------------------- +# ACESample — structural protocol for step access +# --------------------------------------------------------------------------- + + +@runtime_checkable +class ACESample(Protocol): + """Minimal interface that Sample satisfies. + + Steps access ``ctx.sample.question`` uniformly. This protocol makes + the duck typing explicit and type-safe — ``Sample`` satisfies it + structurally without inheriting from it. + """ + + @property + def question(self) -> str: ... + + @property + def context(self) -> str: ... + + @property + def ground_truth(self) -> str | None: ... + + @property + def metadata(self) -> dict: ... + + +# --------------------------------------------------------------------------- +# SkillbookView — read-only projection +# --------------------------------------------------------------------------- + + +class SkillbookView: + """Read-only projection of a Skillbook. + + Wraps a ``Skillbook`` and exposes only read methods. Write methods + don't exist on this class — calling them raises ``AttributeError`` + at runtime and a type error at check time. + + Safe to place on a frozen ``ACEStepContext``. Steps that need to + write to the skillbook receive the real ``Skillbook`` via constructor + injection. + """ + + __slots__ = ("_sb",) + + def __init__(self, skillbook: Skillbook) -> None: + self._sb = skillbook + + # -- Read methods delegated to the underlying Skillbook -- + + def as_prompt(self) -> str: + """Return the markdown-formatted skillbook for LLM consumption.""" + return self._sb.as_prompt() + + def get_skill(self, skill_id: str) -> Skill | None: + """Look up a skill by ID.""" + return self._sb.get_skill(skill_id) + + def skills(self, include_invalid: bool = False) -> list[Skill]: + """Return all active skills (or all including invalid).""" + return self._sb.skills(include_invalid=include_invalid) + + def stats(self) -> dict[str, object]: + """Return skillbook statistics.""" + return self._sb.stats() + + def __len__(self) -> int: + return len(self._sb.skills()) + + def __iter__(self) -> Iterator[Skill]: + return iter(self._sb.skills()) + + def __repr__(self) -> str: + return f"SkillbookView({len(self)} skills)" + + +# --------------------------------------------------------------------------- +# ACEStepContext — immutable context for the ACE pipeline +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class ACEStepContext(StepContext): + """Immutable context carrying all step-to-step data for the ACE pipeline. + + The pipeline engine only knows about ``sample`` and ``metadata``; all + ACE-specific fields live here. + + The ``skillbook`` field is a ``SkillbookView`` (read-only). Steps that + need to write to the skillbook receive the real ``Skillbook`` via + constructor injection. + + The ``trace`` field holds the raw execution record from any external + system — a browser-use ``AgentHistoryList``, a LangChain result dict, + a Claude Code transcript, or any arbitrary Python object. It has no + enforced schema. The Reflector receives the raw trace and is + responsible for making sense of it. + """ + + # -- Mode -- + mode: Literal["online", "offline"] = "online" + + # -- Domain fields -- + skillbook: SkillbookView | None = None + trace: object | None = None + agent_output: AgentOutput | None = None + reflections: tuple[ReflectorOutput, ...] = () + skill_manager_output: UpdateBatch | None = None + # Skills rendered into the Agent's prompt this run. Downstream roles + # (Reflector/RR, SkillManager) use this as attribution scope. + injected_skill_ids: tuple[str, ...] = () + + # -- Progress tracking -- + epoch: int = 1 + total_epochs: int = 1 + step_index: int = 0 + total_steps: int | None = None + global_sample_index: int = 0 diff --git a/ace/core/environments.py b/ace/core/environments.py new file mode 100644 index 0000000000000000000000000000000000000000..58a54190a54b4d2616171fd6b378b2148d3649b7 --- /dev/null +++ b/ace/core/environments.py @@ -0,0 +1,63 @@ +"""Data contracts for the ACE pipeline — samples, environments, and step results.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Dict, Optional + +from .outputs import AgentOutput + + +@dataclass +class Sample: + """Single task instance presented to ACE.""" + + question: str + context: str = "" + ground_truth: Optional[str] = None + metadata: Dict[str, object] = field(default_factory=dict) + id: Optional[str] = None + + +@dataclass +class EnvironmentResult: + """Feedback returned by the task environment after evaluating agent output.""" + + feedback: str + ground_truth: Optional[str] + metrics: Dict[str, float] = field(default_factory=dict) + + +class TaskEnvironment(ABC): + """Abstract interface for evaluating agent outputs.""" + + @abstractmethod + def evaluate(self, sample: Sample, agent_output: AgentOutput) -> EnvironmentResult: + """Evaluate the agent's output for a given sample.""" + + +class SimpleEnvironment(TaskEnvironment): + """Built-in environment that checks if ground truth appears in the answer.""" + + def evaluate(self, sample: Sample, agent_output: AgentOutput) -> EnvironmentResult: + if not sample.ground_truth: + return EnvironmentResult( + feedback="No ground truth provided", + ground_truth=None, + metrics={"correct": 0.0}, + ) + + answer = agent_output.final_answer.lower() + truth = sample.ground_truth.lower() + is_correct = truth in answer + + return EnvironmentResult( + feedback=( + "Correct!" + if is_correct + else f"Incorrect. Expected: {sample.ground_truth}" + ), + ground_truth=sample.ground_truth, + metrics={"correct": 1.0 if is_correct else 0.0}, + ) diff --git a/ace/core/insight_source.py b/ace/core/insight_source.py new file mode 100644 index 0000000000000000000000000000000000000000..654be456e04589f9e0165522c6cfce827ee058f3 --- /dev/null +++ b/ace/core/insight_source.py @@ -0,0 +1,284 @@ +"""Typed provenance models for skillbook insight sources.""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import dataclass, field +from typing import Any, Iterable, Mapping + +TRACE_IDENTITY_METADATA_KEY = "ace.trace_identity" + + +def make_trace_uid(source_system: str, trace_id: str) -> str: + """Return a stable composite identifier for a trace.""" + return f"{source_system}:{trace_id}" + + +def fingerprint_trace(value: Any) -> str: + """Return a stable content fingerprint for a trace-like object.""" + try: + payload = json.dumps(value, ensure_ascii=False, sort_keys=True, default=str) + except TypeError: + payload = repr(value) + return hashlib.sha256(payload.encode("utf-8")).hexdigest() + + +def _coerce_str(value: Any) -> str | None: + if value is None: + return None + text = str(value).strip() + return text or None + + +def _legacy_trace_id(payload: Mapping[str, Any]) -> str | None: + for key in ("sample_id", "item_id", "task_id", "id"): + value = _coerce_str(payload.get(key)) + if value is not None: + return value + return None + + +def _safe_int(value: Any) -> int | None: + if isinstance(value, int): + return value + if isinstance(value, str) and value.strip().lstrip("-").isdigit(): + return int(value) + return None + + +@dataclass +class TraceIdentity: + """Stable identity for a trace across storage and UI layers.""" + + source_system: str + trace_id: str + display_name: str | None = None + trace_uid: str | None = None + + def __post_init__(self) -> None: + self.source_system = self.source_system.strip() or "local" + self.trace_id = self.trace_id.strip() + if not self.trace_uid: + self.trace_uid = make_trace_uid(self.source_system, self.trace_id) + if self.display_name is not None: + self.display_name = self.display_name.strip() or None + + @classmethod + def from_dict(cls, payload: Mapping[str, Any]) -> "TraceIdentity": + trace_uid = _coerce_str(payload.get("trace_uid")) + source_system = _coerce_str(payload.get("source_system")) + trace_id = _coerce_str(payload.get("trace_id")) + if ( + trace_uid + and (source_system is None or trace_id is None) + and ":" in trace_uid + ): + inferred_source, inferred_id = trace_uid.split(":", 1) + source_system = source_system or inferred_source + trace_id = trace_id or inferred_id + if trace_id is None: + legacy_id = _legacy_trace_id(payload) + if legacy_id is not None: + trace_id = legacy_id + if source_system is None: + source_system = "legacy" + if trace_id is None: + trace_id = fingerprint_trace(dict(payload)) + return cls( + source_system=source_system, + trace_id=trace_id, + display_name=_coerce_str(payload.get("display_name")) + or _legacy_trace_id(payload) + or trace_id, + trace_uid=trace_uid, + ) + + def to_dict(self) -> dict[str, Any]: + data: dict[str, Any] = {} + data["trace_uid"] = self.trace_uid + data["source_system"] = self.source_system + data["trace_id"] = self.trace_id + if self.display_name is not None: + data["display_name"] = self.display_name + return data + + +@dataclass +class InsightSource: + """A single provenance record describing how a trace informed a skill.""" + + trace_uid: str + source_system: str + trace_id: str + display_name: str | None = None + relation: str | None = None + sample_question: str | None = None + epoch: int | None = None + operation_type: str | None = None + error_identification: str | None = None + learning_text: str | None = None + + @classmethod + def from_dict(cls, payload: Mapping[str, Any]) -> "InsightSource": + identity = TraceIdentity.from_dict(payload) + return cls( + trace_uid=identity.trace_uid + or make_trace_uid(identity.source_system, identity.trace_id), + source_system=identity.source_system, + trace_id=identity.trace_id, + display_name=identity.display_name, + relation=_coerce_str(payload.get("relation")), + sample_question=_coerce_str(payload.get("sample_question")), + epoch=_safe_int(payload.get("epoch")), + operation_type=_coerce_str(payload.get("operation_type")), + error_identification=_coerce_str(payload.get("error_identification")), + learning_text=_coerce_str(payload.get("learning_text")), + ) + + def to_dict(self) -> dict[str, Any]: + data: dict[str, Any] = { + "trace_uid": self.trace_uid, + "source_system": self.source_system, + "trace_id": self.trace_id, + } + if self.display_name is not None: + data["display_name"] = self.display_name + if self.relation is not None: + data["relation"] = self.relation + if self.sample_question is not None: + data["sample_question"] = self.sample_question + if self.epoch is not None: + data["epoch"] = self.epoch + if self.operation_type is not None: + data["operation_type"] = self.operation_type + if self.error_identification is not None: + data["error_identification"] = self.error_identification + if self.learning_text is not None: + data["learning_text"] = self.learning_text + return data + + +def coerce_trace_identity(value: TraceIdentity | Mapping[str, Any]) -> TraceIdentity: + if isinstance(value, TraceIdentity): + return value + return TraceIdentity.from_dict(value) + + +def coerce_insight_source(value: InsightSource | Mapping[str, Any]) -> InsightSource: + if isinstance(value, InsightSource): + return value + return InsightSource.from_dict(value) + + +def coerce_insight_sources(value: Any) -> list[InsightSource]: + if value is None: + return [] + if isinstance(value, InsightSource): + return [value] + if isinstance(value, Mapping): + return [InsightSource.from_dict(value)] + if isinstance(value, Iterable) and not isinstance(value, (str, bytes)): + sources: list[InsightSource] = [] + for item in value: + if isinstance(item, InsightSource): + sources.append(item) + elif isinstance(item, Mapping): + sources.append(InsightSource.from_dict(item)) + return sources + return [] + + +def infer_trace_identity( + *, + trace: Any = None, + sample: Any = None, + metadata: Mapping[str, Any] | None = None, + default_source_system: str = "local", +) -> TraceIdentity: + """Infer the best available stable trace identity.""" + if metadata: + raw_metadata_identity = metadata.get( + TRACE_IDENTITY_METADATA_KEY + ) or metadata.get("trace_identity") + if isinstance(raw_metadata_identity, (TraceIdentity, Mapping)): + return coerce_trace_identity(raw_metadata_identity) + + sample_metadata = getattr(sample, "metadata", None) + if isinstance(sample_metadata, Mapping): + raw_sample_identity = sample_metadata.get( + TRACE_IDENTITY_METADATA_KEY + ) or sample_metadata.get("trace_identity") + if isinstance(raw_sample_identity, (TraceIdentity, Mapping)): + return coerce_trace_identity(raw_sample_identity) + trace_id = _coerce_str(sample_metadata.get("trace_id")) or _legacy_trace_id( + sample_metadata + ) + source_system = _coerce_str(sample_metadata.get("source_system")) + display_name = _coerce_str(sample_metadata.get("display_name")) + trace_uid = _coerce_str(sample_metadata.get("trace_uid")) + if trace_uid or trace_id: + if trace_id is None and trace_uid and ":" in trace_uid: + inferred_source, inferred_id = trace_uid.split(":", 1) + source_system = source_system or inferred_source + trace_id = inferred_id + if trace_id is not None: + return TraceIdentity( + source_system=source_system or "sample", + trace_id=trace_id, + display_name=display_name + or _legacy_trace_id(sample_metadata) + or _coerce_str(getattr(sample, "id", None)) + or trace_id, + trace_uid=trace_uid, + ) + + if isinstance(trace, Mapping): + raw_identity = trace.get(TRACE_IDENTITY_METADATA_KEY) or trace.get( + "trace_identity" + ) + if isinstance(raw_identity, (TraceIdentity, Mapping)): + return coerce_trace_identity(raw_identity) + + if any(key in trace for key in ("trace_uid", "trace_id", "source_system")): + return TraceIdentity.from_dict(trace) + + legacy_id = _legacy_trace_id(trace) + if legacy_id is not None: + return TraceIdentity( + source_system=_coerce_str(trace.get("source_system")) + or default_source_system, + trace_id=legacy_id, + display_name=_coerce_str(trace.get("display_name")) + or _coerce_str(trace.get("question")) + or legacy_id, + ) + + sample_id = _coerce_str(getattr(sample, "id", None)) + if sample_id is not None: + return TraceIdentity( + source_system="sample", + trace_id=sample_id, + display_name=sample_id, + ) + + fallback_source = ( + trace if trace is not None else getattr(sample, "question", sample) + ) + fallback_id = fingerprint_trace(fallback_source) + + display_name = None + if isinstance(trace, Mapping): + display_name = _coerce_str(trace.get("question")) or _coerce_str( + trace.get("sample_id") + ) + if display_name is None: + display_name = _legacy_trace_id(trace) + if display_name is None: + display_name = _coerce_str(getattr(sample, "question", None)) or sample_id + + return TraceIdentity( + source_system=default_source_system, + trace_id=fallback_id, + display_name=display_name or fallback_id, + ) diff --git a/ace/core/metered_model.py b/ace/core/metered_model.py new file mode 100644 index 0000000000000000000000000000000000000000..39f988587b5343a38e361746f535acf9cef1ad90 --- /dev/null +++ b/ace/core/metered_model.py @@ -0,0 +1,69 @@ +"""``MeteredModel`` — a pydantic-ai ``WrapperModel`` that fires a usage hook. + +Wraps any pydantic-ai ``Model`` and invokes ``callback(usage, model_name)`` +after every completed ``request`` / ``request_stream`` call. Exceptions raised +inside the callback are caught and logged so metering failures never crash the +pipeline. + +Using a ``WrapperModel`` gives metering at the framework's own boundary: +every agent-driven LLM call — orchestrator turns, sub-agent runs, tool-call +follow-ups — is metered from one place, with no per-call-site plumbing. +""" + +from __future__ import annotations + +import logging +from typing import Any, AsyncIterator, Callable +from contextlib import asynccontextmanager + +from pydantic_ai.messages import ModelMessage, ModelResponse +from pydantic_ai.models import Model, ModelRequestParameters, StreamedResponse +from pydantic_ai.models.wrapper import WrapperModel +from pydantic_ai.settings import ModelSettings +from pydantic_ai.tools import RunContext +from pydantic_ai.usage import RequestUsage + +logger = logging.getLogger(__name__) + +UsageCallback = Callable[[RequestUsage, str], None] + + +class MeteredModel(WrapperModel): + """Wraps a ``Model`` and fires ``callback(usage, model_name)`` per request.""" + + def __init__(self, wrapped: Model, callback: UsageCallback) -> None: + super().__init__(wrapped) + self._callback = callback + + async def request( + self, + messages: list[ModelMessage], + model_settings: ModelSettings | None, + model_request_parameters: ModelRequestParameters, + ) -> ModelResponse: + response = await self.wrapped.request( + messages, model_settings, model_request_parameters + ) + self._emit(response.usage) + return response + + @asynccontextmanager + async def request_stream( + self, + messages: list[ModelMessage], + model_settings: ModelSettings | None, + model_request_parameters: ModelRequestParameters, + run_context: RunContext[Any] | None = None, + ) -> AsyncIterator[StreamedResponse]: + async with self.wrapped.request_stream( + messages, model_settings, model_request_parameters, run_context + ) as stream: + yield stream + # Streamed usage is only final once iteration completes. + self._emit(stream.usage()) + + def _emit(self, usage: RequestUsage) -> None: + try: + self._callback(usage, self.model_name) + except Exception: + logger.exception("usage_callback failed") diff --git a/ace/core/outputs.py b/ace/core/outputs.py new file mode 100644 index 0000000000000000000000000000000000000000..54a4bac8158f2005a99e0738c8e736205628e7cc --- /dev/null +++ b/ace/core/outputs.py @@ -0,0 +1,107 @@ +"""Output types produced by ACE roles.""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from .skillbook import UpdateBatch + + +class AgentOutput(BaseModel): + """Output from the Agent role containing reasoning and answer.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + reasoning: str = Field(..., description="Step-by-step reasoning process") + final_answer: str = Field(..., description="The final answer to the question") + skill_ids: List[str] = Field( + default_factory=list, description="IDs of strategies cited in reasoning" + ) + raw: Dict[str, Any] = Field( + default_factory=dict, description="Raw LLM response data" + ) + trace_context: Optional[Any] = Field( + default=None, + exclude=True, + description="Pre-built TraceContext from integration (bypasses auto-detection)", + ) + + +class ExtractedLearning(BaseModel): + """A single learning extracted by the Reflector from task execution.""" + + learning: str = Field(..., description="The extracted learning or insight") + evidence: str = Field( + default="", + description=( + "Specific traces/items where this pattern was observed. " + "Cite task IDs or item indices, e.g. 'task_2, task_16, task_29'." + ), + ) + justification: str = Field( + default="", + description=( + "Why this is worth remembering: how many traces exhibited this pattern, " + "whether it's recurring or a one-off, and why it generalizes beyond these examples." + ), + ) + + +class ReflectorOutput(BaseModel): + """Output from the Reflector role containing pure analysis. + + Reflector reports what it found; downstream (SkillManager) decides how + to act. No tagging fields, no prescriptive output. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + reasoning: str = Field(..., description="Overall reasoning about the outcome") + error_identification: str = Field( + default="", description="Description of what went wrong (if applicable)" + ) + root_cause_analysis: str = Field( + default="", description="Analysis of why errors occurred" + ) + correct_approach: str = Field( + ..., description="What the correct approach should be" + ) + key_insight: str = Field( + ..., description="The main lesson learned from this iteration" + ) + raw: Dict[str, Any] = Field( + default_factory=dict, description="Raw LLM response data" + ) + + +class SkillManagerOutput(BaseModel): + """Output from the SkillManager role containing skillbook update operations. + + Accepts both nested ``{"update": {"reasoning": ..., "operations": [...]}}`` + and the flat shape the LLM actually returns: + ``{"reasoning": ..., "operations": [...]}``. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + update: UpdateBatch = Field( + ..., description="Batch of update operations to apply to skillbook" + ) + raw: Dict[str, Any] = Field( + default_factory=dict, description="Raw LLM response data" + ) + + @model_validator(mode="before") + @classmethod + def _accept_flat_shape(cls, data: Any) -> Any: + """If the LLM returns {reasoning, operations, ...} without an 'update' + wrapper, nest it automatically so Pydantic can validate.""" + if isinstance(data, dict) and "update" not in data and "operations" in data: + reasoning = data.pop("reasoning", "") + operations = data.pop("operations", []) + data["update"] = UpdateBatch.from_json( + {"reasoning": reasoning, "operations": operations} + ) + return data diff --git a/ace/core/recursive_agent.py b/ace/core/recursive_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..c0f3a5b4b3a5154562bdc812ff70081fb677b099 --- /dev/null +++ b/ace/core/recursive_agent.py @@ -0,0 +1,799 @@ +"""Recursive agent — reusable agentic step with compaction and recursion. + +A ``RecursiveAgent`` wraps a PydanticAI agent with: +- Two-tier compaction (microcompaction + full summarization) +- Depth-based recursion via a ``recurse`` tool +- Budget management (token + request limits) +- Sync/async execution + +Both the RR (Recursive Reflector) and the agentic SkillManager build +on this. Callers provide their own tools, output type, and prompts. + +Usage:: + + from ace.core.recursive_agent import RecursiveAgent, AgenticConfig + + agent = RecursiveAgent( + model="gpt-4o-mini", + output_type=MyOutput, + system_prompt="You are a ...", + config=AgenticConfig(max_requests=20), + tools=[my_tool_registrar], # list of (agent) -> None functions + tool_names_to_compact=("my_tool",), + ) + output, metadata = agent.run(prompt="Analyze this", deps=my_deps) +""" + +from __future__ import annotations + +import asyncio +import concurrent.futures +import copy +import logging +from contextlib import nullcontext +from dataclasses import dataclass +from typing import Any, Awaitable, Callable, Sequence, Type + +from pydantic_ai import Agent as PydanticAgent + +try: + import logfire + + _logfire: Any = logfire +except ImportError: + _logfire = None + + +def _rr_span(name: str, **attrs: Any): + """Open a logfire span if logfire is installed, else a no-op context.""" + if _logfire is not None: + return _logfire.span(name, **attrs) + return nullcontext() + + +from pydantic_ai.exceptions import UsageLimitExceeded +from pydantic_ai.messages import ( + ModelRequest, + ModelResponse, + TextPart, + ToolReturnPart, + UserPromptPart, +) +from pydantic_ai.models import Model as PydanticModel +from pydantic_ai.settings import ModelSettings +from pydantic_ai.usage import RequestUsage, UsageLimits + +from pydantic_ai import ModelRetry, RunContext + +from .metered_model import MeteredModel +from .sandbox import TraceSandbox +from ..providers.pydantic_ai import resolve_model + +UsageCallback = Callable[[RequestUsage, str], None] + +logger = logging.getLogger(__name__) + + +# ------------------------------------------------------------------ +# Default tools +# ------------------------------------------------------------------ + + +def register_execute_code(agent: PydanticAgent[AgenticDeps, Any]) -> None: + """Register the generic ``execute_code`` tool. + + Expects ``deps.sandbox`` (a :class:`TraceSandbox` or compatible) + and ``deps.config.timeout`` / ``deps.config.max_output_chars``. + """ + + @agent.tool(retries=3) + def execute_code(ctx: RunContext[AgenticDeps], code: str) -> str: + """Execute Python code in the sandbox. + + Variables persist across calls. Pre-loaded modules: + ``json``, ``re``, ``collections``, ``datetime``. + + Built-in helper: ``register_helper(name, source, description)`` + defines a reusable Python function in this sandbox AND auto-injects + it into every child you later spawn via ``recurse`` — register + extraction/scoring logic once, reuse it across children. + + Args: + code: Python code to execute. + + Returns: + Captured stdout/stderr from execution. + """ + ctx.deps.iteration += 1 + if ctx.deps.sandbox is None: + return "(no sandbox configured)" + + sandbox = ctx.deps.sandbox + timeout = ctx.deps.config.timeout + max_output = ctx.deps.config.max_output_chars + + result = sandbox.execute(code, timeout=timeout) + + if result.exception: + error_msg = f"{type(result.exception).__name__}: {result.exception}" + stdout_ctx = "" + if result.stdout: + stdout_ctx = f"stdout before error:\n{result.stdout[:max_output]}\n\n" + raise ModelRetry( + f"{stdout_ctx}Code error:\n{error_msg}\n\nFix the bug and try again." + ) + + parts: list[str] = [] + if result.stdout: + parts.append(result.stdout) + if result.stderr: + parts.append(f"stderr: {result.stderr}") + + output = "\n".join(parts) if parts else "(no output)" + + if len(output) > max_output: + remaining = len(output) - max_output + output = ( + f"{output[:max_output]}\n" f"[TRUNCATED: {remaining} chars remaining]" + ) + + return output + + +def register_recurse(agent: PydanticAgent[AgenticDeps, Any]) -> None: + """Register the generic ``recurse`` tool for depth-based decomposition. + + Expects ``deps.run_session_fn`` to be set (done by + :meth:`RecursiveAgent.run`). + """ + + @agent.tool + async def recurse( + ctx: RunContext[AgenticDeps], + prompt: str, + context_code: str = "", + ) -> str: + """Spawn a child session to investigate a sub-problem in isolation. + + Use this to keep your context lean: the child works through + bulky data in its own context window and returns only a text + summary. The child inherits a copy of your sandbox variables + and any helpers you've registered via `register_helper`. It + does NOT see your conversation, so `prompt` must be self-contained. + Calling `recurse` multiple times in a single assistant turn + dispatches the children in parallel. + + Args: + prompt: Self-contained instructions. Name the sandbox + variables to inspect and say what to return. + context_code: Optional Python run once in the child's + sandbox before it starts (e.g. ``chunk = traces[5:10]``). + + Returns: + Text summary of the child's structured output. + """ + deps = ctx.deps + if deps.run_session_fn is None: + return "(recurse unavailable — no session runner configured)" + + if deps.sandbox is None: + return "(recurse unavailable — no sandbox on deps)" + + sandbox = deps.sandbox + + # Create child sandbox inheriting parent's injected data + child_sandbox = TraceSandbox(trace=None) + for key, value in sandbox.namespace.items(): + if not key.startswith("_") and not callable(value): + child_sandbox.inject(key, value) + + # Inherit registered helpers + parent_registry = sandbox.namespace.get("helper_registry", {}) + if isinstance(parent_registry, dict): + timeout = deps.config.timeout + for hname, meta in parent_registry.items(): + if isinstance(meta, dict) and isinstance(meta.get("source"), str): + try: + child_sandbox.execute(meta["source"], timeout=timeout) + child_registry = child_sandbox.namespace.setdefault( + "helper_registry", {} + ) + child_registry[hname] = { + "description": meta.get("description", ""), + "source": meta["source"], + } + except Exception: + pass + + # Run optional context_code + if context_code.strip(): + result = child_sandbox.execute(context_code, timeout=deps.config.timeout) + if result.exception: + raise ModelRetry( + f"context_code failed: {result.exception}\n" + "Fix the code and try again." + ) + + # Compute child budget + cfg = deps.config + remaining = max(0, cfg.max_tokens - deps.parent_usage_tokens) + child_token_budget = max(10_000, int(remaining * cfg.child_budget_fraction)) + + # Build child deps (same type as parent) + child_deps = deps.__class__( + **{ + **{ + f.name: getattr(deps, f.name) + for f in deps.__dataclass_fields__.values() + }, + "sandbox": child_sandbox, + "depth": deps.depth + 1, + "iteration": 0, + "parent_usage_tokens": 0, + } + ) + + try: + output, _ = await deps.run_session_fn( + deps=child_deps, + prompt=prompt, + depth=deps.depth + 1, + ) + + # Serialize child output to text + if hasattr(output, "model_dump"): + d = output.model_dump(exclude={"raw"}, exclude_defaults=True) + parts = [f"{k}: {v}" for k, v in d.items() if v] + return "\n".join(parts) if parts else "(empty output)" + return str(output) if output else "(empty output)" + + except Exception as e: + return f"(child session failed: {e})" + + +# ------------------------------------------------------------------ +# Configuration +# ------------------------------------------------------------------ + +DEFAULT_COMPACTION_SUMMARY_PROMPT = """\ +Summarize your progress so far. Structure your response with these sections: + +1. **What you've done**: Steps completed, tools used, key decisions made. +2. **Findings so far**: Concrete results, computed values, identified patterns. +3. **Remaining work**: What hasn't been done yet. +4. **Current direction**: What you were investigating when this summary was requested. + +Be concise but preserve all concrete results and variable names.""" + + +@dataclass +class AgenticConfig: + """Base configuration for agentic steps with compaction and recursion. + + Subclass to add step-specific fields (e.g. sandbox timeout). + """ + + # Budget (wired to PydanticAI UsageLimits) + max_tokens: int = 500_000 + max_requests: int = 50 + context_window: int = 128_000 + # Recursion + max_depth: int = 2 + child_budget_fraction: float = 0.5 + # Compaction + max_compactions: int = 3 + microcompact_keep_recent: int = 3 + # Sandbox execution + timeout: float = 60.0 + max_output_chars: int = 50_000 + # Metering — fired once per completed pydantic-ai model request + # (orchestrator turn, child session, compaction summary). Exceptions + # inside the callback are swallowed by MeteredModel so a broken + # meter never crashes a run. + usage_callback: UsageCallback | None = None + + def build_usage_limits(self, remaining_tokens: int | None = None) -> UsageLimits: + base = remaining_tokens or self.max_tokens + return UsageLimits( + total_tokens_limit=base, + request_limit=self.max_requests, + ) + + +# ------------------------------------------------------------------ +# Dependency container +# ------------------------------------------------------------------ + + +@dataclass +class AgenticDeps: + """Base dependencies for agentic steps. + + Subclass to add step-specific deps (trace data, etc.). + """ + + config: AgenticConfig + sandbox: Any = None # TraceSandbox or compatible + depth: int = 0 + max_depth: int = 2 + iteration: int = 0 + run_session_fn: Callable[..., Awaitable[tuple[Any, Any]]] | None = None + parent_usage_tokens: int = 0 + + +# ------------------------------------------------------------------ +# Exceptions +# ------------------------------------------------------------------ + + +class BudgetExhausted(Exception): + """Raised when the agent's token or request budget is fully spent.""" + + def __init__(self, compaction_count: int = 0, usage: Any = None) -> None: + self.compaction_count = compaction_count + self.usage = usage + super().__init__("Agent budget exhausted") + + +# ------------------------------------------------------------------ +# Compaction utilities +# ------------------------------------------------------------------ + + +def cost_equivalent_tokens(usage: Any) -> int: + """Cost-equivalent token count for budget purposes. + + Anthropic Bedrock pricing (input side): + - fresh: 1.0x base + - cache_write: 1.25x base + - cache_read: 0.10x base + + PydanticAI's Bedrock wrapper sets ``input_tokens = fresh + cache_write + + cache_read``. We rebuild the cost-equivalent: subtract 0.90x of cache_read + (since it should weigh 0.10 not 1.0) and add 0.25x of cache_write (since + it should weigh 1.25 not 1.0). Output is counted at 1.0x. + """ + input_tokens = getattr(usage, "input_tokens", 0) or 0 + cache_read = getattr(usage, "cache_read_tokens", 0) or 0 + cache_write = getattr(usage, "cache_write_tokens", 0) or 0 + output = getattr(usage, "output_tokens", 0) or 0 + cost_input = input_tokens - 0.90 * cache_read + 0.25 * cache_write + return int(cost_input + output) + + +def is_budget_exhausted( + limits: UsageLimits, + usage: Any, + cost_budget: int | None = None, +) -> bool: + """True if cost-equivalent or request budget is spent. + + When ``cost_budget`` is provided, the *cost-equivalent* token count + (``cost_equivalent_tokens``) is checked against it. The gross + ``total_tokens_limit`` on ``limits`` is treated as a coarse outer cap and + is normally inflated relative to the real budget, so it should rarely + trip first when caching is in play. + """ + if cost_budget is not None and cost_equivalent_tokens(usage) >= cost_budget: + return True + if limits.total_tokens_limit and usage.total_tokens >= limits.total_tokens_limit: + return True + if limits.request_limit and usage.requests >= limits.request_limit: + return True + return False + + +def microcompact( + messages: list, + keep_recent: int, + tool_names: tuple[str, ...], + placeholder: str = "[cleared — use tools to re-inspect if needed]", +) -> list: + """Tier 1: Clear old tool results from message history. + + Returns the **same list object** if nothing was cleared — caller + uses identity check to detect whether compaction did anything. + """ + tool_result_positions = [] + for msg_idx, msg in enumerate(messages): + if isinstance(msg, ModelRequest): + for part_idx, part in enumerate(msg.parts): + if isinstance(part, ToolReturnPart) and part.tool_name in tool_names: + tool_result_positions.append((msg_idx, part_idx)) + + if len(tool_result_positions) <= keep_recent: + return messages + + to_clear = ( + tool_result_positions[:-keep_recent] + if keep_recent > 0 + else tool_result_positions + ) + + compacted = copy.deepcopy(messages) + for msg_idx, part_idx in to_clear: + compacted[msg_idx].parts[part_idx].content = placeholder + return compacted + + +async def summarize_and_compact( + agent: PydanticAgent[AgenticDeps, Any], + messages: list, + deps: Any, + compaction_count: int, + summary_prompt: str = DEFAULT_COMPACTION_SUMMARY_PROMPT, + continuation_message: str = "", +) -> list: + """Tier 2: Full summarization — LLM summarizes, history pruned.""" + summary_result = await agent.run( + summary_prompt, + message_history=messages, + deps=deps, + output_type=str, + ) + summary = summary_result.output + + if not continuation_message: + continuation_message = ( + f"Your conversation was compacted ({compaction_count} time(s)). " + "Do NOT repeat work already completed. Continue." + ) + + return [ + ModelResponse( + parts=[ + TextPart(content=f"[Compaction summary #{compaction_count}]\n{summary}") + ] + ), + ModelRequest(parts=[UserPromptPart(content=continuation_message)]), + ] + + +# ------------------------------------------------------------------ +# Async runner +# ------------------------------------------------------------------ + + +async def run_agent_with_compaction( + agent: PydanticAgent[AgenticDeps, Any], + *, + deps: AgenticDeps, + prompt: str, + usage_limits: UsageLimits, + config: AgenticConfig, + tool_names_to_compact: tuple[str, ...] = (), + compaction_summary_prompt: str = DEFAULT_COMPACTION_SUMMARY_PROMPT, + compaction_continuation: str = "", + microcompact_placeholder: str = "[cleared — use tools to re-inspect if needed]", + on_compaction: Callable[[AgenticDeps, int, list], None] | None = None, + span_label: str = "rr", +) -> tuple[Any, dict]: + """Run a PydanticAI agent with two-tier compaction. + + Returns ``(output, metadata)``. + Raises :class:`BudgetExhausted` when budget is fully spent. + """ + message_history = None + compaction_count = 0 + user_prompt = prompt + cumulative_usage = None + last_run: Any = None + + span_name = ( + f"{span_label}.session" if deps.depth == 0 else f"{span_label}.session.child" + ) + with _rr_span(span_name, depth=deps.depth): + while True: + try: + async with agent.iter( + user_prompt, + deps=deps, + message_history=message_history, + usage_limits=usage_limits, + usage=cumulative_usage, + ) as agent_run: + last_run = agent_run + async for _node in agent_run: + deps.parent_usage_tokens = agent_run.usage().total_tokens or 0 + + assert agent_run.result is not None + output = agent_run.result.output + usage = agent_run.result.usage() + + metadata = { + "usage": { + "input_tokens": usage.input_tokens, + "output_tokens": usage.output_tokens, + "total_tokens": usage.total_tokens, + "requests": usage.requests, + "cache_read_tokens": getattr(usage, "cache_read_tokens", 0), + "cache_write_tokens": getattr( + usage, "cache_write_tokens", 0 + ), + }, + "compactions": compaction_count, + "depth": deps.depth, + "iterations": deps.iteration, + "timed_out": False, + } + return output, metadata + + except UsageLimitExceeded: + messages = last_run.all_messages() + cumulative_usage = last_run.usage() + + if is_budget_exhausted( + usage_limits, + cumulative_usage, + cost_budget=config.max_tokens, + ): + raise BudgetExhausted( + compaction_count=compaction_count, + usage=cumulative_usage, + ) + + compacted = microcompact( + messages, + config.microcompact_keep_recent, + tool_names_to_compact, + placeholder=microcompact_placeholder, + ) + + if compacted is messages: + compaction_count += 1 + if compaction_count > config.max_compactions: + raise BudgetExhausted( + compaction_count=compaction_count, + usage=cumulative_usage, + ) + + if on_compaction: + on_compaction(deps, compaction_count, messages) + + compacted = await summarize_and_compact( + agent, + messages, + deps, + compaction_count, + summary_prompt=compaction_summary_prompt, + continuation_message=compaction_continuation, + ) + + message_history = compacted + user_prompt = "Continue your analysis." + + +# ------------------------------------------------------------------ +# Sync wrapper +# ------------------------------------------------------------------ + + +def run_agent_sync( + agent: PydanticAgent[AgenticDeps, Any], **kwargs: Any +) -> tuple[Any, dict]: + """Synchronous wrapper around :func:`run_agent_with_compaction`.""" + coro = run_agent_with_compaction(agent, **kwargs) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + + if loop and loop.is_running(): + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: + return pool.submit(asyncio.run, coro).result() + else: + return asyncio.run(coro) + + +# ------------------------------------------------------------------ +# RecursiveAgent — high-level API +# ------------------------------------------------------------------ + + +ToolRegistrar = Callable[..., None] + + +class RecursiveAgent: + """A PydanticAI agent with compaction, recursion, and budget management. + + This is the high-level API. Callers provide: + - ``output_type``: The structured output schema + - ``tools``: List of tool registrar functions ``(agent) -> None`` + - ``system_prompt``: The system prompt + - ``config``: Budget, compaction, and recursion settings + + The agent handles compaction and child session spawning automatically. + + Example:: + + agent = RecursiveAgent( + model="gpt-4o-mini", + output_type=ReflectorOutput, + system_prompt="You are a trace analyst...", + tools=[register_execute_code, register_analysis_tools], + tool_names_to_compact=("execute_code", "analyze"), + ) + output, metadata = agent.run(prompt="Analyze...", deps=my_deps) + """ + + def __init__( + self, + model: str | PydanticModel, + *, + output_type: Type, + system_prompt: str, + config: AgenticConfig | None = None, + model_settings: ModelSettings | None = None, + tools: Sequence[ToolRegistrar] = (), + tool_names_to_compact: tuple[str, ...] = (), + compaction_summary_prompt: str = DEFAULT_COMPACTION_SUMMARY_PROMPT, + compaction_continuation: str = "", + microcompact_placeholder: str = "[cleared — use tools to re-inspect if needed]", + on_compaction: Callable[[AgenticDeps, int, list], None] | None = None, + span_label: str = "rr", + ) -> None: + self.config = config or AgenticConfig() + self._model = model + self._model_settings = model_settings + self._output_type = output_type + self._system_prompt = system_prompt + self._tools = list(tools) + self._tool_names_to_compact = tool_names_to_compact + self._compaction_summary_prompt = compaction_summary_prompt + self._compaction_continuation = compaction_continuation + self._microcompact_placeholder = microcompact_placeholder + self._on_compaction = on_compaction + self._span_label = span_label + + # Build root agent (depth=0) + self._agent = self._create_agent(depth=0) + + def _create_agent(self, depth: int = 0) -> PydanticAgent[AgenticDeps, Any]: + """Create a PydanticAI agent for the given recursion depth. + + The root (depth 0) uses the configured ``output_type`` (typically + a structured Pydantic model). Children return free-form text: + they exist to investigate one sub-problem and report a focused + answer, not to produce a full reflection. + """ + if isinstance(self._model, PydanticModel): + resolved = self._model + else: + resolved = resolve_model(self._model) + + if self.config.usage_callback is not None: + resolved = MeteredModel(resolved, self.config.usage_callback) + + output_type = self._output_type if depth == 0 else str + + agent: PydanticAgent[AgenticDeps, Any] = PydanticAgent( + resolved, + output_type=output_type, + system_prompt=self._system_prompt, + retries=3, + model_settings=self._model_settings, + defer_model_check=True, + ) + + # Default tools: execute_code + recurse (if not at max depth) + register_execute_code(agent) + if depth < self.config.max_depth: + register_recurse(agent) + + # Additional caller-provided tools + for registrar in self._tools: + registrar(agent) + + return agent + + async def _run_child_session( + self, + *, + deps: AgenticDeps, + prompt: str, + depth: int = 0, + ) -> tuple[Any, AgenticDeps]: + """Run a child session with its own agent and budget.""" + child_agent = self._create_agent(depth=depth) + + remaining = getattr(deps, "_remaining_tokens", None) + try: + output, metadata = await run_agent_with_compaction( + child_agent, + deps=deps, + prompt=prompt, + usage_limits=self.config.build_usage_limits(remaining_tokens=remaining), + config=self.config, + tool_names_to_compact=self._tool_names_to_compact, + compaction_summary_prompt=self._compaction_summary_prompt, + compaction_continuation=self._compaction_continuation, + microcompact_placeholder=self._microcompact_placeholder, + on_compaction=self._on_compaction, + span_label=self._span_label, + ) + return output, deps + except BudgetExhausted: + return None, deps + + def run( + self, + *, + deps: AgenticDeps, + prompt: str, + remaining_tokens: int | None = None, + ) -> tuple[Any, dict]: + """Run the agent synchronously with compaction. + + Args: + deps: Agent dependencies. + prompt: Initial prompt. + remaining_tokens: Override token budget (for child sessions). + + Returns: + Tuple of (output, metadata_dict). + + Raises: + BudgetExhausted: When budget is fully spent. + """ + # Wire up child session runner + deps.run_session_fn = self._run_child_session + + return run_agent_sync( + self._agent, + deps=deps, + prompt=prompt, + usage_limits=self.config.build_usage_limits( + remaining_tokens=remaining_tokens + ), + config=self.config, + tool_names_to_compact=self._tool_names_to_compact, + compaction_summary_prompt=self._compaction_summary_prompt, + compaction_continuation=self._compaction_continuation, + microcompact_placeholder=self._microcompact_placeholder, + on_compaction=self._on_compaction, + span_label=self._span_label, + ) + + # ------------------------------------------------------------------ + # Sandbox helpers + # ------------------------------------------------------------------ + + def create_sandbox( + self, + *, + trace: Any = None, + variables: dict[str, Any] | None = None, + ) -> TraceSandbox: + """Create a sandbox and inject variables. + + Args: + trace: Optional trace object passed to TraceSandbox constructor. + variables: Dict of ``{name: value}`` to inject into the sandbox + namespace. + + Returns: + A ready-to-use :class:`TraceSandbox`. + """ + sandbox = TraceSandbox(trace=trace, llm_query_fn=None) + if variables: + for name, value in variables.items(): + sandbox.inject(name, value) + return sandbox + + @staticmethod + def on_compaction(deps: AgenticDeps, compaction_count: int, messages: list) -> None: + """Default compaction callback — save metadata to sandbox history. + + Subclasses can override or pass a different callback via + ``on_compaction`` in ``__init__``. + """ + sandbox = getattr(deps, "sandbox", None) + if sandbox is not None: + history = sandbox.namespace.get("history", []) + history.append( + { + "compaction_round": compaction_count, + "message_count": len(messages), + } + ) + sandbox.namespace["history"] = history diff --git a/ace/core/sandbox.py b/ace/core/sandbox.py new file mode 100644 index 0000000000000000000000000000000000000000..3f44576d0e2bfdf28a4bfed51fb77722352301bb --- /dev/null +++ b/ace/core/sandbox.py @@ -0,0 +1,825 @@ +"""Lightweight sandbox for executing LLM-generated Python code.""" + +from __future__ import annotations + +import collections +import copy +import io +import json +import logging +import platform +import re +import math +import signal +import threading +import time as _time_mod +from concurrent.futures import ThreadPoolExecutor +from contextlib import redirect_stdout, redirect_stderr +from dataclasses import dataclass +from datetime import datetime, timedelta, date, time, timezone +from typing import Any, Callable, Dict, List, Optional + +logger = logging.getLogger(__name__) + + +class ExecutionTimeoutError(Exception): + """Raised when code execution exceeds the timeout.""" + + pass + + +@dataclass +class ExecutionResult: + """Result of executing code in the sandbox. + + Attributes: + stdout: Captured standard output + stderr: Captured standard error + final_value: Value passed to FINAL() if called, otherwise None + exception: Exception that occurred during execution, if any + """ + + stdout: str = "" + stderr: str = "" + final_value: Any = None + exception: Optional[Exception] = None + + @property + def success(self) -> bool: + """Return True if execution completed without errors.""" + return self.exception is None + + +class TraceSandbox: + """Lightweight sandbox using exec() with restricted builtins. + + This sandbox restricts builtins but is NOT secure against determined escape + attempts. Security relies on trusting the LLM not to generate malicious code. + Do not use this sandbox to execute untrusted or user-provided code. + + Restrictions (defense-in-depth, not security guarantees): + - No file access: `open` and `__import__` are blocked + - No code injection: `eval`, `exec`, `compile` are blocked + - Read-only trace: trace data is injected as-is + - Timeout protection: Configurable per-execution timeout (Unix only) + - Worst case: bad code fails -> fallback to simple reflector + + Example: + >>> sandbox = TraceSandbox(trace=trace, llm_query_fn=llm_query) + >>> result = sandbox.execute("print(len(trace.steps))", timeout=30.0) + >>> print(result.stdout) + 5 + """ + + # Safe builtins that don't allow file/network access or code injection + SAFE_BUILTINS: Dict[str, Any] = { + # Core types + "print": print, + "len": len, + "str": str, + "int": int, + "float": float, + "list": list, + "dict": dict, + "set": set, + "tuple": tuple, + "bool": bool, + "type": type, + "isinstance": isinstance, + "issubclass": issubclass, + "range": range, + "bytes": bytes, + "bytearray": bytearray, + # Iteration + "enumerate": enumerate, + "zip": zip, + "map": map, + "filter": filter, + "sorted": sorted, + "reversed": reversed, + "iter": iter, + "next": next, + # Math + "min": min, + "max": max, + "sum": sum, + "abs": abs, + "round": round, + "pow": pow, + "divmod": divmod, + # Logic + "any": any, + "all": all, + "not": lambda x: not x, + # String/Formatting + "chr": chr, + "ord": ord, + "repr": repr, + "format": format, + "ascii": ascii, + "bin": bin, + "hex": hex, + "oct": oct, + # Object inspection (getattr blocks dunder access — see __init__) + "hasattr": hasattr, + "getattr": None, # Replaced with safe_getattr in __init__ + "dir": dir, + "vars": lambda obj=None: {} if obj is None else vars(obj), + "id": id, + "hash": hash, + "callable": callable, + # Exceptions (for try/except in generated code) + "Exception": Exception, + "BaseException": BaseException, + "ValueError": ValueError, + "KeyError": KeyError, + "IndexError": IndexError, + "TypeError": TypeError, + "AttributeError": AttributeError, + "RuntimeError": RuntimeError, + "StopIteration": StopIteration, + "AssertionError": AssertionError, + "LookupError": LookupError, + "ZeroDivisionError": ZeroDivisionError, + "NameError": NameError, + "OverflowError": OverflowError, + "FloatingPointError": FloatingPointError, + "ArithmeticError": ArithmeticError, + "SyntaxError": SyntaxError, + "IndentationError": IndentationError, + "TabError": TabError, + "UnicodeError": UnicodeError, + "UnicodeDecodeError": UnicodeDecodeError, + "UnicodeEncodeError": UnicodeEncodeError, + "NotImplementedError": NotImplementedError, + "RecursionError": RecursionError, + # Constants + "True": True, + "False": False, + "None": None, + # BLOCKED - security sensitive (raise clear errors, not NoneType) + "open": None, + "__import__": None, # replaced with _safe_import in __init__ + "eval": None, + "exec": None, + "compile": None, + "input": None, + "globals": None, + "locals": None, + "breakpoint": None, + "memoryview": None, + } + + def __init__( + self, + trace: Optional[str] = None, + llm_query_fn: Optional[Callable[[str], str]] = None, + additional_globals: Optional[Dict[str, Any]] = None, + *, + parallel_max_concurrency: int = 10, + parallel_max_retries: int = 3, + parallel_retry_delay: float = 1.0, + parallel_timeout: Optional[float] = None, + ) -> None: + """Initialize the sandbox with trace and optional LLM query function. + + Args: + trace: Trace string for exploration (can be None). Non-string + values are coerced to str; None is left as-is. + llm_query_fn: Function to call for sub-LLM queries + additional_globals: Extra variables to inject into the namespace + parallel_max_concurrency: Max concurrent workers for parallel_map + parallel_max_retries: Max retries per item in parallel_map + parallel_retry_delay: Base delay (seconds) for exponential backoff + parallel_timeout: Per-item timeout in seconds (None = no timeout) + """ + self._final_value: Any = None + self._final_called = False + + # parallel_map configuration (infrastructure-side only) + self._parallel_max_concurrency = parallel_max_concurrency + self._parallel_max_retries = parallel_max_retries + self._parallel_retry_delay = parallel_retry_delay + self._parallel_timeout = parallel_timeout + + # Sanitize trace: coerce to str if provided + if trace is not None and not isinstance(trace, str): + trace = str(trace) + + # Build the namespace + self.namespace: Dict[str, Any] = { + "__builtins__": self.SAFE_BUILTINS.copy(), + # Core analysis objects + "trace": trace, + "FINAL": self._final, + "FINAL_VAR": self._final_var, + "SHOW_VARS": self._show_vars, + "helper_registry": {}, + "register_helper": self._register_helper, + "list_helpers": self._list_helpers, + "run_helper": self._run_helper, + "get_batch_item": self._get_batch_item, + "get_item_payload": self._get_item_payload, + "get_item_messages": self._get_item_messages, + "get_item_question": self._get_item_question, + "get_item_feedback": self._get_item_feedback, + "get_item_id": self._get_item_id, + "get_message_text": self._get_message_text, + "preview_item": self._preview_item, + "parallel_map": self._parallel_map, + # Safe stdlib modules + "json": json, + "re": re, + "math": math, + "collections": collections, + # datetime module and commonly used classes + "datetime": datetime, + "timedelta": timedelta, + "date": date, + "time": time, + "timezone": timezone, + } + + # Safe getattr that blocks dunder access — override in both + # builtins (so bare getattr() works) and namespace (for direct ref) + def safe_getattr(obj, name, *default): + if name.startswith("_"): + raise AttributeError(f"Access to '{name}' blocked") + return getattr(obj, name, *default) if default else getattr(obj, name) + + self.namespace["__builtins__"]["getattr"] = safe_getattr + self.namespace["safe_getattr"] = safe_getattr + + # Safe import — allows pre-loaded modules, blocks everything else. + # LLMs often write `import json` even when json is already available. + _allowed_modules = { + "json": json, + "re": re, + "math": math, + "collections": collections, + "datetime": __import__("datetime"), + } + + def _safe_import(name: str, *args: Any, **kwargs: Any) -> Any: + if name in _allowed_modules: + return _allowed_modules[name] + raise ImportError( + f"import {name!r} is blocked in sandbox. " + f"Pre-loaded modules ({', '.join(sorted(_allowed_modules))}) " + f"are already available — use them directly." + ) + + self.namespace["__builtins__"]["__import__"] = _safe_import + + # Add llm_query if provided + if llm_query_fn is not None: + self.namespace["llm_query"] = llm_query_fn + else: + # Provide a stub that explains the feature is disabled + self.namespace["llm_query"] = lambda _prompt: ( + "(llm_query disabled - analyze with available data)" + ) + + # Add any additional globals + if additional_globals: + self.namespace.update(additional_globals) + + def _final(self, value: Any) -> None: + """Called by LLM code to output the final result. + + Args: + value: The final analysis result (should be a dict matching ReflectorOutput) + + Raises: + StopIteration: Always raised to signal completion + """ + self._final_value = value + self._final_called = True + raise StopIteration("FINAL called - analysis complete") + + def _final_var(self, var_name: str) -> None: + """Called by LLM code to output a variable as the final result. + + Convenience function to finalize with a pre-built result stored in a variable. + Useful when the analysis result is built up across multiple code blocks. + + Args: + var_name: Name of the variable in the namespace to use as the result + + Raises: + ValueError: If the variable doesn't exist + StopIteration: Always raised to signal completion + """ + if var_name not in self.namespace: + available = [k for k in self.namespace.keys() if not k.startswith("_")] + raise ValueError( + f"Variable '{var_name}' not found. Available: {available[:20]}" + ) + self._final(self.namespace[var_name]) + + def _show_vars(self) -> None: + """Print available variables in the namespace for debugging. + + Prints a list of user-accessible variables (excludes internal/dunder names). + """ + user_vars = [k for k in self.namespace.keys() if not k.startswith("_")] + # Exclude builtins and modules for cleaner output + excluded = { + "__builtins__", + "json", + "re", + "collections", + "datetime", + "timedelta", + "date", + "time", + "timezone", + "safe_getattr", + } + user_vars = [k for k in user_vars if k not in excluded] + logger.debug("Available variables: %s", sorted(user_vars)) + + def _register_helper( + self, + name: str, + source: str, + description: str = "", + ) -> str: + """Register reusable helper code in the sandbox. + + The helper source is executed immediately and stored so that future + sandbox snapshots can recreate the same helper definitions for + sub-agents. + """ + if not name.isidentifier(): + raise ValueError(f"Invalid helper name: {name!r}") + if not source.strip(): + raise ValueError("Helper source cannot be empty") + + exec(source, self.namespace, self.namespace) + helper = self.namespace.get(name) + if not callable(helper): + raise ValueError(f"Helper source must define a callable named {name!r}") + + registry = self.namespace.setdefault("helper_registry", {}) + registry[name] = { + "description": description, + "source": source, + } + return f"Registered helper {name}" + + def _list_helpers(self) -> list[dict[str, str]]: + """Return metadata for registered helpers.""" + registry = self.namespace.get("helper_registry", {}) + if not isinstance(registry, dict): + return [] + + helpers: list[dict[str, str]] = [] + for name, meta in registry.items(): + if not isinstance(meta, dict): + continue + helpers.append( + { + "name": str(name), + "description": str(meta.get("description", "")), + } + ) + return helpers + + def _run_helper(self, name: str, *args: Any, **kwargs: Any) -> Any: + """Invoke a registered helper by name.""" + helper = self.namespace.get(name) + if not callable(helper): + raise KeyError(f"Helper {name!r} is not registered") + return helper(*args, **kwargs) + + def _get_batch_item(self, index: int) -> Any: + """Return a batch item by index when batch helpers are available.""" + batch_items = self.namespace.get("batch_items") + if not isinstance(batch_items, list): + raise RuntimeError("batch_items is not available in this sandbox") + return batch_items[index] + + def _resolve_batch_item(self, item_or_index: Any) -> Any: + """Resolve a batch helper argument to the underlying item.""" + if isinstance(item_or_index, int): + return self._get_batch_item(item_or_index) + return item_or_index + + def _get_item_payload(self, item_or_index: Any) -> Any: + """Return the payload for a batch item or index without rewriting it.""" + item = self._resolve_batch_item(item_or_index) + if ( + isinstance(item, dict) + and item.get("role") == "conversation" + and isinstance(item.get("content"), dict) + ): + return item["content"] + return item + + def _get_item_messages(self, item_or_index: Any) -> list[Any]: + """Return a best-effort message list for a batch item or index.""" + payload = self._get_item_payload(item_or_index) + if isinstance(payload, list): + return payload + if isinstance(payload, dict): + trace_value = payload.get("trace") + if isinstance(trace_value, list): + return trace_value + if isinstance(trace_value, dict): + for key in ("messages", "steps", "trace"): + nested = trace_value.get(key) + if isinstance(nested, list): + return nested + for key in ("messages", "steps"): + value = payload.get(key) + if isinstance(value, list): + return value + return [] + + def _get_item_field(self, item_or_index: Any, field: str) -> str: + """Extract a string field from a batch item payload when present.""" + payload = self._get_item_payload(item_or_index) + if isinstance(payload, dict): + value = payload.get(field) + if value is not None: + return str(value) + return "" + + def _get_item_question(self, item_or_index: Any) -> str: + """Return the question field for a batch item or index.""" + return self._get_item_field(item_or_index, "question") + + def _get_item_feedback(self, item_or_index: Any) -> str: + """Return the feedback field for a batch item or index.""" + return self._get_item_field(item_or_index, "feedback") + + def _get_item_id(self, item_or_index: Any) -> str: + """Return a stable identifier for a batch item or index.""" + item = self._resolve_batch_item(item_or_index) + payload = self._get_item_payload(item) + + if isinstance(item_or_index, int): + item_ids = self.namespace.get("item_ids") + if isinstance(item_ids, list) and 0 <= item_or_index < len(item_ids): + return str(item_ids[item_or_index]) + + if isinstance(item, dict): + for key in ("item_id", "task_id", "id"): + value = item.get(key) + if value is not None: + return str(value) + if isinstance(payload, dict): + for key in ("item_id", "task_id", "id"): + value = payload.get(key) + if value is not None: + return str(value) + + return "unknown_item" + + def _get_message_text(self, message: Any) -> str: + """Return a readable text summary for a message-like object.""" + if isinstance(message, dict): + content = message.get("content") + if content not in (None, ""): + if isinstance(content, str): + return content + try: + return json.dumps(content, default=str) + except Exception: + return str(content) + + tool_calls = message.get("tool_calls") + if tool_calls: + return f"tool_calls={json.dumps(tool_calls, default=str)}" + + tool_results = message.get("tool_results") + if tool_results: + return f"tool_results={json.dumps(tool_results, default=str)}" + + for key in ("reasoning", "answer", "text"): + value = message.get(key) + if value not in (None, ""): + return str(value) + + try: + return json.dumps(message, default=str) + except Exception: + return str(message) + + return str(message) + + def _preview_item(self, item_or_index: Any) -> dict[str, Any]: + """Return a compact preview for a batch item or index.""" + messages = self._get_item_messages(item_or_index) + first_message = self._get_message_text(messages[0]) if messages else "" + payload = self._get_item_payload(item_or_index) + return { + "item_id": self._get_item_id(item_or_index), + "question_preview": self._get_item_question(item_or_index)[:120], + "feedback_preview": self._get_item_feedback(item_or_index)[:120], + "message_count": len(messages), + "first_message_preview": first_message[:120], + "payload_type": type(payload).__name__, + } + + def _parallel_map( + self, fn: Callable[[Any], Any], inputs: list, *, return_exceptions: bool = False + ) -> List[Any]: + """Execute fn over inputs in parallel using a thread pool. + + Concurrency, retries, backoff, and timeout are controlled by the + sandbox configuration — the agent cannot override them. + + Args: + fn: A callable to apply to each input + inputs: Ordered list of inputs + return_exceptions: If True, failed items appear as exceptions in + the results list instead of raising immediately + + Returns: + Ordered list of results (same length/order as inputs) + + Raises: + Exception: Re-raises the first worker exception when + return_exceptions is False + """ + if not inputs: + return [] + + max_concurrency = self._parallel_max_concurrency + max_retries = self._parallel_max_retries + retry_delay = self._parallel_retry_delay + timeout = self._parallel_timeout + + def _worker(item: Any) -> Any: + last_exc: Optional[Exception] = None + for attempt in range(max_retries + 1): + try: + return fn(item) + except Exception as exc: + last_exc = exc + if attempt < max_retries: + backoff = retry_delay * (2**attempt) + _time_mod.sleep(backoff) + raise last_exc # type: ignore[misc] + + pool_size = min(len(inputs), max_concurrency) + results: List[Any] = [None] * len(inputs) + first_exc: Optional[Exception] = None + first_exc_idx: Optional[int] = None + + with ThreadPoolExecutor(max_workers=pool_size) as pool: + futures = { + pool.submit(_worker, item): idx for idx, item in enumerate(inputs) + } + for future in futures: + idx = futures[future] + try: + results[idx] = future.result(timeout=timeout) + except Exception as exc: + if return_exceptions: + results[idx] = exc + else: + if first_exc_idx is None or idx < first_exc_idx: + first_exc = exc + first_exc_idx = idx + + if first_exc is not None and not return_exceptions: + raise first_exc + + return results + + @property + def final_value(self) -> Any: + """Return the value passed to FINAL(), or None if not called.""" + return self._final_value + + @property + def final_called(self) -> bool: + """Return True if FINAL() was called.""" + return self._final_called + + def inject(self, name: str, value: Any) -> None: + """Inject a variable into the sandbox namespace. + + Args: + name: Variable name + value: Variable value + """ + self.namespace[name] = value + + def execute(self, code: str, timeout: float = 30.0) -> ExecutionResult: + """Execute code in the sandbox and capture output. + + Args: + code: Python code to execute + timeout: Maximum execution time in seconds (default: 30.0). + - Unix: uses signal.SIGALRM + - Windows: not enforced (in-process execution) + + Returns: + ExecutionResult with stdout, stderr, final_value, and exception + """ + if platform.system() == "Windows": + return self._execute_no_timeout(code) + elif threading.current_thread() is not threading.main_thread(): + return self._execute_no_timeout(code) + else: + return self._execute_unix(code, timeout) + + def _execute_unix(self, code: str, timeout: float) -> ExecutionResult: + """Execute code using signal-based timeout (Unix only). + + Args: + code: Python code to execute + timeout: Maximum execution time in seconds + + Returns: + ExecutionResult with stdout, stderr, final_value, and exception + """ + stdout_buf = io.StringIO() + stderr_buf = io.StringIO() + + # Set up timeout handler (Unix only) + use_timeout = timeout > 0 + old_handler = None + + def timeout_handler(_signum: int, _frame: Any) -> None: + raise ExecutionTimeoutError(f"Execution exceeded {timeout}s timeout") + + if use_timeout: + old_handler = signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(math.ceil(timeout)) + + try: + with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf): + exec(code, self.namespace, self.namespace) + except StopIteration: + # FINAL() was called - this is expected + pass + except ExecutionTimeoutError as e: + stderr_buf.write(f"\nExecutionTimeoutError: {e}") + return ExecutionResult( + stdout=stdout_buf.getvalue(), + stderr=stderr_buf.getvalue(), + final_value=self._final_value, + exception=e, + ) + except Exception as e: + # Capture the exception info + stderr_buf.write(f"\n{type(e).__name__}: {e}") + return ExecutionResult( + stdout=stdout_buf.getvalue(), + stderr=stderr_buf.getvalue(), + final_value=self._final_value, + exception=e, + ) + finally: + if use_timeout: + signal.alarm(0) # Cancel the alarm + signal.signal(signal.SIGALRM, old_handler) + + return ExecutionResult( + stdout=stdout_buf.getvalue(), + stderr=stderr_buf.getvalue(), + final_value=self._final_value, + exception=None, + ) + + def _execute_windows(self, code: str, timeout: float) -> ExecutionResult: + """Execute code on Windows without timeout enforcement. + + Windows multiprocessing uses 'spawn' which cannot pass functions, + trace objects, or injected variables to subprocesses. Instead, + execute in-process for full feature support (no timeout enforcement). + + Args: + code: Python code to execute + timeout: Ignored on Windows (logged as warning) + + Returns: + ExecutionResult with stdout, stderr, final_value, and exception + """ + logger.debug("Windows: executing in-process (timeout not enforced)") + return self._execute_no_timeout(code) + + def _execute_no_timeout(self, code: str) -> ExecutionResult: + """Execute code without timeout enforcement. + + Fallback when multiprocessing is unavailable or fails. + + Args: + code: Python code to execute + + Returns: + ExecutionResult with stdout, stderr, final_value, and exception + """ + stdout_buf = io.StringIO() + stderr_buf = io.StringIO() + + try: + with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf): + exec(code, self.namespace, self.namespace) + except StopIteration: + # FINAL() was called - this is expected + pass + except Exception as e: + stderr_buf.write(f"\n{type(e).__name__}: {e}") + return ExecutionResult( + stdout=stdout_buf.getvalue(), + stderr=stderr_buf.getvalue(), + final_value=self._final_value, + exception=e, + ) + + return ExecutionResult( + stdout=stdout_buf.getvalue(), + stderr=stderr_buf.getvalue(), + final_value=self._final_value, + exception=None, + ) + + def reset(self) -> None: + """Reset the sandbox state for a new execution.""" + self._final_value = None + self._final_called = False + + +def create_readonly_sandbox(parent: TraceSandbox) -> TraceSandbox: + """Create an isolated sandbox snapshot for sub-agent use. + + Deep-copies data variables from the parent sandbox so the sub-agent + can explore trace data via ``execute_code`` without affecting the + parent's state. Safe for parallel use — each snapshot is independent. + + Args: + parent: The parent sandbox to snapshot. + + Returns: + A new TraceSandbox with deep-copied data variables. + """ + sandbox = TraceSandbox( + trace=None, + llm_query_fn=None, + parallel_max_concurrency=parent._parallel_max_concurrency, + parallel_max_retries=parent._parallel_max_retries, + parallel_retry_delay=parent._parallel_retry_delay, + parallel_timeout=parent._parallel_timeout, + ) + + # Keys already set up by TraceSandbox.__init__ — skip them + infrastructure = { + "__builtins__", + "FINAL", + "FINAL_VAR", + "SHOW_VARS", + "parallel_map", + "llm_query", + "safe_getattr", + "trace", + "register_helper", + "list_helpers", + "run_helper", + "get_batch_item", + "get_item_payload", + "get_item_messages", + "get_item_question", + "get_item_feedback", + "get_item_id", + "get_message_text", + "preview_item", + "json", + "re", + "math", + "collections", + "datetime", + "timedelta", + "date", + "time", + "timezone", + } + + for key, value in parent.namespace.items(): + if key in infrastructure or key.startswith("_"): + continue + try: + sandbox.namespace[key] = copy.deepcopy(value) + except (TypeError, copy.Error): + # Modules, functions, etc. — share by reference + sandbox.namespace[key] = value + + registry = sandbox.namespace.get("helper_registry", {}) + if isinstance(registry, dict): + for name, meta in registry.items(): + if not isinstance(meta, dict): + continue + source = meta.get("source") + if not isinstance(source, str) or not source.strip(): + continue + try: + exec(source, sandbox.namespace, sandbox.namespace) + except Exception as exc: + logger.warning("Failed to restore helper %s in snapshot: %s", name, exc) + + return sandbox diff --git a/ace/core/skillbook.py b/ace/core/skillbook.py new file mode 100644 index 0000000000000000000000000000000000000000..29349e8fec734ae4ca17476cc849fc597f30330f --- /dev/null +++ b/ace/core/skillbook.py @@ -0,0 +1,876 @@ +"""Skill, Skillbook, and update operations for the ACE framework.""" + +from __future__ import annotations + +import json +import re +import threading +from dataclasses import asdict, dataclass, field, fields as dataclass_fields +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, FrozenSet, Iterable, List, Literal, Optional, Union, cast + +from .insight_source import InsightSource, coerce_insight_source, coerce_insight_sources + +# --------------------------------------------------------------------------- +# Constants / helpers +# --------------------------------------------------------------------------- + +OperationType = Literal["ADD", "UPDATE", "TAG", "REMOVE"] +SkillSection = Literal["context", "harness"] +SCHEMA_VERSION = "2" +VALID_SECTIONS: frozenset[str] = frozenset({"context", "harness"}) +DEFAULT_LEGACY_SECTION: SkillSection = "context" +InsightSourceInput = Union[ + InsightSource, + Dict[str, Any], + List[Union[InsightSource, Dict[str, Any]]], +] +_UNSET = object() + + +def _now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _normalize_optional_text(value: Any) -> str | None: + if value is None: + return None + text = str(value).strip() + return text or None + + +def _normalize_required_text(value: Any, field_name: str) -> str: + text = _normalize_optional_text(value) + if text is None: + raise ValueError(f"{field_name} is required and must be non-empty") + return text + + +def _normalize_keyword(value: Any) -> str | None: + text = _normalize_optional_text(value) + if text is None: + return None + text = re.sub(r"\s+", "_", text.lower()) + return text or None + + +def _normalize_keywords(keywords: Iterable[Any] | None) -> list[str]: + normalized: list[str] = [] + seen: set[str] = set() + if keywords is None: + return normalized + for value in keywords: + keyword = _normalize_keyword(value) + if keyword is None or keyword in seen: + continue + normalized.append(keyword) + seen.add(keyword) + return normalized + + +def _coerce_section_and_keywords( + section: str, + keywords: Iterable[Any] | None, +) -> tuple[SkillSection, list[str]]: + normalized_section = _normalize_required_text(section, "section").lower() + normalized_keywords = _normalize_keywords(keywords) + if normalized_section in VALID_SECTIONS: + return cast(SkillSection, normalized_section), normalized_keywords + + # Backward-compatible coercion for legacy free-form sections. + legacy_keywords = _normalize_keywords([normalized_section, *normalized_keywords]) + return DEFAULT_LEGACY_SECTION, legacy_keywords + + +def _embedding_sidecar_path(file_path: Path) -> Path: + if file_path.suffix: + stem = file_path.with_suffix("") + else: + stem = file_path + return stem.parent / f"{stem.name}.embeddings.npz" + + +def _insight_source_signature(source: InsightSource) -> str: + return json.dumps(source.to_dict(), ensure_ascii=False, sort_keys=True, default=str) + + +def _append_unique_sources( + existing: List[InsightSource], + incoming: Iterable[InsightSource], +) -> None: + seen = {_insight_source_signature(source) for source in existing} + for source in incoming: + signature = _insight_source_signature(source) + if signature in seen: + continue + existing.append(source) + seen.add(signature) + + +def _serialize_sources(sources: Iterable[InsightSource]) -> list[dict[str, Any]]: + return [source.to_dict() for source in sources] + + +def _deserialize_sources(raw_sources: Any) -> list[InsightSource]: + if not isinstance(raw_sources, list): + return [] + deduped_sources: list[InsightSource] = [] + _append_unique_sources( + deduped_sources, + [ + coerce_insight_source(item) + for item in raw_sources + if isinstance(item, (InsightSource, dict)) + ], + ) + return deduped_sources + + +# --------------------------------------------------------------------------- +# Update operations +# --------------------------------------------------------------------------- + + +@dataclass +class UpdateOperation: + """Single mutation to apply to the skillbook.""" + + type: OperationType + section: str + issue: Optional[str] = None + keywords: List[str] = field(default_factory=list) + insight: Optional[str] = None + skill_id: Optional[str] = None + metadata: Dict[str, int] = field(default_factory=dict) + reason: Optional[str] = None + insight_source: Optional[InsightSourceInput] = None + learning_index: Optional[int] = None + reflection_index: Optional[int] = None + reflection_indices: List[int] = field(default_factory=list) + + @classmethod + def from_json(cls, payload: Dict[str, object]) -> "UpdateOperation": + metadata_raw = payload.get("metadata") or {} + metadata: Dict[str, Any] = ( + cast(Dict[str, Any], metadata_raw) if isinstance(metadata_raw, dict) else {} + ) + + op_type = str(payload["type"]).upper() + if op_type not in ("ADD", "UPDATE", "TAG", "REMOVE"): + raise ValueError(f"Invalid operation type: {op_type}") + + raw_source = payload.get("insight_source") + insight_source: Optional[InsightSourceInput] = None + if isinstance(raw_source, dict): + insight_source = InsightSource.from_dict(cast(Dict[str, Any], raw_source)) + elif isinstance(raw_source, Iterable) and not isinstance( + raw_source, (str, bytes) + ): + insight_source = [ + InsightSource.from_dict(cast(Dict[str, Any], item)) + for item in raw_source + if isinstance(item, dict) + ] + + raw_learning_index = payload.get("learning_index") + learning_index: Optional[int] = None + if raw_learning_index is not None: + try: + learning_index = int(cast(int, raw_learning_index)) + except (TypeError, ValueError): + pass + + raw_reflection_index = payload.get("reflection_index") + reflection_index: Optional[int] = None + if raw_reflection_index is not None: + try: + reflection_index = int(cast(int, raw_reflection_index)) + except (TypeError, ValueError): + pass + + reflection_indices: List[int] = [] + raw_reflection_indices = payload.get("reflection_indices") + if isinstance(raw_reflection_indices, Iterable) and not isinstance( + raw_reflection_indices, (str, bytes) + ): + for value in raw_reflection_indices: + try: + reflection_indices.append(int(cast(int, value))) + except (TypeError, ValueError): + continue + + raw_keywords = payload.get("keywords") + keywords: list[str] = [] + if isinstance(raw_keywords, Iterable) and not isinstance( + raw_keywords, (str, bytes) + ): + keywords = _normalize_keywords(raw_keywords) + + issue = _normalize_optional_text(payload.get("issue")) + insight = _normalize_optional_text(payload.get("insight")) + reason = _normalize_optional_text(payload.get("reason")) + + return cls( + type=cast(OperationType, op_type), + section=str(payload.get("section", "")), + issue=issue, + keywords=keywords, + insight=insight, + skill_id=( + str(payload["skill_id"]) + if payload.get("skill_id") is not None + else None + ), + metadata={str(k): int(v) for k, v in metadata.items()}, + reason=reason, + insight_source=insight_source, + learning_index=learning_index, + reflection_index=reflection_index, + reflection_indices=reflection_indices, + ) + + def to_json(self) -> Dict[str, object]: + data: Dict[str, object] = {"type": self.type, "section": self.section} + if self.issue is not None: + data["issue"] = self.issue + if self.keywords: + data["keywords"] = list(self.keywords) + if self.insight is not None: + data["insight"] = self.insight + if self.skill_id is not None: + data["skill_id"] = self.skill_id + if self.metadata: + data["metadata"] = self.metadata + if self.reason is not None: + data["reason"] = self.reason + if self.insight_source is not None: + sources = coerce_insight_sources(self.insight_source) + if len(sources) == 1: + data["insight_source"] = sources[0].to_dict() + elif sources: + data["insight_source"] = [source.to_dict() for source in sources] + if self.learning_index is not None: + data["learning_index"] = self.learning_index + if self.reflection_index is not None: + data["reflection_index"] = self.reflection_index + if self.reflection_indices: + data["reflection_indices"] = list(self.reflection_indices) + return data + + +@dataclass +class UpdateBatch: + """Bundle of skill manager reasoning and operations.""" + + reasoning: str + operations: List[UpdateOperation] = field(default_factory=list) + + @classmethod + def from_json(cls, payload: Dict[str, object]) -> "UpdateBatch": + ops_payload = payload.get("operations") + operations = [] + if isinstance(ops_payload, Iterable): + for item in ops_payload: + if isinstance(item, dict): + operations.append(UpdateOperation.from_json(item)) + return cls(reasoning=str(payload.get("reasoning", "")), operations=operations) + + def to_json(self) -> Dict[str, object]: + return { + "reasoning": self.reasoning, + "operations": [op.to_json() for op in self.operations], + } + + +# --------------------------------------------------------------------------- +# Skill types +# --------------------------------------------------------------------------- + + +@dataclass +class SimilarityDecision: + """Record of a SkillManager decision to KEEP two skills separate.""" + + decision: Literal["KEEP"] + reasoning: str + decided_at: str + similarity_at_decision: float + + +@dataclass +class Skill: + """Single skillbook entry.""" + + id: str + section: SkillSection + keywords: list[str] + issue: str + insight: str | None = None + occurrences: List[InsightSource] = field(default_factory=list) + active: bool = True + used_count: int = 0 + helpful_count: int = 0 + harmful_count: int = 0 + neutral_count: int = 0 + embedding: Optional[List[float]] = None + created_at: str = field(default_factory=_now_iso) + updated_at: str = field(default_factory=_now_iso) + + def to_llm_dict(self) -> Dict[str, Any]: + return { + "id": self.id, + "section": self.section, + "keywords": list(self.keywords), + "issue": self.issue, + "insight": self.insight, + "active": self.active, + "used_count": self.used_count, + "helpful_count": self.helpful_count, + "harmful_count": self.harmful_count, + "neutral_count": self.neutral_count, + } + + def embedding_text(self) -> str: + parts = [self.issue] + if self.insight: + parts.append(self.insight) + if self.keywords: + parts.append(f"Keywords: {', '.join(self.keywords)}") + return "\n\n".join(parts) + + +# --------------------------------------------------------------------------- +# Skillbook +# --------------------------------------------------------------------------- + + +class Skillbook: + """Structured context store as defined by ACE.""" + + def __init__(self) -> None: + self._skills: Dict[str, Skill] = {} + self._sections: Dict[str, List[str]] = {} + self._next_id = 0 + self._similarity_decisions: Dict[FrozenSet[str], SimilarityDecision] = {} + self._lock = threading.RLock() + + def __repr__(self) -> str: + return f"Skillbook(skills={len(self._skills)}, sections={list(self._sections.keys())})" + + def __str__(self) -> str: + if not self._skills: + return "Skillbook(empty)" + return self.as_prompt() + + # ------------------------------------------------------------------ # + # CRUD + # ------------------------------------------------------------------ # + + def add_skill( + self, + section: str, + issue: str | None = None, + *, + keywords: Iterable[Any] | None = None, + insight: str | None = None, + skill_id: Optional[str] = None, + insight_source: Optional[InsightSourceInput] = None, + ) -> Skill: + with self._lock: + raw_section = _normalize_required_text(section, "section").lower() + normalized_section, normalized_keywords = _coerce_section_and_keywords( + section, keywords + ) + issue_text = _normalize_required_text(issue, "issue") + insight_text = _normalize_optional_text(insight) + + if not normalized_keywords: + normalized_keywords = _normalize_keywords([raw_section]) + if normalized_section == "context" and insight_text is None: + if raw_section not in VALID_SECTIONS: + insight_text = issue_text + else: + raise ValueError("context skills require a non-empty insight") + + skill_id = skill_id or self._generate_id(normalized_section) + skill = Skill( + id=skill_id, + section=normalized_section, + keywords=normalized_keywords, + issue=issue_text, + insight=insight_text, + ) + _append_unique_sources( + skill.occurrences, coerce_insight_sources(insight_source) + ) + self._skills[skill_id] = skill + self._sections.setdefault(normalized_section, []).append(skill_id) + return skill + + def update_skill( + self, + skill_id: str, + *, + issue: object = _UNSET, + keywords: object = _UNSET, + insight: object = _UNSET, + insight_source: Optional[InsightSourceInput] = None, + ) -> Optional[Skill]: + with self._lock: + skill = self._skills.get(skill_id) + if skill is None: + return None + + if issue is not _UNSET and issue is not None: + skill.issue = _normalize_required_text(issue, "issue") + + if keywords is not _UNSET and keywords is not None: + normalized_keywords = _normalize_keywords(cast(Iterable[Any], keywords)) + if not normalized_keywords: + raise ValueError("keywords are required and must be non-empty") + skill.keywords = normalized_keywords + + if insight is not _UNSET and insight is not None: + skill.insight = _normalize_optional_text(insight) + + if skill.section == "context" and not skill.insight: + raise ValueError("context skills require a non-empty insight") + + if insight_source is not None: + _append_unique_sources( + skill.occurrences, + coerce_insight_sources(insight_source), + ) + + skill.embedding = None + skill.updated_at = _now_iso() + return skill + + def tag_skill( + self, + skill_id: str, + delta: Literal[1, -1, 0], + *, + insight_source: Optional[InsightSourceInput] = None, + ) -> Optional[Skill]: + """Record an effectiveness observation for a skill.""" + with self._lock: + skill = self._skills.get(skill_id) + if skill is None: + return None + if delta == 1: + skill.helpful_count += 1 + elif delta == -1: + skill.harmful_count += 1 + else: + skill.neutral_count += 1 + if insight_source is not None: + _append_unique_sources( + skill.occurrences, + coerce_insight_sources(insight_source), + ) + skill.updated_at = _now_iso() + return skill + + def mark_used(self, skill_ids: Iterable[str]) -> None: + """Bump ``used_count`` for each active skill ID.""" + with self._lock: + for sid in skill_ids: + skill = self._skills.get(sid) + if skill is not None and skill.active: + skill.used_count += 1 + skill.updated_at = _now_iso() + + def remove_skill( + self, + skill_id: str, + soft: bool = True, + *, + insight_source: Optional[InsightSourceInput] = None, + ) -> None: + with self._lock: + skill = self._skills.get(skill_id) + if skill is None: + return + if soft: + skill.active = False + if insight_source is not None: + _append_unique_sources( + skill.occurrences, + coerce_insight_sources(insight_source), + ) + skill.updated_at = _now_iso() + else: + self.purge(skill_id) + + def purge(self, skill_id: str) -> None: + with self._lock: + skill = self._skills.pop(skill_id, None) + if skill is None: + return + section_list = self._sections.get(skill.section) + if section_list: + self._sections[skill.section] = [ + sid for sid in section_list if sid != skill_id + ] + if not self._sections[skill.section]: + del self._sections[skill.section] + + def get_skill(self, skill_id: str) -> Optional[Skill]: + return self._skills.get(skill_id) + + def skills(self, include_invalid: bool = False) -> List[Skill]: + if include_invalid: + return list(self._skills.values()) + return [s for s in self._skills.values() if s.active] + + # ------------------------------------------------------------------ # + # Similarity decisions + # ------------------------------------------------------------------ # + + def get_similarity_decision( + self, skill_id_a: str, skill_id_b: str + ) -> Optional[SimilarityDecision]: + pair_key = frozenset([skill_id_a, skill_id_b]) + return self._similarity_decisions.get(pair_key) + + def set_similarity_decision( + self, + skill_id_a: str, + skill_id_b: str, + decision: SimilarityDecision, + ) -> None: + with self._lock: + pair_key = frozenset([skill_id_a, skill_id_b]) + self._similarity_decisions[pair_key] = decision + + def has_keep_decision(self, skill_id_a: str, skill_id_b: str) -> bool: + decision = self.get_similarity_decision(skill_id_a, skill_id_b) + return decision is not None and decision.decision == "KEEP" + + # ------------------------------------------------------------------ # + # Serialization + # ------------------------------------------------------------------ # + + def to_dict(self, exclude_embeddings: bool = False) -> Dict[str, object]: + del exclude_embeddings # JSON never carries embeddings in v2. + similarity_decisions_serialized = { + ",".join(sorted(pair_ids)): asdict(decision) + for pair_ids, decision in self._similarity_decisions.items() + } + skills_serialized = {} + for skill_id, skill in self._skills.items(): + skill_dict = asdict(skill) + skill_dict.pop("embedding", None) + skill_dict["occurrences"] = _serialize_sources(skill.occurrences) + skills_serialized[skill_id] = skill_dict + return { + "schema_version": SCHEMA_VERSION, + "skills": skills_serialized, + "sections": self._sections, + "next_id": self._next_id, + "similarity_decisions": similarity_decisions_serialized, + } + + @classmethod + def from_dict(cls, payload: Dict[str, object]) -> "Skillbook": + schema_version = str(payload.get("schema_version", "")) + if schema_version != SCHEMA_VERSION: + raise ValueError("Skillbook format v2 required — regenerate") + + instance = cls() + skills_payload = payload.get("skills", {}) + if isinstance(skills_payload, dict): + for skill_id, skill_value in skills_payload.items(): + if not isinstance(skill_value, dict): + continue + skill_data = dict(skill_value) + skill_data["embedding"] = None + skill_data["active"] = bool(skill_data.get("active", True)) + raw_keywords = skill_data.get("keywords") + skill_data["keywords"] = _normalize_keywords( + raw_keywords if isinstance(raw_keywords, list) else [] + ) + if not skill_data["keywords"]: + legacy_section = skill_data.get("section", DEFAULT_LEGACY_SECTION) + skill_data["keywords"] = _normalize_keywords([legacy_section]) + + section_value = str(skill_data.get("section", DEFAULT_LEGACY_SECTION)) + normalized_section, normalized_keywords = _coerce_section_and_keywords( + section_value, + skill_data["keywords"], + ) + skill_data["section"] = normalized_section + skill_data["keywords"] = normalized_keywords + skill_data["issue"] = _normalize_required_text( + skill_data.get("issue"), "issue" + ) + skill_data["insight"] = _normalize_optional_text( + skill_data.get("insight") + ) + skill_data["occurrences"] = _deserialize_sources( + skill_data.get("occurrences") + ) + valid_fields = {f.name for f in dataclass_fields(Skill)} + skill_data = {k: v for k, v in skill_data.items() if k in valid_fields} + instance._skills[str(skill_id)] = Skill(**skill_data) + + sections_payload = payload.get("sections", {}) + if isinstance(sections_payload, dict): + normalized_sections: dict[str, list[str]] = {} + for section, ids in sections_payload.items(): + if not isinstance(ids, Iterable) or isinstance(ids, (str, bytes)): + continue + normalized_sections[str(section)] = [str(item) for item in ids] + instance._sections = normalized_sections + next_id_value = payload.get("next_id", 0) + instance._next_id = ( + int(cast(Union[int, str], next_id_value)) + if next_id_value is not None + else 0 + ) + similarity_decisions_payload = payload.get("similarity_decisions", {}) + if isinstance(similarity_decisions_payload, dict): + for pair_key_str, decision_value in similarity_decisions_payload.items(): + if isinstance(decision_value, dict): + pair_ids = frozenset(str(pair_key_str).split(",")) + instance._similarity_decisions[pair_ids] = SimilarityDecision( + **decision_value + ) + + # Prefer the explicit serialized section ordering, but rebuild if needed. + if not instance._sections: + for skill in instance._skills.values(): + instance._sections.setdefault(skill.section, []).append(skill.id) + + return instance + + def dumps(self, exclude_embeddings: bool = False) -> str: + return json.dumps( + self.to_dict(exclude_embeddings=exclude_embeddings), + ensure_ascii=False, + indent=2, + ) + + @classmethod + def loads(cls, data: str) -> "Skillbook": + payload = json.loads(data) + if not isinstance(payload, dict): + raise ValueError("Skillbook serialization must be a JSON object.") + return cls.from_dict(payload) + + def save_to_file(self, path: str, exclude_embeddings: bool = False) -> None: + file_path = Path(path) + sidecar_path = _embedding_sidecar_path(file_path) + file_path.parent.mkdir(parents=True, exist_ok=True) + with file_path.open("w", encoding="utf-8") as f: + f.write(self.dumps(exclude_embeddings=True)) + + if exclude_embeddings: + return + + embeddings = { + skill.id: skill.embedding + for skill in self._skills.values() + if skill.embedding is not None + } + if not embeddings: + if sidecar_path.exists(): + sidecar_path.unlink() + return + + import numpy as np + + sidecar_path.parent.mkdir(parents=True, exist_ok=True) + arrays = { + skill_id: np.asarray(embedding, dtype="float32") + for skill_id, embedding in embeddings.items() + } + np.savez_compressed(sidecar_path, **arrays) # type: ignore[arg-type] + + @classmethod + def load_from_file(cls, path: str) -> "Skillbook": + file_path = Path(path) + if not file_path.exists(): + raise FileNotFoundError(f"Skillbook file not found: {path}") + with file_path.open("r", encoding="utf-8") as f: + skillbook = cls.loads(f.read()) + + sidecar_path = _embedding_sidecar_path(file_path) + if not sidecar_path.exists(): + return skillbook + + import numpy as np + + with np.load(sidecar_path) as embeddings: + for skill_id in embeddings.files: + skill = skillbook.get_skill(skill_id) + if skill is not None: + skill.embedding = embeddings[skill_id].astype("float32").tolist() + return skillbook + + # ------------------------------------------------------------------ # + # Update application + # ------------------------------------------------------------------ # + + def apply_update(self, update: UpdateBatch) -> None: + with self._lock: + for operation in update.operations: + self._apply_operation(operation) + + def _apply_operation(self, operation: UpdateOperation) -> None: + op_type = operation.type.upper() + if op_type == "ADD": + self.add_skill( + section=operation.section, + issue=operation.issue or "", + keywords=operation.keywords, + insight=operation.insight, + skill_id=operation.skill_id, + insight_source=operation.insight_source, + ) + elif op_type == "UPDATE": + if operation.skill_id is None: + return + self.update_skill( + operation.skill_id, + issue=operation.issue if operation.issue is not None else _UNSET, + keywords=operation.keywords if operation.keywords else _UNSET, + insight=operation.insight if operation.insight is not None else _UNSET, + insight_source=operation.insight_source, + ) + elif op_type == "TAG": + if operation.skill_id is None: + return + delta = int(operation.metadata.get("delta", 0)) + if delta > 0: + delta = 1 + elif delta < 0: + delta = -1 + self.tag_skill( + operation.skill_id, + cast(Literal[1, -1, 0], delta), + insight_source=operation.insight_source, + ) + elif op_type == "REMOVE": + if operation.skill_id is None: + return + self.remove_skill( + operation.skill_id, insight_source=operation.insight_source + ) + + # ------------------------------------------------------------------ # + # Presentation + # ------------------------------------------------------------------ # + + def as_prompt(self) -> str: + parts: List[str] = [] + for section in ("context", "harness"): + skill_ids = self._sections.get(section, []) + section_skills = [ + self._skills[sid] for sid in skill_ids if self._skills[sid].active + ] + if not section_skills: + continue + parts.append(f"## {section}") + for skill in section_skills: + parts.append(f"- [{skill.id}]") + parts.append(f" Keywords: {', '.join(skill.keywords)}") + parts.append(f" Issue: {skill.issue}") + if skill.insight: + parts.append(f" Insight: {skill.insight}") + parts.append("") + return "\n".join(parts).rstrip() + + def stats(self) -> Dict[str, object]: + active_skills = [skill for skill in self._skills.values() if skill.active] + by_section: dict[str, int] = {section: 0 for section in VALID_SECTIONS} + for skill in active_skills: + by_section[skill.section] = by_section.get(skill.section, 0) + 1 + return { + "sections": len( + [section for section, count in by_section.items() if count] + ), + "skills": len(self._skills), + "active_skills": len(active_skills), + "by_section": by_section, + } + + # ------------------------------------------------------------------ # + # Insight source analysis + # ------------------------------------------------------------------ # + + def source_map(self) -> Dict[str, List[Dict[str, Any]]]: + with self._lock: + result: Dict[str, List[Dict[str, Any]]] = {} + for skill_id, skill in self._skills.items(): + if skill.occurrences: + result[skill_id] = _serialize_sources(skill.occurrences) + return result + + def source_summary(self) -> Dict[str, Any]: + with self._lock: + epochs: Dict[Optional[int], int] = {} + source_systems: Dict[str, int] = {} + trace_uids: Dict[str, int] = {} + sample_questions: Dict[str, int] = {} + total = 0 + for skill in self._skills.values(): + for src in skill.occurrences: + total += 1 + epochs[src.epoch] = epochs.get(src.epoch, 0) + 1 + source_systems[src.source_system] = ( + source_systems.get(src.source_system, 0) + 1 + ) + trace_uids[src.trace_uid] = trace_uids.get(src.trace_uid, 0) + 1 + sq = src.sample_question or "" + if sq: + sample_questions[sq] = sample_questions.get(sq, 0) + 1 + return { + "total_sources": total, + "epochs": epochs, + "source_systems": source_systems, + "trace_uids": trace_uids, + "sample_questions": sample_questions, + } + + def source_filter( + self, + *, + epoch: Optional[int] = None, + sample_question: Optional[str] = None, + trace_uid: Optional[str] = None, + trace_id: Optional[str] = None, + source_system: Optional[str] = None, + ) -> Dict[str, List[Dict[str, Any]]]: + with self._lock: + result: Dict[str, List[Dict[str, Any]]] = {} + for skill_id, skill in self._skills.items(): + matches = [] + for src in skill.occurrences: + if epoch is not None and src.epoch != epoch: + continue + if trace_uid is not None and src.trace_uid != trace_uid: + continue + if trace_id is not None and src.trace_id != trace_id: + continue + if source_system is not None and src.source_system != source_system: + continue + if sample_question is not None: + sq = src.sample_question or "" + if sample_question.lower() not in sq.lower(): + continue + matches.append(src.to_dict()) + if matches: + result[skill_id] = matches + return result + + # ------------------------------------------------------------------ # + # Internal + # ------------------------------------------------------------------ # + + def _generate_id(self, section: SkillSection) -> str: + self._next_id += 1 + section_prefix = section.split()[0].lower() + return f"{section_prefix}-{self._next_id:05d}" diff --git a/ace/deduplication/__init__.py b/ace/deduplication/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0269044d3faa722652ef3a687f02e9a2df39c1bb --- /dev/null +++ b/ace/deduplication/__init__.py @@ -0,0 +1,23 @@ +"""Skill deduplication subsystem for ACE framework.""" + +from .detector import SimilarityDetector +from .manager import DeduplicationManager +from .operations import ( + ConsolidationOperation, + DeleteOp, + KeepOp, + MergeOp, + UpdateOp, + apply_consolidation_operations, +) + +__all__ = [ + "DeduplicationManager", + "SimilarityDetector", + "ConsolidationOperation", + "MergeOp", + "DeleteOp", + "KeepOp", + "UpdateOp", + "apply_consolidation_operations", +] diff --git a/ace/deduplication/detector.py b/ace/deduplication/detector.py new file mode 100644 index 0000000000000000000000000000000000000000..5a666f98e41f56e088cf4abac9c4facb84b92832 --- /dev/null +++ b/ace/deduplication/detector.py @@ -0,0 +1,238 @@ +"""Similarity detection for skill deduplication.""" + +from __future__ import annotations + +import importlib +import logging +import threading +from typing import TYPE_CHECKING, List, Optional, Tuple + +from ..protocols.deduplication import DeduplicationConfig + +if TYPE_CHECKING: + from ..core.skillbook import Skill + from ..core.skillbook import Skillbook + +logger = logging.getLogger(__name__) + + +def _has(module: str) -> bool: + """Return True if *module* can be imported.""" + try: + importlib.import_module(module) + return True + except ImportError: + return False + + +class SimilarityDetector: + """Detect similar skill pairs using cosine similarity on embeddings.""" + + def __init__(self, config: DeduplicationConfig | None = None) -> None: + self.config = config or DeduplicationConfig() + self._model: object | None = None # lazy sentence-transformers model + self._model_lock = threading.Lock() + + # ------------------------------------------------------------------ + # Single / batch embedding computation + # ------------------------------------------------------------------ + + def compute_embedding(self, text: str) -> Optional[List[float]]: + """Compute embedding for a single text.""" + if self.config.embedding_provider == "litellm": + return self._embed_litellm(text) + return self._embed_st(text) + + def compute_embeddings_batch(self, texts: List[str]) -> List[Optional[List[float]]]: + """Compute embeddings for multiple texts (more efficient).""" + if not texts: + return [] + if self.config.embedding_provider == "litellm": + return self._embed_batch_litellm(texts) + return self._embed_batch_st(texts) + + # ------------------------------------------------------------------ + # LiteLLM provider + # ------------------------------------------------------------------ + + def _embed_litellm(self, text: str) -> Optional[List[float]]: + if not _has("litellm"): + logger.warning("LiteLLM not available for embeddings") + return None + try: + import litellm + + response = litellm.embedding( + model=self.config.embedding_model, input=[text] + ) + return response.data[0]["embedding"] + except Exception as e: + logger.warning( + "Failed to compute embedding via LiteLLM (%s): %s", type(e).__name__, e + ) + return None + + def _embed_batch_litellm(self, texts: List[str]) -> List[Optional[List[float]]]: + if not _has("litellm"): + logger.warning("LiteLLM not available for embeddings") + return [None] * len(texts) + try: + import litellm + + response = litellm.embedding(model=self.config.embedding_model, input=texts) + return [item["embedding"] for item in response.data] + except Exception as e: + logger.warning( + "Failed to compute batch embeddings via LiteLLM (%s): %s", + type(e).__name__, + e, + ) + return [None] * len(texts) + + # ------------------------------------------------------------------ + # sentence-transformers provider + # ------------------------------------------------------------------ + + def _embed_st(self, text: str) -> Optional[List[float]]: + if not _has("sentence_transformers"): + logger.warning("sentence-transformers not available for embeddings") + return None + try: + model = self._get_st_model() + embedding = model.encode(text, convert_to_numpy=True) + return embedding.tolist() + except Exception as e: + logger.warning( + "Failed to compute embedding via sentence-transformers (%s): %s", + type(e).__name__, + e, + ) + return None + + def _embed_batch_st(self, texts: List[str]) -> List[Optional[List[float]]]: + if not _has("sentence_transformers"): + logger.warning("sentence-transformers not available for embeddings") + return [None] * len(texts) + try: + model = self._get_st_model() + embeddings = model.encode(texts, convert_to_numpy=True) + return [emb.tolist() for emb in embeddings] + except Exception as e: + logger.warning( + "Failed to compute batch embeddings via sentence-transformers (%s): %s", + type(e).__name__, + e, + ) + return [None] * len(texts) + + def _get_st_model(self): + """Lazy-load the sentence-transformers model (thread-safe).""" + if self._model is None: + with self._model_lock: + if self._model is None: # double-check after acquiring lock + from sentence_transformers import SentenceTransformer + + self._model = SentenceTransformer(self.config.local_model_name) + return self._model + + # ------------------------------------------------------------------ + # Cosine similarity + # ------------------------------------------------------------------ + + def cosine_similarity(self, a: List[float], b: List[float]) -> float: + """Compute cosine similarity between two embedding vectors.""" + if not _has("numpy"): + # Pure-Python fallback + dot = sum(x * y for x, y in zip(a, b)) + norm_a = sum(x * x for x in a) ** 0.5 + norm_b = sum(x * x for x in b) ** 0.5 + if norm_a == 0 or norm_b == 0: + return 0.0 + return dot / (norm_a * norm_b) + + import numpy as np + + a_arr = np.array(a) + b_arr = np.array(b) + dot = np.dot(a_arr, b_arr) + norm_a = np.linalg.norm(a_arr) + norm_b = np.linalg.norm(b_arr) + if norm_a == 0 or norm_b == 0: + return 0.0 + return float(dot / (norm_a * norm_b)) + + # ------------------------------------------------------------------ + # High-level API + # ------------------------------------------------------------------ + + def ensure_embeddings(self, skillbook: "Skillbook") -> int: + """Ensure all active skills have embeddings computed. + + Returns: + Number of new embeddings computed. + """ + needs = [s for s in skillbook.skills() if s.embedding is None] + if not needs: + return 0 + + texts = [s.embedding_text() for s in needs] + embeddings = self.compute_embeddings_batch(texts) + + count = 0 + for skill, embedding in zip(needs, embeddings): + if embedding is not None: + skill.embedding = embedding + count += 1 + + logger.info("Computed %d embeddings for skills", count) + return count + + def detect_similar_pairs( + self, + skillbook: "Skillbook", + threshold: float | None = None, + ) -> List[Tuple["Skill", "Skill", float]]: + """Find all skill pairs with similarity >= *threshold*. + + Returns: + Sorted list of ``(skill_a, skill_b, similarity)`` tuples + (descending by score). + """ + threshold = threshold or self.config.similarity_threshold + similar_pairs: List[Tuple["Skill", "Skill", float]] = [] + + skills = skillbook.skills(include_invalid=False) + + if self.config.within_section_only: + sections: dict[str, list] = {} + for skill in skills: + sections.setdefault(skill.section, []).append(skill) + for section_skills in sections.values(): + similar_pairs.extend( + self._find_similar(section_skills, skillbook, threshold) + ) + else: + similar_pairs = self._find_similar(skills, skillbook, threshold) + + similar_pairs.sort(key=lambda x: x[2], reverse=True) + return similar_pairs + + def _find_similar( + self, + skills: List["Skill"], + skillbook: "Skillbook", + threshold: float, + ) -> List[Tuple["Skill", "Skill", float]]: + pairs: List[Tuple["Skill", "Skill", float]] = [] + for i, skill_a in enumerate(skills): + if skill_a.embedding is None: + continue + for skill_b in skills[i + 1 :]: + if skill_b.embedding is None: + continue + if skillbook.has_keep_decision(skill_a.id, skill_b.id): + continue + sim = self.cosine_similarity(skill_a.embedding, skill_b.embedding) + if sim >= threshold: + pairs.append((skill_a, skill_b, sim)) + return pairs diff --git a/ace/deduplication/manager.py b/ace/deduplication/manager.py new file mode 100644 index 0000000000000000000000000000000000000000..708f6a2633e6131953d4c81412d09f81d96f5e88 --- /dev/null +++ b/ace/deduplication/manager.py @@ -0,0 +1,165 @@ +"""DeduplicationManager — coordinates similarity detection and consolidation.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +from ..protocols.deduplication import DeduplicationConfig +from .detector import SimilarityDetector +from .operations import ( + ConsolidationOperation, + ConsolidationOpType, + DeleteOp, + KeepOp, + MergeOp, + UpdateOp, + apply_consolidation_operations, +) +from .prompts import format_pair_for_logging, generate_similarity_report + +if TYPE_CHECKING: + from ..core.skillbook import Skillbook + +logger = logging.getLogger(__name__) + + +class DeduplicationManager: + """Manages similarity detection and feeds info to SkillManager. + + Coordinates: + 1. Computing / updating embeddings for skills + 2. Detecting similar skill pairs + 3. Generating similarity reports for the SkillManager prompt + 4. Parsing and applying consolidation operations + + Satisfies :class:`DeduplicationManagerLike` via ``get_similarity_report``. + """ + + def __init__(self, config: DeduplicationConfig | None = None) -> None: + self.config = config or DeduplicationConfig() + self.detector = SimilarityDetector(self.config) + + # ------------------------------------------------------------------ + # DeduplicationManagerLike interface + # ------------------------------------------------------------------ + + def get_similarity_report(self, skillbook: "Skillbook") -> Optional[str]: + """Generate a similarity report for the SkillManager prompt. + + Should be called **before** the SkillManager runs. + + Returns: + Formatted report, or ``None`` if no similar pairs found or + deduplication is disabled. + """ + if not self.config.enabled: + return None + + self.detector.ensure_embeddings(skillbook) + similar_pairs = self.detector.detect_similar_pairs(skillbook) + + if len(similar_pairs) < self.config.min_pairs_to_report: + if similar_pairs: + logger.debug( + "Found %d similar pairs, below threshold of %d", + len(similar_pairs), + self.config.min_pairs_to_report, + ) + return None + + logger.info("Found %d similar skill pairs", len(similar_pairs)) + for skill_a, skill_b, similarity in similar_pairs: + logger.debug(format_pair_for_logging(skill_a, skill_b, similarity)) + + return generate_similarity_report(similar_pairs) + + # ------------------------------------------------------------------ + # Consolidation operation parsing / application + # ------------------------------------------------------------------ + + def parse_consolidation_operations( + self, response_data: Dict[str, Any] + ) -> List[ConsolidationOperation]: + """Parse consolidation operations from SkillManager response data.""" + operations: List[ConsolidationOperation] = [] + raw_ops = response_data.get("consolidation_operations", []) + + if not isinstance(raw_ops, list): + logger.warning("consolidation_operations is not a list") + return operations + + for raw_op in raw_ops: + if not isinstance(raw_op, dict): + continue + raw_type = raw_op.get("type", "").upper() + + try: + op_type = ConsolidationOpType(raw_type) + except ValueError: + logger.warning("Unknown consolidation operation type: %r", raw_type) + continue + + try: + if op_type is ConsolidationOpType.MERGE: + operations.append( + MergeOp( + source_ids=raw_op.get("source_ids", []), + merged_content=raw_op.get("merged_content", ""), + keep_id=raw_op.get("keep_id", ""), + reasoning=raw_op.get("reasoning", ""), + ) + ) + elif op_type is ConsolidationOpType.DELETE: + operations.append( + DeleteOp( + skill_id=raw_op.get("skill_id", ""), + reasoning=raw_op.get("reasoning", ""), + ) + ) + elif op_type is ConsolidationOpType.KEEP: + operations.append( + KeepOp( + skill_ids=raw_op.get("skill_ids", []), + differentiation=raw_op.get("differentiation", ""), + reasoning=raw_op.get("reasoning", ""), + ) + ) + elif op_type is ConsolidationOpType.UPDATE: + operations.append( + UpdateOp( + skill_id=raw_op.get("skill_id", ""), + new_content=raw_op.get("new_content", ""), + reasoning=raw_op.get("reasoning", ""), + ) + ) + except Exception as e: + logger.warning( + "Failed to parse consolidation operation (%s): %s", + type(e).__name__, + e, + ) + + logger.info("Parsed %d consolidation operations", len(operations)) + return operations + + def apply_operations( + self, + operations: List[ConsolidationOperation], + skillbook: "Skillbook", + ) -> None: + """Apply consolidation operations to the skillbook.""" + if not operations: + return + logger.info("Applying %d consolidation operations", len(operations)) + apply_consolidation_operations(operations, skillbook) + + def apply_operations_from_response( + self, + response_data: Dict[str, Any], + skillbook: "Skillbook", + ) -> List[ConsolidationOperation]: + """Parse and apply consolidation operations in one step.""" + operations = self.parse_consolidation_operations(response_data) + self.apply_operations(operations, skillbook) + return operations diff --git a/ace/deduplication/operations.py b/ace/deduplication/operations.py new file mode 100644 index 0000000000000000000000000000000000000000..6bac719b049194e8618737354477419105b1724c --- /dev/null +++ b/ace/deduplication/operations.py @@ -0,0 +1,162 @@ +"""Consolidation operations for skill deduplication.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field +from datetime import datetime, timezone +from enum import Enum +from typing import TYPE_CHECKING, List, Literal, Union + +if TYPE_CHECKING: + from ..core.skillbook import Skillbook + +logger = logging.getLogger(__name__) + + +class ConsolidationOpType(str, Enum): + """Valid consolidation operation types from SkillManager responses.""" + + MERGE = "MERGE" + DELETE = "DELETE" + KEEP = "KEEP" + UPDATE = "UPDATE" + + +@dataclass +class MergeOp: + """Merge multiple skills into one. + + Combines helpful/harmful counts from all source skills into the kept skill. + Other skills are soft-deleted. + """ + + type: Literal["MERGE"] = "MERGE" + source_ids: List[str] = field(default_factory=list) + merged_content: str = "" + keep_id: str = "" + reasoning: str = "" + + +@dataclass +class DeleteOp: + """Soft-delete a skill as redundant.""" + + type: Literal["DELETE"] = "DELETE" + skill_id: str = "" + reasoning: str = "" + + +@dataclass +class KeepOp: + """Keep both skills separate (they serve different purposes).""" + + type: Literal["KEEP"] = "KEEP" + skill_ids: List[str] = field(default_factory=list) + differentiation: str = "" + reasoning: str = "" + + +@dataclass +class UpdateOp: + """Update a skill's content to differentiate it.""" + + type: Literal["UPDATE"] = "UPDATE" + skill_id: str = "" + new_content: str = "" + reasoning: str = "" + + +ConsolidationOperation = Union[MergeOp, DeleteOp, KeepOp, UpdateOp] + + +# --------------------------------------------------------------------------- +# Apply helpers +# --------------------------------------------------------------------------- + + +def apply_consolidation_operations( + operations: List[ConsolidationOperation], + skillbook: "Skillbook", +) -> None: + """Apply a list of consolidation operations to a skillbook.""" + for op in operations: + if isinstance(op, MergeOp): + _apply_merge(op, skillbook) + elif isinstance(op, DeleteOp): + _apply_delete(op, skillbook) + elif isinstance(op, KeepOp): + _apply_keep(op, skillbook) + elif isinstance(op, UpdateOp): + _apply_update(op, skillbook) + else: + logger.warning("Unknown operation type: %s", type(op)) + + +def _apply_merge(op: MergeOp, skillbook: "Skillbook") -> None: + keep_skill = skillbook.get_skill(op.keep_id) + if keep_skill is None: + logger.warning("MERGE: Keep skill %s not found", op.keep_id) + return + + for source_id in op.source_ids: + if source_id == op.keep_id: + continue + source = skillbook.get_skill(source_id) + if source is None: + logger.warning("MERGE: Source skill %s not found", source_id) + continue + skillbook.remove_skill(source_id, soft=True) + logger.info("MERGE: Soft-deleted %s into %s", source_id, op.keep_id) + + if op.merged_content: + if keep_skill.section == "context": + keep_skill.insight = op.merged_content + else: + keep_skill.issue = op.merged_content + + keep_skill.embedding = None + keep_skill.updated_at = datetime.now(timezone.utc).isoformat() + logger.info("MERGE: Completed merge into %s", op.keep_id) + + +def _apply_delete(op: DeleteOp, skillbook: "Skillbook") -> None: + skill = skillbook.get_skill(op.skill_id) + if skill is None: + logger.warning("DELETE: Skill %s not found", op.skill_id) + return + skillbook.remove_skill(op.skill_id, soft=True) + logger.info("DELETE: Soft-deleted %s", op.skill_id) + + +def _apply_keep(op: KeepOp, skillbook: "Skillbook") -> None: + if len(op.skill_ids) < 2: + logger.warning("KEEP: Need at least 2 skill IDs") + return + + from ..core.skillbook import SimilarityDecision + + for i, id_a in enumerate(op.skill_ids): + for id_b in op.skill_ids[i + 1 :]: + decision = SimilarityDecision( + decision="KEEP", + reasoning=op.reasoning or op.differentiation, + decided_at=datetime.now(timezone.utc).isoformat(), + similarity_at_decision=0.0, + ) + skillbook.set_similarity_decision(id_a, id_b, decision) + logger.info("KEEP: Stored decision for (%s, %s)", id_a, id_b) + + +def _apply_update(op: UpdateOp, skillbook: "Skillbook") -> None: + skill = skillbook.get_skill(op.skill_id) + if skill is None: + logger.warning("UPDATE: Skill %s not found", op.skill_id) + return + if skill.section == "context": + skill.insight = op.new_content + else: + skill.issue = op.new_content + skill.embedding = None + skill.updated_at = datetime.now(timezone.utc).isoformat() + logger.info("UPDATE: Updated content of %s", op.skill_id) diff --git a/ace/deduplication/prompts.py b/ace/deduplication/prompts.py new file mode 100644 index 0000000000000000000000000000000000000000..d20d41f932d3b9ccf17046a2b86ec607342c429a --- /dev/null +++ b/ace/deduplication/prompts.py @@ -0,0 +1,118 @@ +"""Prompts and report generation for skill deduplication.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Tuple + +if TYPE_CHECKING: + from ..core.skillbook import Skill + +SIMILARITY_REPORT_HEADER = """ +## Similar Skills Detected + +The following skill pairs have high semantic similarity and may need consolidation. +Work your way methodologically through each pair. For each pair, you can decide to: +- **MERGE**: Combine into a single improved skill (provide merged_content and keep_id) +- **DELETE**: Remove one as redundant (specify skill_id to delete) +- **KEEP**: Keep both separate if they serve different purposes (explain differentiation) +- **UPDATE**: Refine one skill's content to clarify the difference (provide new_content) + +""" + +PAIR_TEMPLATE = """### Pair {index}: {similarity:.0%} similar +**Skill A** [{id_a}] +> {content_a} + +**Skill B** [{id_b}] +> {content_b} + +""" + + +def generate_similarity_report( + similar_pairs: List[Tuple["Skill", "Skill", float]], +) -> str: + """Generate a human-readable similarity report for the SkillManager. + + Args: + similar_pairs: List of (skill_a, skill_b, similarity_score) tuples. + + Returns: + Formatted report string to include in SkillManager prompt. + """ + if not similar_pairs: + return "" + + parts = [SIMILARITY_REPORT_HEADER] + + for i, (skill_a, skill_b, similarity) in enumerate(similar_pairs, 1): + parts.append( + PAIR_TEMPLATE.format( + index=i, + similarity=similarity, + id_a=skill_a.id, + content_a=skill_a.insight or skill_a.issue, + id_b=skill_b.id, + content_b=skill_b.insight or skill_b.issue, + ) + ) + + parts.append(""" +## Consolidation Operations Format + +Include consolidation operations in your response under a `consolidation_operations` key. +Each operation should have a `type` field and relevant fields for that type: + +```json +{ + "consolidation_operations": [ + { + "type": "MERGE", + "source_ids": ["skill-id-1", "skill-id-2"], + "keep_id": "skill-id-1", + "merged_content": "Improved combined strategy text", + "reasoning": "Why merging improves the skillbook" + }, + { + "type": "DELETE", + "skill_id": "skill-id-to-remove", + "reasoning": "Why this skill is redundant" + }, + { + "type": "KEEP", + "skill_ids": ["skill-id-1", "skill-id-2"], + "differentiation": "How they differ in purpose", + "reasoning": "Why both are needed" + }, + { + "type": "UPDATE", + "skill_id": "skill-id-to-update", + "new_content": "Refined content with context tag like [Batch] or [API]", + "reasoning": "How this clarifies the distinction" + } + ] +} +``` + +**Guidelines:** +- MERGE when skills are semantically identical or near-identical +- KEEP when they serve different contexts (batch vs real-time, different APIs, etc.) +- UPDATE to add context tags like "[Batch Jobs]" or "[User-Facing API]" to differentiate +- DELETE only when one is clearly redundant with no unique value + +""") + + return "".join(parts) + + +def format_pair_for_logging( + skill_a: "Skill", skill_b: "Skill", similarity: float +) -> str: + """Format a single pair for logging output.""" + text_a = skill_a.insight or skill_a.issue + text_b = skill_b.insight or skill_b.issue + return ( + f"[{skill_a.id}] '{text_a[:50]}...' " + f"<-> [{skill_b.id}] '{text_b[:50]}...' " + f"({similarity:.0%} similar)" + ) diff --git a/ace/implementations/__init__.py b/ace/implementations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6099dca2ec1aff86e7f3d384b87370303150a72c --- /dev/null +++ b/ace/implementations/__init__.py @@ -0,0 +1,7 @@ +"""Concrete LLM-based role implementations for ACE steps.""" + +from .agent import Agent +from .reflector import Reflector +from .skill_manager import SkillManager + +__all__ = ["Agent", "Reflector", "SkillManager"] diff --git a/ace/implementations/agent.py b/ace/implementations/agent.py new file mode 100644 index 0000000000000000000000000000000000000000..3f92f261ad6bf5eb848b06e0bc80a3295d1e72cc --- /dev/null +++ b/ace/implementations/agent.py @@ -0,0 +1,117 @@ +"""Agent — produces answers using the current skillbook of strategies. + +Uses PydanticAI for structured output validation with automatic retry +and error feedback. +""" + +from __future__ import annotations + +import logging +from typing import Any, Optional, Union + +from pydantic_ai import Agent as PydanticAgent +from pydantic_ai.settings import ModelSettings + +from ..core.context import SkillbookView +from ..core.outputs import AgentOutput +from ..core.skillbook import Skillbook +from ..providers.pydantic_ai import resolve_model +from .helpers import format_optional +from .prompts import AGENT_PROMPT + +logger = logging.getLogger(__name__) + + +class Agent: + """Produces answers using the current skillbook of strategies. + + The Agent is one of three core ACE roles. It takes a question and + uses the accumulated strategies in the skillbook to produce reasoned + answers. + + Args: + model: Model identifier string. Supports any LiteLLM model + (e.g. ``"gpt-4o-mini"``, ``"openrouter/anthropic/claude-3.5-sonnet"``) + or a PydanticAI-native identifier (e.g. ``"openai:gpt-4o"``). + prompt_template: Custom prompt template (defaults to + :data:`AGENT_PROMPT`). + max_retries: Maximum retries for structured output validation. + PydanticAI feeds validation errors back to the LLM on retry. + model_settings: Optional PydanticAI ``ModelSettings`` for + temperature, max_tokens, etc. + + Example:: + + agent = Agent("gpt-4o-mini") + output = agent.generate( + question="What is the capital of France?", + context="Answer concisely", + skillbook=skillbook, + ) + print(output.final_answer) # "Paris" + """ + + def __init__( + self, + model: str, + *, + prompt_template: str = AGENT_PROMPT, + max_retries: int = 3, + model_settings: ModelSettings | None = None, + ) -> None: + self._prompt_template = prompt_template + self._agent = PydanticAgent( + resolve_model(model), + output_type=AgentOutput, + retries=max_retries, + model_settings=model_settings, + defer_model_check=True, + ) + + def generate( + self, + *, + question: str, + context: Optional[str], + skillbook: Union[SkillbookView, Skillbook], + reflection: Optional[str] = None, + **kwargs: Any, + ) -> AgentOutput: + """Generate an answer using skillbook strategies. + + This method signature matches :class:`AgentLike`. + + Args: + question: The question to answer. + context: Additional context or requirements. + skillbook: Current skillbook (needs ``as_prompt``). + reflection: Optional reflection from a previous attempt. + **kwargs: Accepted for protocol compatibility but not forwarded. + + Returns: + :class:`AgentOutput` with reasoning, final_answer, and + cited skill_ids. + """ + prompt = self._prompt_template.format( + skillbook=skillbook.as_prompt() or "(empty skillbook)", + reflection=format_optional(reflection), + question=question, + context=format_optional(context), + ) + + result = self._agent.run_sync(prompt) + output = result.output + output.raw = _extract_usage(result) + return output + + +def _extract_usage(result: Any) -> dict[str, Any]: + """Extract usage metadata from a PydanticAI run result.""" + usage = result.usage() + return { + "usage": { + "prompt_tokens": usage.input_tokens or 0, + "completion_tokens": usage.output_tokens or 0, + "total_tokens": usage.total_tokens or 0, + }, + } diff --git a/ace/implementations/helpers.py b/ace/implementations/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..453bddf16b739c2942a008d18a6cc79cd7bd8404 --- /dev/null +++ b/ace/implementations/helpers.py @@ -0,0 +1,65 @@ +"""Shared utilities for ACE role implementations.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, List, Optional, Sequence + +if TYPE_CHECKING: + from ..core.context import SkillbookView + from ..core.skillbook import Skillbook + + SkillbookLike = Skillbook | SkillbookView + + +def extract_cited_skill_ids(text: str) -> List[str]: + """Extract skill IDs cited in text using ``[id-format]`` notation. + + Parses ``[section-00001]`` patterns and returns unique IDs in order + of first appearance. + + Args: + text: Text containing skill citations. + + Returns: + Deduplicated list of skill IDs preserving first-occurrence order. + + Example:: + + >>> extract_cited_skill_ids("Following [general-00042], I verified the data.") + ['general-00042'] + """ + matches = re.findall(r"\[([a-zA-Z_]+-\d+)\]", text) + return list(dict.fromkeys(matches)) + + +def format_optional(value: Optional[str]) -> str: + """Return *value* or ``"(none)"`` when falsy.""" + return value or "(none)" + + +def make_skillbook_excerpt(skillbook: "SkillbookLike", skill_ids: Sequence[str]) -> str: + """Build a compact excerpt of cited skills. + + Args: + skillbook: Skillbook to look up skills in. + skill_ids: Ordered skill IDs cited by the agent. + + Returns: + One ``[id] content`` line per unique cited skill found. + """ + lines: list[str] = [] + seen: set[str] = set() + for skill_id in skill_ids: + if skill_id in seen: + continue + skill = skillbook.get_skill(skill_id) + if skill: + seen.add(skill_id) + excerpt = f"[{skill.id}] Issue: {skill.issue}" + if skill.insight: + excerpt += f" | Insight: {skill.insight}" + if skill.keywords: + excerpt += f" | Keywords: {', '.join(skill.keywords)}" + lines.append(excerpt) + return "\n".join(lines) diff --git a/ace/implementations/prompts.py b/ace/implementations/prompts.py new file mode 100644 index 0000000000000000000000000000000000000000..babf9c62e28833745c97d468648974113e8735e1 --- /dev/null +++ b/ace/implementations/prompts.py @@ -0,0 +1,542 @@ +"""Default v2.1 prompt templates for ACE role implementations. + +The ``{current_date}`` placeholder is filled at import time so callers +never need to worry about it. +""" + +from __future__ import annotations + +from datetime import datetime + +# --------------------------------------------------------------------------- +# Shared constants +# --------------------------------------------------------------------------- + +SKILLBOOK_USAGE_INSTRUCTIONS = """\ +**How to use these strategies:** +- Review skills relevant to your current task +- **When applying a strategy, cite its ID in your reasoning** (e.g., "Following [content_extraction-00001], I will extract the title...") + - Citations enable precise tracking of strategy effectiveness + - Makes reasoning transparent and auditable + - Improves learning quality through accurate attribution +- Prioritize strategies with high success rates (helpful > harmful) +- Apply strategies when they match your context +- Adapt general strategies to your specific situation +- Learn from both successful patterns and failure avoidance + +**Important:** These are learned patterns, not rigid rules. Use judgment.\ +""" + + +def wrap_skillbook_for_external_agent(skillbook) -> str: + """Wrap skillbook skills with explanation for external agents. + + This is the canonical function for injecting skillbook context into + external agentic systems (browser-use, custom agents, LangChain, etc.). + + Args: + skillbook: Skillbook instance with learned strategies. + + Returns: + Formatted text with skillbook strategies and usage instructions, + or empty string if skillbook has no skills. + """ + skills = skillbook.skills() + if not skills: + return "" + + skill_text = skillbook.as_prompt() + + return f""" +## Available Strategic Knowledge (Learned from Experience) + +The following strategies have been learned from previous task executions. +Each skill shows its success rate based on helpful/harmful feedback: + +{skill_text} + +{SKILLBOOK_USAGE_INSTRUCTIONS} +""" + + +# --------------------------------------------------------------------------- +# Agent prompt — v2.1 +# --------------------------------------------------------------------------- + +_CURRENT_DATE = datetime.now().strftime("%Y-%m-%d") + +AGENT_PROMPT = ( + """\ +# Identity and Metadata +You are ACE Agent v2.1, an expert problem-solving agent. +Prompt Version: 2.1.0 +Current Date: """ + + _CURRENT_DATE + + """ +Mode: Strategic Problem Solving with Skillbook Application + +## Core Mission +You are an advanced problem-solving agent that applies accumulated strategic knowledge from the skillbook to solve problems and generate accurate, well-reasoned answers. Your success depends on methodical strategy application with transparent reasoning. + +## Core Responsibilities +1. Apply accumulated skillbook strategies to solve problems +2. Show complete step-by-step reasoning with clear justification +3. Execute strategies to produce accurate, complete answers +4. Cite specific skills when applying strategic knowledge + +## Skillbook Application Protocol + +### Step 1: Analyze Available Strategies +Examine the skillbook and identify relevant skills: +{skillbook} + +### Step 2: Consider Recent Reflection +Integrate learnings from recent analysis: +{reflection} + +### Step 3: Process the Question +Question: {question} +Additional Context: {context} + +### Step 4: Generate Solution +Follow this EXACT procedure: + +1. **Strategy Selection** + - Scan ALL skillbook skills for relevance to current question + - Select skills whose content directly addresses the current problem + - Apply ALL relevant skills that contribute to the solution + - Use natural language understanding to determine relevance + - NEVER apply skills that are irrelevant to the question domain + - If no relevant skills exist, state "no_applicable_strategies" + +2. **Problem Decomposition** + - Break complex problems into atomic sub-problems + - Identify prerequisite knowledge needed + - State assumptions explicitly + +3. **Strategy Application** + - ALWAYS cite specific skill IDs before applying them + - Show how each strategy applies to this specific case + - Apply strategies in logical sequence based on problem-solving flow + - Execute the strategy to solve the problem + - NEVER mix unrelated strategies + +4. **Solution Execution** + - Number every reasoning step + - Show complete problem-solving process + - Apply strategies to reach concrete answer + - Include all intermediate calculations and logic steps + - NEVER stop at methodology without solving + +## CRITICAL REQUIREMENTS + +**Specificity Constraints:** +When skillbook says "use [option/tool/service]": +- Valid: "use a [option/tool/service] like those mentioned in instructions" +- Invalid: "use [option/tool/service] specifically" (unless skill explicitly recommends that tool) +- Default to generic implementation unless skill explicitly recommends specific tool/method/service +- Default to generic implementation unless evidence shows one option is superior to alternatives + +**MUST** follow these rules: +- ALWAYS include complete reasoning chain with numbered steps +- ALWAYS cite specific skill IDs when applying strategies +- ALWAYS show complete problem-solving process +- ALWAYS execute strategies to reach concrete answers +- ALWAYS include all intermediate calculations or logic steps +- ALWAYS provide direct, complete answers to the question + +**NEVER** do these: +- Say "based on the skillbook" without specific skill citations +- Provide partial or incomplete answers +- Skip intermediate calculations or logic steps +- Mix unrelated strategies +- Include meta-commentary like "I will now..." +- Guess or fabricate information +- Specify particular tools/services/methods unless explicitly in skillbook skills +- Add implementation details not supported by cited strategies +- Choose specific options without evidence they work better than alternatives +- Fabricate preferences between equivalent tools/methods/approaches +- Over-specify when general guidance is sufficient +- Stop at methodology without executing the solution + +## Output Format + +Return a SINGLE valid JSON object with this EXACT schema: + +{{ + "reasoning": "<detailed step-by-step chain of thought with numbered steps and skill citations (e.g., 'Following [general-00042], I will...'). Cite skill IDs inline whenever applying a strategy.>", + "step_validations": ["<validation1>", "<validation2>"], + "final_answer": "<complete, direct answer to the question>", + "answer_confidence": 0.95, + "quality_check": {{ + "addresses_question": true, + "reasoning_complete": true, + "citations_provided": true + }} +}} + +## Examples + +### Good Example: +Skillbook contains: +- [skill_023] "Break down multiplication using distributive property" +- [skill_045] "Verify calculations by working backwards" + +Question: "What is 15 x 24?" + +{{ + "reasoning": "1. Problem: Calculate 15 x 24. 2. Following [skill_023], applying multiplication decomposition. 3. Breaking down: 15 x 24 = 15 x (20 + 4). 4. Computing: 15 x 20 = 300. 5. Computing: 15 x 4 = 60. 6. Adding: 300 + 60 = 360. 7. Using [skill_045] for verification: 360 / 24 = 15", + "step_validations": ["Decomposition applied correctly", "Calculations verified", "Answer confirmed"], + "final_answer": "360", + "answer_confidence": 1.0, + "quality_check": {{ + "addresses_question": true, + "reasoning_complete": true, + "citations_provided": true + }} +}} + +### Bad Example (DO NOT DO THIS): +{{ + "reasoning": "Using the skillbook strategies, the answer is clear.", + "final_answer": "360" +}} + +## Error Recovery + +If JSON generation fails: +1. Verify all required fields are present +2. Ensure proper escaping of special characters +3. Validate answer_confidence is between 0 and 1 +4. Ensure no trailing commas +5. Maximum retry attempts: 3 + +Begin response with `{{` and end with `}}` +""" +) + + +# --------------------------------------------------------------------------- +# Reflector prompt — v2.1 +# --------------------------------------------------------------------------- + +REFLECTOR_PROMPT = """\ +# QUICK REFERENCE +Role: ACE Reflector v2.1 - Senior Analytical Reviewer +Mission: Diagnose generator performance and extract concrete learnings +Success Metrics: Root cause identification, Evidence-based tagging, Actionable insights +Analysis Mode: Diagnostic Review with Atomicity Scoring +Key Rule: Extract SPECIFIC experiences, not generalizations + +# CORE MISSION +You are a senior reviewer who diagnoses generator performance through systematic analysis, extracting concrete, actionable learnings from actual execution experiences to improve future performance. + +## WHEN TO PERFORM ANALYSIS + +MANDATORY - Analyze when: +- Agent produces any output (correct or incorrect) +- Environment provides execution feedback +- Ground truth is available for comparison +- Strategy application can be evaluated + +CRITICAL - Deep analysis when: +- Agent fails to reach correct answer +- New error pattern emerges +- Strategy misapplication detected +- Performance degrades unexpectedly + +## INPUT ANALYSIS CONTEXT + +### Performance Data +Question: {question} +Model Reasoning: {reasoning} +Model Prediction: {prediction} +Ground Truth: {ground_truth} +Environment Feedback: {feedback} + +### Skillbook Context +Strategies Applied: +{skillbook_excerpt} + +## MANDATORY DIAGNOSTIC PROTOCOL + +Execute in STRICT priority order - apply FIRST matching condition: + +### Priority 1: SUCCESS_CASE_DETECTED +WHEN: prediction matches ground truth AND feedback positive +- REQUIRED: Identify contributing strategies +- MANDATORY: Extract reusable patterns +- CRITICAL: Tag helpful skills with evidence + +### Priority 2: CALCULATION_ERROR_DETECTED +WHEN: mathematical/logical error in reasoning chain +- REQUIRED: Pinpoint exact error location (step number) +- MANDATORY: Identify root cause (e.g., order of operations) +- CRITICAL: Specify correct calculation method + +### Priority 3: STRATEGY_MISAPPLICATION_DETECTED +WHEN: correct strategy but execution failed +- REQUIRED: Identify execution divergence point +- MANDATORY: Explain correct application +- Tag as "neutral" (strategy OK, execution failed) + +### Priority 4: WRONG_STRATEGY_SELECTED +WHEN: inappropriate strategy for problem type +- REQUIRED: Explain strategy-problem mismatch +- MANDATORY: Identify correct strategy type +- CONSIDER: Was specific tool/method choice the root cause? +- EVALUATE: If strategy recommended specific approach, assess if that approach is consistently problematic +- Tag as "harmful" for this context + +### Priority 5: MISSING_STRATEGY_DETECTED +WHEN: no applicable strategy existed +- REQUIRED: Define missing capability precisely +- MANDATORY: Describe strategy that would help +- CONSIDER: If failure involved tool/method choice, note which approaches to avoid vs recommend +- Mark for skill_manager to create + +## EXPERIENCE-DRIVEN CONCRETE EXTRACTION + +CRITICAL: Extract from ACTUAL EXECUTION, not theoretical principles: + +### MANDATORY Extraction Requirements +From environment feedback, extract: +- **Specific Tools**: "used tool X" not "used appropriate tools" +- **Exact Metrics**: "completed in 4 steps" not "completed efficiently" +- **Precise Failures**: "timeout at 30s" not "took too long" +- **Concrete Actions**: "called function_name()" not "processed data" +- **Actual Errors**: "ConnectionError at line 42" not "connection issues" + +### Transform Observations -> Specific Learnings +GOOD: "Tool X completed task in 4 steps with 98% accuracy" +BAD: "Tool was effective" + +GOOD: "Method Y failed at step 3 due to TypeError on null value" +BAD: "Method had issues" + +GOOD: "API rate limit hit after 60 requests/minute" +BAD: "Hit rate limits" + +### CHOICE-OUTCOME PATTERN RECOGNITION +CONSIDER when relevant: Choice-outcome relationships +- What specific tool/method/approach was selected? +- Did the choice contribute to success or failure? +- Are there patterns suggesting some options work better than others? +- Would a different choice have likely prevented this failure? + +## ATOMICITY SCORING + +Score each extracted learning (0-100%): + +### Scoring Factors +- **Base Score**: 100% +- **Deductions**: + - Each "and/also/plus": -15% + - Metadata phrases ("user said", "we discussed"): -40% + - Vague terms ("something", "various"): -20% + - Temporal refs ("yesterday", "earlier"): -15% + - Over 15 words: -5% per extra word + +### Quality Levels +- **Excellent (95-100%)**: Single atomic concept +- **Good (85-95%)**: Mostly atomic, minor improvement possible +- **Fair (70-85%)**: Acceptable but could be split +- **Poor (40-70%)**: Too compound, needs splitting +- **Rejected (<40%)**: Too vague or compound + +## CRITICAL REQUIREMENTS + +### MANDATORY Include +- Specific error identification with line/step numbers +- Root cause analysis beyond surface symptoms +- Actionable corrections with concrete examples +- Atomicity scores for extracted learnings + +### FORBIDDEN Phrases +- "The model was wrong" +- "Should have known better" +- "Obviously incorrect" +- "Failed to understand" +- "Misunderstood the question" + +## OUTPUT FORMAT + +CRITICAL: Return ONLY valid JSON: + +{{ + "reasoning": "<systematic analysis with numbered points>", + "error_identification": "<specific error or 'none' if correct>", + "root_cause_analysis": "<underlying reason for error or success>", + "correct_approach": "<detailed correct method with example>", + "key_insight": "<most valuable reusable learning>" +}} + +## GOOD Analysis Example + +{{ + "reasoning": "1. Agent attempted 15x24 using decomposition. 2. ERROR at step 3: Calculated 15x20=310 instead of 300.", + "error_identification": "Arithmetic error in multiplication at step 3 of reasoning chain", + "root_cause_analysis": "Multiplication error: 15x2=30, so 15x20=300, not 310", + "correct_approach": "15x24 = 15x20 + 15x4 = 300 + 60 = 360", + "key_insight": "Double-check multiplications involving tens" +}} + +MANDATORY: Begin response with `{{` and end with `}}` +""" + + +# --------------------------------------------------------------------------- +# SkillManager prompts — agentic (tool-calling) +# --------------------------------------------------------------------------- + +SKILL_MANAGER_SYSTEM = """\ +You are the SkillManager — the skillbook architect. You mutate a live skillbook \ +via atomic tools (add_skill, update_skill, remove_skill, tag_skill). Every change \ +is applied immediately; there is no staging or review stage after you return. \ +Take explicit, auditable actions. + +Key rules: +- Every skill belongs to exactly one pipeline-facing section: `context` or `harness`. +- Fine-grained topic labels live in `keywords`, not in `section`. +- Every ADD / UPDATE must include a concrete `issue`. +- `context` skills require an `insight`; `harness` skills may omit it if there is \ +no reliable workaround yet. +- `insight` is the only part of the skill that gets injected into the downstream \ +agent's prompt. It must be self-sufficient: it carries both the trigger condition \ +(when this applies) AND the action to take. Do NOT assume the agent will see `issue` \ +or `keywords` — they are retrieval / metadata only. +- `insight` shape: one trigger + one action. Structure your `insight` as \ +`<single trigger condition>, <single imperative action>`. 15–50 words. Imperative \ +voice. Positive framing by default; negation only for hard prohibitions paired with \ +the positive alternative. No hedging ("try to", "consider", "it may help"). Embed a \ +one-line concrete example only when the rule is about format / shape (regex, schema, \ +tool-argument structure); skip examples for purely behavioral rules. + + Good — atomic, one trigger one action: + ``` + When <trigger condition>, <imperative action> — <optional one-line clarification \ +or verbatim phrase>. + ``` + + Bad — compound, three triggers chained: + ``` + When <trigger A>, <action 1>, then if <trigger B>, <action 2>, and after \ +<event C>, <action 3>. + ``` + The bad shape bundles three behaviors firing under three different triggers. It \ +must be split into three separate ADD calls — one skill per trigger. If you find \ +yourself stringing multiple "When…" / "if…" clauses together, or writing "and after \ +that, when X…", you are about to make this mistake. Call ADD multiple times — once \ +per trigger — even when the triggers feel logically chained in the reflection. + + Sequential steps under a single trigger are NOT compound and should stay in one \ +skill. Example: `"When upgrading cabin class on a multi-leg reservation, compute \ +new_total = sum(price_per_leg × passengers) for ALL legs, subtract original_total, \ +then verify within budget before requesting confirmation."` — one trigger, three \ +ordered procedural steps, one skill. The diagnostic question is: *"could each step \ +fire independently of the others under a different trigger?"* If yes → split. If no \ +(the steps must always co-occur under the same trigger) → one skill. + + Two skills with the SAME action and only surface-different triggers are ONE skill, \ +not two. Example of over-decomposition (do NOT do): \ +`"When user claims a membership tier that conflicts with system, use system record"` \ ++ `"When user claims a flight date that conflicts with system, use system record"` — \ +both have identical action ("use system record") and only the named field differs. \ +Merge into a single skill whose trigger names the category: \ +`"When a user-claimed value (membership tier, reservation ID, flight date, etc.) \ +conflicts with system data, use the system record as authoritative."` Split only \ +when the ACTION genuinely differs, not when only the trigger surface differs. + +- Cross-trace generalization gate. Before writing a broad/categorical skill that \ +subsumes existing narrow ones (or UPDATEing to broaden a trigger across domains), \ +ALL four must hold: + 1. ≥3 confirming surface instances exist across ≥2 distinct domains (visible via \ +search_skills). + 2. The broad rule has ≥1 named slot the agent fills at runtime (e.g. `<scope>`, \ +`<api-name>`). Pure principles ("be careful with scope") fail. + 3. The action references no API-specific names, fields, or error codes — if it does, \ +keep narrow. + 4. The trigger has a verifiable runtime check (e.g. "does the user's stated scope \ +differ from the tool's documented scope?"). Vibe triggers ("when something feels off") \ +fail. + If any fails, write/keep narrow per-domain skills. If all pass, write the broad skill \ +with 1-2 concrete mini-examples in `issue` (NOT in `insight`, to keep it under 50 \ +words), and leave contributing narrow skills in place this pass — do not delete on the \ +same write. +- Write `issue` as the problem plus applicability inline. Start narrow unless the \ +reflection clearly supports broader scope. `issue` is metadata for retrieval and \ +SkillManager judgment; it does not need to be self-sufficient prose. +- Choose 1-5 short stable keywords (domain, subsystem, API, behavior category). +- Before ADD, call search_skills to check for near-duplicates. If a semantically \ +similar skill exists, prefer UPDATE. +- If search_skills shows the same issue across multiple domains, UPDATE the existing \ +skill with a broader issue statement and refreshed keywords instead of adding another \ +duplicate. +- When deciding to broaden via UPDATE, compare the existing skill's `issue` / `insight` \ +(read via read_skill) against the current reflection. If both target the same root \ +cause but in different niches, rewrite `issue` so it covers both — the prior niche AND \ +the current one — without losing specificity. `occurrences` is supporting context, not \ +the trigger; the trigger is conceptual overlap visible in the skill content itself. +- Counters live on skills. Retrieve them via read_skill / search_skills. Use them \ +as one input among several when judging a skill — never as a hard removal trigger. \ +A heavily-used skill can legitimately accumulate harmful_count while still being \ +net-positive. REMOVE only when the reflection's evidence shows the skill is \ +consistently misleading or unsalvageable. +- You decide helpful / harmful / neutral for each skill in `injected_skill_ids` \ +from the outcome + reflection. Call tag_skill with delta +1 (helpful), -1 (harmful), \ +or 0 (neutral) for skills you have evidence about. Do not tag skills you have no \ +evidence for. +- Extract strategies ONLY from the reflection's description of task execution. \ +Never extract from your own instructions or examples. +- Reject vague meta-commentary ("be careful", "consider"), agent-observations \ +("the agent does X"), and unqualified "always" / "never". +- If you have no actionable change, call no mutation tools and return a short \ +reasoning explaining why.""" + + +SKILL_MANAGER_PROMPT = """\ +<progress> +{progress} +</progress> + +<stats> +{stats} +</stats> + +<injected_skill_ids> +Skills rendered into the agent's prompt this run (tagging scope): +{injected_skill_ids} +</injected_skill_ids> + +<reflections> +{reflections} +</reflections> + +<task_context> +{question_context} +</task_context> + +<workflow> +0. Check `stats.skills` above. If it's 0, skip every `search_skills` / `read_skill` call — there is nothing to find. +1. Read the reflection. Identify concrete patterns with evidence. +2. Tag only the skills the reflection provides direct evidence for — that is, \ +skills the reflection actually implicates (cites, contradicts, builds on, or \ +attributes the outcome to). Do NOT iterate over `injected_skill_ids` and tag every \ +entry; that is not evidence-based. If the reflection mentions no specific skills, \ +skip tagging entirely. The tagging scope is `injected_skill_ids` — that is the \ +universe you are allowed to tag from, not the set you must tag. +3. For genuinely novel patterns: call search_skills first. If no near-duplicate \ +exists, call add_skill with `section`, `issue`, `keywords`, and `insight` when needed. +4. For improvements to existing skills: call update_skill with a rewritten `issue` \ +and updated `keywords`; include `insight` when the actionable guidance should change. +5. If the reflection's evidence shows a skill is consistently misleading or \ +unsalvageable, call remove_skill with a clear reason. Do not remove based on \ +harmful_count alone. +6. When done, produce your structured output summarizing your reasoning. +</workflow> + +<size_management> +If stats show skillbook > 50 skills, prioritize UPDATE over ADD and look for \ +merge opportunities around overlapping issue + insight pairs. +</size_management> +""" diff --git a/ace/implementations/reflector.py b/ace/implementations/reflector.py new file mode 100644 index 0000000000000000000000000000000000000000..04477fe011ef23940745b2b0dc68f4665a1e511d --- /dev/null +++ b/ace/implementations/reflector.py @@ -0,0 +1,131 @@ +"""Reflector — analyzes agent outputs to extract lessons and improve strategies. + +Uses PydanticAI for structured output validation with automatic retry +and error feedback. +""" + +from __future__ import annotations + +import logging +from typing import Any, Optional, Union + +from pydantic_ai import Agent as PydanticAgent +from pydantic_ai.settings import ModelSettings + +from ..core.context import SkillbookView +from ..core.outputs import AgentOutput, ReflectorOutput +from ..core.skillbook import Skillbook +from ..providers.pydantic_ai import resolve_model +from .helpers import format_optional, make_skillbook_excerpt +from .prompts import REFLECTOR_PROMPT + +logger = logging.getLogger(__name__) + + +class Reflector: + """Analyzes agent outputs to extract lessons and improve strategies. + + The Reflector is the second ACE role. It analyzes the Agent's output + and environment feedback to understand what went right or wrong, + classifying which skillbook skills were helpful, harmful, or neutral. + + This implementation supports **SIMPLE** mode only (single-pass + reflection). Recursive mode is handled by :mod:`ace.steps.rr`. + + The Reflector produces pure analysis — it does not classify or tag + skills. Skill-effectiveness decisions are made by the SkillManager + using ``ctx.injected_skill_ids`` plus the reflection. + + Args: + model: Model identifier string. Supports any LiteLLM model + or PydanticAI-native identifier. + prompt_template: Custom prompt template (defaults to + :data:`REFLECTOR_PROMPT`). + max_retries: Maximum retries for structured output validation. + model_settings: Optional PydanticAI ``ModelSettings``. + + Example:: + + reflector = Reflector("gpt-4o-mini") + reflection = reflector.reflect( + question="What is 2+2?", + agent_output=agent_output, + skillbook=skillbook, + ground_truth="4", + feedback="Correct!", + ) + print(reflection.key_insight) + """ + + def __init__( + self, + model: str, + *, + prompt_template: str = REFLECTOR_PROMPT, + max_retries: int = 3, + model_settings: ModelSettings | None = None, + ) -> None: + self._prompt_template = prompt_template + self._agent = PydanticAgent( + resolve_model(model), + output_type=ReflectorOutput, + retries=max_retries, + model_settings=model_settings, + defer_model_check=True, + ) + + def reflect( + self, + *, + question: str, + agent_output: AgentOutput, + skillbook: Union[SkillbookView, Skillbook], + ground_truth: Optional[str] = None, + feedback: Optional[str] = None, + injected_skill_ids: tuple[str, ...] = (), + **kwargs: Any, + ) -> ReflectorOutput: + """Analyze agent performance and extract learnings. + + This method signature matches :class:`ReflectorLike`. + + Args: + question: The original question. + agent_output: The agent's output to analyze. + skillbook: Current skillbook (needs ``get_skill``). + ground_truth: Expected correct answer (if available). + feedback: Environment feedback text. + injected_skill_ids: Skills rendered into the agent's prompt + this run. Used to build the "Strategies Applied" excerpt. + **kwargs: Accepted for protocol compatibility but not forwarded. + + Returns: + :class:`ReflectorOutput` with pure analysis (no tagging). + """ + skillbook_excerpt = make_skillbook_excerpt(skillbook, injected_skill_ids) + + if skillbook_excerpt: + skillbook_context = f"Strategies Applied:\n{skillbook_excerpt}" + else: + skillbook_context = "(No strategies injected - outcome-based learning)" + + prompt = self._prompt_template.format( + question=question, + reasoning=agent_output.reasoning, + prediction=agent_output.final_answer, + ground_truth=format_optional(ground_truth), + feedback=format_optional(feedback), + skillbook_excerpt=skillbook_context, + ) + + result = self._agent.run_sync(prompt) + output = result.output + usage = result.usage() + output.raw = { + "usage": { + "prompt_tokens": usage.input_tokens or 0, + "completion_tokens": usage.output_tokens or 0, + "total_tokens": usage.total_tokens or 0, + }, + } + return output diff --git a/ace/implementations/rr/__init__.py b/ace/implementations/rr/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a5ad372d86b82dc5bd4140932a7e5c40b314086a --- /dev/null +++ b/ace/implementations/rr/__init__.py @@ -0,0 +1,13 @@ +"""Recursive Reflector implementation. + +Config, prompts, and tools live here. The ``RecursiveReflector`` class +and the pipeline step ``RRStep`` live in ``ace.steps.rr_step``. +""" + +from .config import RecursiveConfig as RRConfig +from .tools import RRDeps + +__all__ = [ + "RRConfig", + "RRDeps", +] diff --git a/ace/implementations/rr/config.py b/ace/implementations/rr/config.py new file mode 100644 index 0000000000000000000000000000000000000000..99416ad0db8110d7adb2ad6cb65a8af7dc8d51d4 --- /dev/null +++ b/ace/implementations/rr/config.py @@ -0,0 +1,19 @@ +"""Configuration for recursive reflector.""" + +from dataclasses import dataclass +from typing import Literal + +from ...core.recursive_agent import AgenticConfig + + +@dataclass +class RecursiveConfig(AgenticConfig): + """Configuration for the Recursive Reflector. + + Inherits all fields from :class:`AgenticConfig`. Overrides + ``max_output_chars`` for larger trace outputs. + """ + + max_output_chars: int = 50_000 + cache_prompts: bool = True + cache_ttl: Literal["5m", "1h"] = "5m" diff --git a/ace/implementations/rr/prompts.py b/ace/implementations/rr/prompts.py new file mode 100644 index 0000000000000000000000000000000000000000..88c3366e75f27aa5f82276c78cedec56b5e4ce06 --- /dev/null +++ b/ace/implementations/rr/prompts.py @@ -0,0 +1,173 @@ +""" +Recursive reflector prompts — tool-calling version for PydanticAI. + +execute_code is the primary evidence-gathering tool; think is a scratch +prose channel for working notes during the run; recurse decomposes large +sub-problems. Conclusions live only in the final ReflectorOutput. +""" + +REFLECTOR_RECURSIVE_SYSTEM = """\ +You are a recursive agent that analyze agent execution traces and extract learnings that become strategies for future agents. + +## Tools +- `execute_code(code)` — Python workbench. Variables persist across calls. Pre-loaded: `json`, `re`, `collections`, `datetime`. Built-in helper: `register_helper(name, source, description)` defines a function that persists here AND is auto-injected into every `recurse` child you later spawn. **If the same logic would be useful in multiple children, `register_helper` it ONCE — do not re-paste the same code into each child's `context_code`.** +- `think(thought)` — narrate your working state (what you just confirmed, what's next). Keeps prose out of Python stdout. +- `recurse(prompt, context_code?)` — spawn a child session in its own context window. Child inherits a copy of your sandbox **values** (lists, dicts, strings, numbers) plus any `register_helper`-registered helpers; it does NOT inherit plain `def` functions and does NOT see your conversation, so `prompt` must be self-contained. To give a child a function, either `register_helper(...)` it (persists for all future children) or paste the `def` into the child's `context_code` (one-off, exec'd in the child sandbox before its session starts). Children themselves may call `recurse` further as long as the tool appears in their tool list (max-depth gated). Multiple `recurse` calls in one assistant turn dispatch in parallel. + - `search_skillbook(query, top_k)` / `read_skill(skill_id)` — inspect the skillbook (skip when empty). + +## Working rules +- `execute_code` carries DATA (parse, filter, slice, print compact dicts/counts). `think` carries NARRATION. The final `ReflectorOutput` is the only sink that propagates downstream — conclusions live ONLY there, never in print statements. +- **Minimize turns.** Every turn replays the full conversation, so each tool call costs more than the last. Batch independent reads in one turn (parallel `execute_code` / `recurse` calls). Plan a small number of high-yield steps. Stop as soon as you have enough — do NOT keep verifying just because budget remains. +- When you write a `recurse` prompt: ONE deliverable, name the sandbox variables the child should inspect, state the return shape. Do NOT dump multi-question task lists onto a child. **Pre-extract the data the child needs into flat sandbox variables yourself** (e.g. `tool_calls_per_turn = {{...}}`) — don't make the child re-parse raw structure you already understand. +- **Fan out when your task decomposes.** If your work contains numbered sub-questions (1, 2, 3, …) or asks for the same extraction across N items, that IS a decomposition — dispatch N parallel `recurse` calls in ONE assistant turn instead of walking serially. Applies at any depth: don't bundle independent sub-tasks into a single mega-child. +- When you have enough evidence, stop using tools and return your final answer (root sessions return a structured `ReflectorOutput`; child sessions return free-form text answering exactly what their prompt asked for — no JSON schema, just the result).""" + + +REFLECTOR_RECURSIVE_PROMPT = """\ +<purpose> +You a recursive agent which analyze an agent's execution trace to extract learnings for a **skillbook** — strategies +injected into future agents' prompts. Identify WHAT the agent did that mattered and WHY. + +The trace shape is whatever was given to you — it varies. Use `execute_code` to discover. +Use `recurse` to fan-out the work to sub-agents. +</purpose> + +<sandbox> +## Variables (available in execute_code) +| Variable | Description | +|----------|-------------| +| `traces` | {traces_description} ({trace_size_chars} chars) | +| `skillbook` | Current strategies (string, {skillbook_length} chars) | + +Pre-loaded modules: `json`, `re`, `collections`, `datetime`. + +{data_summary} + +## Tools +| Tool | Purpose | +|------|---------| +| `execute_code(code)` | Python workspace for evidence extraction. Variables persist across calls. | +| `think(thought, evidence_refs?)` | Narrate your working state during the run — what you just confirmed, what you're checking next, brief observations. Use this freely whenever you'd otherwise be tempted to print prose from Python. | +| `recurse(prompt, context_code?)` | Spawn a child session with its own sandbox for a sub-problem that needs multi-step investigation. Children share the overall budget. | +| `search_skillbook(query, top_k)` | Search the skillbook for existing strategies. | +| `read_skill(skill_id)` | Read the full payload for a specific skill. | + +## Channel routing +Three channels, three jobs: + +- **`execute_code` carries data.** It parses, filters, computes, assigns variables, prints compact structured artifacts (dicts, slices, counts, check results). Whenever you reach for `print("=== HEADING ===")` or a hand-written narrative, stop — that text belongs in `think`, not here. +- **`think` carries your running narration.** "Numbers look off — checking the breakdown next." "Mismatch confirmed; one more cross-check and I'm done." "This branch is a side path, focusing on the main decision." Use it freely. It is the right home for everything you would naturally say while working. +- **`ReflectorOutput`** is the only sink that propagates downstream. The final conclusion, root cause, correct approach, and key insight live there — and only there. + +**Parallel tool calls.** When you have multiple independent things to check, issue them in a single turn instead of one-at-a-time. Examples: several `search_skillbook` queries with different angles, `read_skill` calls for a batch of IDs, independent `execute_code` reads of disjoint slices, or `recurse` calls dispatched into independent sub-investigations (the natural way to handle huge or batched inputs that need to be split). Don't parallelize calls that depend on each other or share variable writes. + +Bad — manually written report inside `execute_code`: +```python +print("=== KEY DECISIONS ===") +print("1. <hand-written narrative point>") +print("=== AGENT'S MAIN ACTIONS ===") +print("Step 1: <prose the model already knows>") +``` + +Good — `execute_code` extracts evidence as data; `think` carries the narration; `ReflectorOutput` carries the conclusion: +```python +# Extract the pieces of evidence you need into a compact result, then print it. +# The exact keys depend on what the trace contains — discover that yourself. +result = {{ + "claim_to_verify": "<what the agent stated>", + "actual_value": "<what the data shows>", + "matches": False, +}} +print(result) +``` +Then `think("primary check confirmed; investigating the secondary path next")`. The eventual conclusion goes into `reasoning` / `key_insight` of the final output. +</sandbox> + +<strategy> +Start by evaluating your startegy, how should you proceed? +Try to find the important parts of the input and extract them, filter out the noise from the trace to reduce the size substantially. If clean trace, you can skip this, do this in the beginnning. +**Minimize turns.** Every assistant turn replays the full conversation, so each `execute_code` call costs more than the last. Plan a small number of high-yield steps, batch independent reads in a single turn (parallel `execute_code` / `recurse` / `search_skillbook` calls), and stop once you have enough evidence — do NOT keep verifying just because budget remains. + +Explore the trace via `execute_code`, store reusable state in sandbox variables, and verify the agent's claims against the data it received. + +IMPORTANT: Use `recurse` when the input trace is too large (more than roughly 400k chars) or sub-problem needs its own multi-step investigation. This will allow to fan-out the work and make it scalable. +YOU MUST USE SUB AGENTS EXTENSIVELY USING RECURSE EARLY ON, REDUCING CONTEXT WINDOW SIZE. + +`register_helper(...)` any extractor functions you'll want to reuse — plain `def` is NOT inherited by children, registered helpers ARE. +Compute a flat `briefing` dict/list with the pre-extracted slices the child actually needs. Don't make the child re-parse raw structure you already understand. +Built-in helper inside `execute_code`: `register_helper(name, source, description)` defines a Python function that persists in this sandbox AND is auto-injected into every child you later spawn via `recurse`. + +*Recurse call:* the child sees nothing of your conversation, so the prompt is self-contained: + **One question** — what is the child answering? (e.g. "Identify which turns had empty LLM outputs and what triggered them."). + **Instructions** — Use clear instructions if needed to steer behavior + **Inputs** — name the sandbox variables (e.g. `briefing`) and the registered helpers the child should use (e.g. `extract_llm_data`). + **Method hint (optional)** — a one-line nudge if the approach isn't obvious. + **Return shape** — exactly what to put back (e.g. "Return a list of `{{turn_idx, trigger, prompt_excerpt}}` dicts.") + **Hand off Context** — Any additional context that could be useful to get the task done faster + +**Name what you find.** Whenever you discover or compute something useful (a slice, a count, a parsed structure), assign it to a named sandbox variable instead of just printing it. Use distinctive names you'll remember across many turns. +To list the current sandbox vars: `print([k for k in dir() if not k.startswith('_') and k not in ('json','re','collections','datetime')])`. + +Even when the agent's run looks like a clean win, there are still lessons. Look for both: +- **Success patterns** — concrete behavior the agent used that produced the result. These are transferable strategies for future agents to replicate. +- **Subtle deviations** — places the agent did something wrong along the way, even if it didn't break the final outcome. These are still failure-mode lessons. + +**Skim the skillbook early.** If `skillbook_length` above is 0, the skillbook is empty — **do not call `search_skillbook` or `read_skill` at all**, there is nothing to find. Otherwise run `search_skillbook` queries at the *start* of your investigation — for the topic of the trace, the kind of error you suspect, and the agent's apparent strategy. This tells you what's already known and lets you frame the agent's behavior against existing skills as you analyze, not after. Repeat searches as new hypotheses form. In `reasoning`, explicitly call out the relationship between the agent's behavior and the skillbook: +- **Skill that failed to prevent the mistake**: an existing skill already covers this lesson, yet the agent still made the error. Useful signal that the skill needs sharpening, repositioning, or stronger emphasis. +- **Skill that may have caused the mistake**: a skill the agent had access to may have nudged it toward the wrong behavior. Useful signal that the skill is misleading or being misapplied. +- **Overlap or contradiction**: the lesson you're proposing already exists, partially exists, or contradicts an existing skill. + +If a side investigation isn't going to change the conclusion, drop it explicitly with a brief `think` note rather than letting it dangle. + +You have {max_iterations} requests for this session. Child sessions consume from the same budget. Partial results beat running out of requests — produce output when you have enough evidence. +</strategy> + +<output_rules> +- If the agent's claims contradict the data it received, lead the reflection with that contradiction — it is the primary finding, not a footnote. +- The final ReflectorOutput must come from this agent. Do not print the lesson, key insight, or final synthesis from Python; those belong in the structured output. +- Extract only the parts of the trace that directly support your conclusion, not the whole thing. + +## Final ReflectorOutput fields (all required) +- **`reasoning`**: What you found, how you found it, what the data shows. +- **`error_identification`**: The exact failure. If nothing went wrong, say "none". +- **`root_cause_analysis`**: WHY the error occurred — the misunderstood concept or missing process. +- **`correct_approach`**: What the agent should have done instead. Specific and actionable. +- **`key_insight`**: The single most important principle to remember. +</output_rules> + +Now analyze the task. +""" + +# --------------------------------------------------------------------------- +# Online mode: skillbook inspection guidance +# --------------------------------------------------------------------------- + +RR_SKILLBOOK_INSPECTION_SECTION = """\ +<skillbook_inspection> +## Skillbook Inspection (Online Mode) + +The agent had access to a skillbook of strategies. The IDs rendered into the agent's prompt \ +this run are listed as `injected_skill_ids` in the trace dict. Use the `search_skillbook(query, \ +top_k)` and `read_skill(skill_id)` tools to inspect these strategies while forming your analysis. + +Narrate what you observe — which strategies appear to have been covered, contradicted, or \ +missing from the injected set — so the SkillManager has context when deciding what to add, \ +update, remove, or tag. Do NOT prescribe mutations or classifications; that is the \ +SkillManager's job. +</skillbook_inspection> +""" + + +# --------------------------------------------------------------------------- +# Compaction prompts +# --------------------------------------------------------------------------- + +COMPACTION_SUMMARY_PROMPT = """\ +Summarize your analysis progress. Structure your response with these sections: + +1. **What you've done**: Steps completed, tools used, key decisions made. +2. **Findings so far**: Concrete results, computed values, identified patterns. +3. **Remaining work**: What hasn't been done yet. +4. **Current direction**: What you were investigating when this summary was requested. + +Be concise but preserve all concrete results and variable names.""" diff --git a/ace/implementations/rr/tools.py b/ace/implementations/rr/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..8509e174321e2543bb9cfbf44aefdbfa17273385 --- /dev/null +++ b/ace/implementations/rr/tools.py @@ -0,0 +1,175 @@ +"""RR-specific tool registrars and dependency container. + +Generic tools (execute_code, recurse) are provided by +:mod:`ace.core.recursive_agent`. This module adds RR-specific +tools and the RR dependency container. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Optional, Union + +from pydantic_ai import ModelRetry, RunContext + +from ace.core.context import SkillbookView +from ace.core.recursive_agent import AgenticDeps +from ace.core.skillbook import Skillbook + +if TYPE_CHECKING: + from pydantic_ai import Agent as PydanticAgent + +from .config import RecursiveConfig + +# ------------------------------------------------------------------ +# Dependency container +# ------------------------------------------------------------------ + + +@dataclass +class RRDeps(AgenticDeps): + """Dependencies injected into RR tool calls via ``RunContext``. + + Extends :class:`AgenticDeps` with RR-specific trace and skillbook fields. + ``sandbox`` is inherited from :class:`AgenticDeps``. + + ``skillbook`` (optional) is the real :class:`Skillbook` — provided so the + read-only ``search_skillbook`` and ``read_skill`` tools can inspect + strategies without the agent having to scan serialized text. + """ + + trace_data: dict[str, Any] = field(default_factory=dict) + skillbook_text: str = "" + skillbook: Optional[Union[Skillbook, SkillbookView]] = None + thoughts: list[dict[str, Any]] = field(default_factory=list) + + +# ------------------------------------------------------------------ +# RR-specific tool registrars +# ------------------------------------------------------------------ + + +def register_output_validator(agent: "PydanticAgent[RRDeps, Any]") -> None: + """Register the standard output validator on any RR agent.""" + + @agent.output_validator + def validate_output(ctx: RunContext[RRDeps], output: Any) -> Any: + """Ensure the agent explored data before concluding.""" + if ctx.deps.iteration < 1: + raise ModelRetry( + "You haven't explored the data enough. " + "Use execute_code first, then provide your final answer." + ) + return output + + +def register_read_skill(agent: "PydanticAgent[RRDeps, Any]") -> None: + """Register the ``read_skill`` read-only tool. + + Returns the full skill payload (including counters) for a given ID, + or a ``not found`` message. No sandbox, no mutation. + """ + + @agent.tool + def read_skill(ctx: RunContext[RRDeps], skill_id: str) -> dict[str, Any]: + """Look up a skill by ID.""" + sb = ctx.deps.skillbook + if sb is None: + return {"error": "skillbook unavailable"} + skill = sb.get_skill(skill_id) + if skill is None: + return {"error": f"skill not found: {skill_id}"} + return { + "id": skill.id, + "section": skill.section, + "keywords": list(skill.keywords), + "issue": skill.issue, + "insight": skill.insight, + "active": skill.active, + "used_count": skill.used_count, + "helpful_count": skill.helpful_count, + "harmful_count": skill.harmful_count, + "neutral_count": skill.neutral_count, + "occurrences": [source.to_dict() for source in skill.occurrences], + } + + +def register_think(agent: "PydanticAgent[RRDeps, Any]") -> None: + """Register the ``think`` narration channel. + + ``think`` is the home for the model's running narration during a + tool-use turn — what it just confirmed, what it is checking next, brief + observations. This keeps prose out of ``execute_code`` stdout, where + Python should only print compact structured evidence. The final + conclusion still belongs in ``ReflectorOutput`` (the only sink that + propagates to the SkillManager); ``think`` notes are surfaced in + ``output.raw["thoughts"]`` for inspection only. + """ + + @agent.tool + def think( + ctx: RunContext[RRDeps], + thought: str, + evidence_refs: list[str] | None = None, + ) -> dict[str, Any]: + """Narrate your working state during the run. + + Use this for mid-run prose: "checking the constraint window next", + "the mismatch is confirmed", "the decisive message is at index 12". + Use it freely — it is the right home for everything you would + naturally say while working. The final conclusion still goes in + ``ReflectorOutput``; reusable data still lives in sandbox variables + via ``execute_code``. + """ + normalized = thought.strip() + if not normalized: + raise ModelRetry("Thought must be non-empty.") + + refs = [ref.strip() for ref in (evidence_refs or []) if ref.strip()] + entry = { + "thought": normalized, + "evidence_refs": refs, + } + ctx.deps.thoughts.append(entry) + return { + "ok": True, + "thought_count": len(ctx.deps.thoughts), + } + + +def register_search_skillbook(agent: "PydanticAgent[RRDeps, Any]") -> None: + """Register the ``search_skillbook`` read-only tool. + + Returns the top-k skills most relevant to the query via embedding + similarity. Falls back to the first k active skills if embeddings are + unavailable. + """ + + @agent.tool + def search_skillbook( + ctx: RunContext[RRDeps], query: str, top_k: int = 5 + ) -> list[dict[str, Any]]: + """Search for skills matching a natural-language query.""" + sb = ctx.deps.skillbook + if sb is None: + return [{"error": "skillbook unavailable"}] + + from ace.implementations.skill_rendering import retrieve_top_k + + actual_sb = sb._sb if isinstance(sb, SkillbookView) else sb + results = retrieve_top_k(actual_sb, query, top_k=top_k) + return [ + { + "id": s.id, + "section": s.section, + "keywords": list(s.keywords), + "issue": s.issue, + "insight": s.insight, + "active": s.active, + "used_count": s.used_count, + "helpful_count": s.helpful_count, + "harmful_count": s.harmful_count, + "neutral_count": s.neutral_count, + } + for s in results + ] diff --git a/ace/implementations/skill_manager.py b/ace/implementations/skill_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..99b8fc6740f14a0bd94136fef9af2e42a7f95bf6 --- /dev/null +++ b/ace/implementations/skill_manager.py @@ -0,0 +1,234 @@ +"""Agentic SkillManager — mutates the skillbook directly via tool calls. + +The SkillManager runs as a :class:`RecursiveAgent` with atomic mutation +tools (``add_skill``, ``update_skill``, ``remove_skill``, ``tag_skill``) +and read-only inspection tools (``search_skills``, ``read_skill``). Each +tool applies its effect to the real :class:`Skillbook` immediately. + +The ``SkillManagerOutput`` returned by :meth:`SkillManager.update_skills` +is a post-hoc **audit log**: the ``reasoning`` comes from the agent's +structured output, and ``operations`` is the sequence of mutations the +tools recorded during the run. ``UpdateStep`` is the sole invocation +point; there is no downstream ``ApplyStep`` — the skillbook has already +been mutated when ``update_skills`` returns. +""" + +from __future__ import annotations + +import json +import logging +from typing import Any, Optional + +from pydantic import BaseModel, ConfigDict, Field +from pydantic_ai.models import Model as PydanticModel +from pydantic_ai.settings import ModelSettings + +from ..core.insight_source import InsightSource +from ..core.outputs import SkillManagerOutput +from ..core.recursive_agent import AgenticConfig, BudgetExhausted, RecursiveAgent +from ..core.skillbook import Skillbook, UpdateBatch +from .prompts import SKILL_MANAGER_PROMPT, SKILL_MANAGER_SYSTEM +from .sm_tools import ( + SMDeps, + register_add_skill, + register_remove_skill, + register_sm_read_skill, + register_sm_search_skills, + register_tag_skill, + register_update_skill, +) + +logger = logging.getLogger(__name__) + + +class SkillManagerReport(BaseModel): + """Structured output the SkillManager emits when it finishes. + + Only ``reasoning`` is produced by the LLM. The audit trail of + executed mutations is collected by the tools on ``SMDeps.operations`` + and spliced into the final :class:`SkillManagerOutput` by + :meth:`SkillManager.update_skills`. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + reasoning: str = Field(..., description="Summary of the actions you took and why.") + + +class SkillManager(RecursiveAgent): + """Transforms reflections into skillbook mutations via atomic tool calls. + + Subclass of :class:`RecursiveAgent` — inherits compaction, recursion, + and budget management. + + The SkillManager is the third ACE role. Tools mutate the real + :class:`Skillbook` directly: there is no staging, and no + ``ApplyStep`` follows ``UpdateStep``. The returned + :class:`SkillManagerOutput` is an audit log of what the tools + already executed. + + Args: + model: Model identifier string (LiteLLM / PydanticAI) or a + pre-built ``Model`` instance. + config: ``AgenticConfig`` — controls request/token budget, + compaction, recursion depth. ``max_requests=1`` approximates + the old one-shot behavior (a single tool turn). + prompt_template: User prompt template (defaults to + :data:`SKILL_MANAGER_PROMPT`). + system_prompt: System prompt (defaults to + :data:`SKILL_MANAGER_SYSTEM`). + model_settings: Optional PydanticAI ``ModelSettings``. + + Example:: + + sm = SkillManager("gpt-4o-mini", config=AgenticConfig(max_requests=20)) + output = sm.update_skills( + reflections=(reflection_output,), + skillbook=skillbook, # real Skillbook, not a view + question_context="Math problem solving", + progress="5/10 correct", + injected_skill_ids=ctx.injected_skill_ids, + ) + # skillbook has already been mutated; output is the audit log + """ + + def __init__( + self, + model: str | PydanticModel, + *, + config: Optional[AgenticConfig] = None, + prompt_template: str = SKILL_MANAGER_PROMPT, + system_prompt: str = SKILL_MANAGER_SYSTEM, + model_settings: ModelSettings | None = None, + ) -> None: + self._prompt_template = prompt_template + + if model_settings is None: + from pydantic_ai.models.bedrock import BedrockModelSettings + + model_settings = BedrockModelSettings( + bedrock_cache_instructions=True, + bedrock_cache_tool_definitions=True, + bedrock_cache_messages=True, + ) + + super().__init__( + model, + output_type=SkillManagerReport, + system_prompt=system_prompt, + config=config or AgenticConfig(), + model_settings=model_settings, + tools=[ + register_sm_search_skills, + register_sm_read_skill, + register_add_skill, + register_update_skill, + register_remove_skill, + register_tag_skill, + ], + tool_names_to_compact=( + "search_skills", + "read_skill", + ), + span_label="sm", + ) + + def update_skills( + self, + *, + reflections: tuple, + skillbook: Skillbook, + question_context: str, + progress: str, + source: InsightSource | None = None, + injected_skill_ids: tuple[str, ...] = (), + **kwargs: Any, + ) -> SkillManagerOutput: + """Run the agent; tools mutate ``skillbook`` as they fire. + + This method signature matches :class:`SkillManagerLike`. + + Args: + reflections: Tuple of Reflector analyses (1-tuple for single, + N-tuple for batch). + skillbook: Real :class:`Skillbook` — mutated in place by the + agent's tools. + question_context: Description of the task domain. + progress: Current progress summary (e.g. ``"5/10 correct"``). + source: Base provenance record for the current learning trace. + If ``None``, mutations are recorded without provenance. + injected_skill_ids: Skills rendered into the Agent's prompt + this run — the tagging scope surfaced to the agent. + **kwargs: Accepted for protocol compatibility but not + forwarded. + + Returns: + :class:`SkillManagerOutput` audit log. The mutations are + already applied; the caller does NOT need to call + ``skillbook.apply_update()``. + """ + reflections_data = [ + { + "reasoning": r.reasoning, + "error_identification": r.error_identification, + "root_cause_analysis": r.root_cause_analysis, + "correct_approach": r.correct_approach, + "key_insight": r.key_insight, + } + for r in reflections + ] + + prompt = self._prompt_template.format( + progress=progress, + stats=json.dumps(skillbook.stats()), + injected_skill_ids=( + json.dumps(list(injected_skill_ids)) if injected_skill_ids else "[]" + ), + reflections=json.dumps(reflections_data, ensure_ascii=False, indent=2), + question_context=question_context, + ) + + deps = SMDeps( + config=self.config, + depth=0, + max_depth=self.config.max_depth, + skillbook=skillbook, + current_source=source, + ) + + from pydantic_ai.messages import CachePoint + + prompt_payload: Any = [prompt, CachePoint(ttl="5m")] + + try: + report, metadata = self.run(deps=deps, prompt=prompt_payload) + reasoning = report.reasoning if report is not None else "" + raw = { + **metadata, + "sm_trace": { + "total_iterations": deps.iteration, + "compactions": metadata.get("compactions", 0), + }, + } + except BudgetExhausted as exc: + logger.warning( + "SkillManager budget exhausted after %d compactions; returning partial audit", + exc.compaction_count, + ) + reasoning = "SkillManager budget exhausted before completing." + raw = { + "timeout": True, + "sm_trace": { + "total_iterations": deps.iteration, + "compactions": exc.compaction_count, + }, + } + except Exception as e: + logger.error("SkillManager failed: %s", e, exc_info=True) + reasoning = f"SkillManager failed: {e}" + raw = {"error": str(e)} + + return SkillManagerOutput( + update=UpdateBatch(reasoning=reasoning, operations=list(deps.operations)), + raw=raw, + ) diff --git a/ace/implementations/skill_rendering.py b/ace/implementations/skill_rendering.py new file mode 100644 index 0000000000000000000000000000000000000000..fc99e2aedf6822e213218c823627904ebc660faf --- /dev/null +++ b/ace/implementations/skill_rendering.py @@ -0,0 +1,182 @@ +"""XML skill rendering and skill retrieval helpers.""" + +from __future__ import annotations + +import logging +import re +from collections import defaultdict +from typing import TYPE_CHECKING, Iterable +from xml.sax.saxutils import escape + +if TYPE_CHECKING: + from ace.core.skillbook import Skill, Skillbook + from ace.deduplication.detector import SimilarityDetector + +logger = logging.getLogger(__name__) + +RRF_K = 60 + + +def _tokenize(text: str) -> list[str]: + return re.findall(r"[a-z0-9_]+", text.lower()) + + +def _normalize_keywords(keywords: Iterable[str] | None) -> list[str]: + if keywords is None: + return [] + normalized: list[str] = [] + seen: set[str] = set() + for keyword in keywords: + text = str(keyword).strip().lower().replace(" ", "_") + if not text or text in seen: + continue + normalized.append(text) + seen.add(text) + return normalized + + +def _keyword_overlap(skill: "Skill", keywords: list[str]) -> int: + if not keywords: + return 0 + return sum(1 for keyword in keywords if keyword in skill.keywords) + + +def render_skills_xml(skills: list["Skill"]) -> str: + """Render skills as XML ``<strategy>`` elements.""" + if not skills: + return "" + + parts: list[str] = [] + for skill in skills: + keyword_attr = ",".join(skill.keywords) + body = [f" <issue>{escape(skill.issue)}</issue>"] + if skill.insight: + body.append(f" <insight>{escape(skill.insight)}</insight>") + body.append(f" <keywords>{escape(keyword_attr)}</keywords>") + parts.append( + f'<strategy id="{escape(skill.id)}" section="{escape(skill.section)}">\n' + + "\n".join(body) + + "\n</strategy>" + ) + + strategies_block = "\n".join(parts) + return ( + f"{strategies_block}\n\n" + "Adapt these strategies to your current situation; " + "they are patterns, not rigid rules." + ) + + +def _lexical_ranking( + skills: list["Skill"], + query: str, +) -> list["Skill"]: + if not skills: + return [] + query_tokens = _tokenize(query) + if not query_tokens: + return skills + + try: + from rank_bm25 import BM25Okapi + + corpus = [_tokenize(skill.embedding_text()) for skill in skills] + bm25 = BM25Okapi(corpus) + scores = bm25.get_scores(query_tokens) + ranked_pairs = sorted( + zip(scores, skills), + key=lambda item: item[0], + reverse=True, + ) + return [skill for _, skill in ranked_pairs] + except Exception as exc: + logger.debug("BM25 unavailable, falling back to token overlap: %s", exc) + query_token_set = set(query_tokens) + ranked_pairs = [] + for skill in skills: + doc_tokens = set(_tokenize(skill.embedding_text())) + ranked_pairs.append((len(query_token_set & doc_tokens), skill)) + ranked_pairs.sort(key=lambda item: item[0], reverse=True) + return [skill for _, skill in ranked_pairs] + + +def _dense_ranking( + skills: list["Skill"], + query: str, + detector: "SimilarityDetector", +) -> list["Skill"]: + if not skills: + return [] + + query_embedding = detector.compute_embedding(query) + if query_embedding is None: + raise RuntimeError( + "Failed to embed retrieval query — " + "check embedding provider credentials / network." + ) + + ranked_pairs: list[tuple[float, Skill]] = [] + for skill in skills: + if skill.embedding is None: + continue + similarity = detector.cosine_similarity(query_embedding, skill.embedding) + ranked_pairs.append((similarity, skill)) + + ranked_pairs.sort(key=lambda item: item[0], reverse=True) + return [skill for _, skill in ranked_pairs] + + +def retrieve_top_k( + skillbook: "Skillbook", + query: str, + *, + top_k: int = 5, + detector: "SimilarityDetector | None" = None, + section: str | None = None, + keywords: list[str] | None = None, +) -> list["Skill"]: + """Retrieve relevant skills using lexical + dense fusion.""" + if top_k <= 0: + return [] + + candidates = skillbook.skills() + if section: + normalized_section = str(section).strip().lower() + candidates = [ + skill for skill in candidates if skill.section == normalized_section + ] + if not candidates: + return [] + + normalized_keywords = _normalize_keywords(keywords) + + if detector is None: + from ace.deduplication.detector import SimilarityDetector as _Detector + from ace.protocols.deduplication import DeduplicationConfig + + detector = _Detector(DeduplicationConfig()) + + detector.ensure_embeddings(skillbook) + lexical_ranked = _lexical_ranking(candidates, query) + dense_ranked = _dense_ranking(candidates, query, detector) + + fused_scores: dict[str, float] = defaultdict(float) + skills_by_id = {skill.id: skill for skill in candidates} + + for rank, skill in enumerate(lexical_ranked, start=1): + fused_scores[skill.id] += 1.0 / (RRF_K + rank) + for rank, skill in enumerate(dense_ranked, start=1): + fused_scores[skill.id] += 1.0 / (RRF_K + rank) + + if normalized_keywords: + for skill in candidates: + overlap = _keyword_overlap(skill, normalized_keywords) + if overlap: + fused_scores[skill.id] += 0.25 * overlap + + ranked_ids = sorted( + fused_scores, + key=lambda skill_id: fused_scores[skill_id], + reverse=True, + ) + return [skills_by_id[skill_id] for skill_id in ranked_ids[:top_k]] diff --git a/ace/implementations/sm_tools.py b/ace/implementations/sm_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..d80c88bc68bb8b7f5a8fb52b4a9833d7c4a62da7 --- /dev/null +++ b/ace/implementations/sm_tools.py @@ -0,0 +1,316 @@ +"""SkillManager tool registrars and dependency container. + +The agentic SkillManager operates on the real :class:`Skillbook` via +atomic mutation tools (ADD / UPDATE / REMOVE / TAG) and read-only +inspection tools (search / read). Tools apply changes directly; there +is no staging. Each mutation appends an ``UpdateOperation`` to +``deps.operations`` so the caller can recover an audit trail after the +run. + +Generic tools (``execute_code``, ``recurse``) are provided by +:mod:`ace.core.recursive_agent`. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Iterable, Literal, Optional + +from pydantic_ai import RunContext + +from ace.core.insight_source import InsightSource +from ace.core.recursive_agent import AgenticDeps +from ace.core.skillbook import Skillbook, UpdateOperation + +if TYPE_CHECKING: + from pydantic_ai import Agent as PydanticAgent + + +# ------------------------------------------------------------------ +# Dependency container +# ------------------------------------------------------------------ + + +@dataclass +class SMDeps(AgenticDeps): + """Dependencies injected into SkillManager tool calls via ``RunContext``.""" + + skillbook: Optional[Skillbook] = None + operations: list[UpdateOperation] = field(default_factory=list) + current_source: Optional[InsightSource] = None + + +def _normalize_keywords(keywords: Iterable[str]) -> list[str]: + normalized: list[str] = [] + seen: set[str] = set() + for keyword in keywords: + text = str(keyword).strip().lower().replace(" ", "_") + if not text or text in seen: + continue + normalized.append(text) + seen.add(text) + return normalized + + +def _derive_operation_source( + base: InsightSource | None, + *, + operation_type: str, + issue: str | None = None, + insight: str | None = None, + reason: str | None = None, +) -> InsightSource | None: + if base is None: + return None + return InsightSource( + trace_uid=base.trace_uid, + source_system=base.source_system, + trace_id=base.trace_id, + display_name=base.display_name, + relation=base.relation, + sample_question=base.sample_question, + epoch=base.epoch, + operation_type=operation_type, + error_identification=issue or base.error_identification, + learning_text=insight or reason or base.learning_text, + ) + + +# ------------------------------------------------------------------ +# Mutation tools +# ------------------------------------------------------------------ + + +def register_add_skill(agent: "PydanticAgent[SMDeps, Any]") -> None: + """Register ``add_skill``.""" + + @agent.tool + def add_skill( + ctx: RunContext[SMDeps], + section: str, + issue: str, + keywords: list[str], + insight: str | None = None, + ) -> dict[str, Any]: + sb = ctx.deps.skillbook + if sb is None: + return {"error": "skillbook unavailable"} + normalized_keywords = _normalize_keywords(keywords) + op_source = _derive_operation_source( + ctx.deps.current_source, + operation_type="ADD", + issue=issue, + insight=insight, + ) + skill = sb.add_skill( + section=section, + issue=issue, + keywords=normalized_keywords, + insight=insight, + insight_source=op_source, + ) + ctx.deps.operations.append( + UpdateOperation( + type="ADD", + section=skill.section, + issue=issue, + keywords=normalized_keywords, + insight=insight, + skill_id=skill.id, + insight_source=op_source, + ) + ) + return {"ok": True, "skill_id": skill.id} + + +def register_update_skill(agent: "PydanticAgent[SMDeps, Any]") -> None: + """Register ``update_skill``.""" + + @agent.tool + def update_skill( + ctx: RunContext[SMDeps], + skill_id: str, + issue: str, + keywords: list[str] | None = None, + insight: str | None = None, + ) -> dict[str, Any]: + sb = ctx.deps.skillbook + if sb is None: + return {"error": "skillbook unavailable"} + normalized_keywords = ( + _normalize_keywords(keywords) if keywords is not None else None + ) + op_source = _derive_operation_source( + ctx.deps.current_source, + operation_type="UPDATE", + issue=issue, + insight=insight, + ) + skill = sb.update_skill( + skill_id, + issue=issue, + keywords=normalized_keywords if normalized_keywords is not None else None, + insight=insight, + insight_source=op_source, + ) + if skill is None: + return {"error": f"skill not found: {skill_id}"} + ctx.deps.operations.append( + UpdateOperation( + type="UPDATE", + section=skill.section, + skill_id=skill_id, + issue=issue, + keywords=normalized_keywords or [], + insight=insight, + insight_source=op_source, + ) + ) + return {"ok": True, "skill_id": skill_id} + + +def register_remove_skill(agent: "PydanticAgent[SMDeps, Any]") -> None: + """Register ``remove_skill``.""" + + @agent.tool + def remove_skill( + ctx: RunContext[SMDeps], + skill_id: str, + reason: str, + ) -> dict[str, Any]: + sb = ctx.deps.skillbook + if sb is None: + return {"error": "skillbook unavailable"} + skill = sb.get_skill(skill_id) + if skill is None: + return {"error": f"skill not found: {skill_id}"} + op_source = _derive_operation_source( + ctx.deps.current_source, + operation_type="REMOVE", + issue=skill.issue, + reason=reason, + ) + sb.remove_skill(skill_id, insight_source=op_source) + ctx.deps.operations.append( + UpdateOperation( + type="REMOVE", + section=skill.section, + skill_id=skill_id, + reason=reason, + insight_source=op_source, + ) + ) + return {"ok": True, "skill_id": skill_id} + + +def register_tag_skill(agent: "PydanticAgent[SMDeps, Any]") -> None: + """Register ``tag_skill``.""" + + @agent.tool + def tag_skill( + ctx: RunContext[SMDeps], + skill_id: str, + delta: Literal[1, -1, 0], + ) -> dict[str, Any]: + sb = ctx.deps.skillbook + if sb is None: + return {"error": "skillbook unavailable"} + existing = sb.get_skill(skill_id) + if existing is None: + return {"error": f"skill not found: {skill_id}"} + op_source = _derive_operation_source( + ctx.deps.current_source, + operation_type="TAG", + issue=existing.issue, + reason=f"effectiveness_delta={int(delta)}", + ) + skill = sb.tag_skill(skill_id, delta, insight_source=op_source) + if skill is None: + return {"error": f"skill not found: {skill_id}"} + ctx.deps.operations.append( + UpdateOperation( + type="TAG", + section=skill.section, + skill_id=skill_id, + metadata={"delta": int(delta)}, + insight_source=op_source, + ) + ) + return { + "ok": True, + "skill_id": skill_id, + "helpful_count": skill.helpful_count, + "harmful_count": skill.harmful_count, + "neutral_count": skill.neutral_count, + } + + +# ------------------------------------------------------------------ +# Read-only tools +# ------------------------------------------------------------------ + + +def register_sm_read_skill(agent: "PydanticAgent[SMDeps, Any]") -> None: + """Register ``read_skill``.""" + + @agent.tool + def read_skill(ctx: RunContext[SMDeps], skill_id: str) -> dict[str, Any]: + sb = ctx.deps.skillbook + if sb is None: + return {"error": "skillbook unavailable"} + skill = sb.get_skill(skill_id) + if skill is None: + return {"error": f"skill not found: {skill_id}"} + return { + "id": skill.id, + "section": skill.section, + "keywords": list(skill.keywords), + "issue": skill.issue, + "insight": skill.insight, + "active": skill.active, + "used_count": skill.used_count, + "helpful_count": skill.helpful_count, + "harmful_count": skill.harmful_count, + "neutral_count": skill.neutral_count, + "occurrences": [source.to_dict() for source in skill.occurrences], + } + + +def register_sm_search_skills(agent: "PydanticAgent[SMDeps, Any]") -> None: + """Register ``search_skills``.""" + + @agent.tool + def search_skills( + ctx: RunContext[SMDeps], + query: str, + top_k: int = 5, + section: str | None = None, + keywords: list[str] | None = None, + ) -> list[dict[str, Any]]: + sb = ctx.deps.skillbook + if sb is None: + return [{"error": "skillbook unavailable"}] + from ace.implementations.skill_rendering import retrieve_top_k + + results = retrieve_top_k( + sb, + query, + top_k=top_k, + section=section, + keywords=keywords, + ) + return [ + { + "id": skill.id, + "section": skill.section, + "keywords": list(skill.keywords), + "issue": skill.issue, + "insight": skill.insight, + "active": skill.active, + "used_count": skill.used_count, + "helpful_count": skill.helpful_count, + "harmful_count": skill.harmful_count, + "neutral_count": skill.neutral_count, + } + for skill in results + ] diff --git a/ace/integrations/__init__.py b/ace/integrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a5f20fee2641eb601d5f098ec602242a8cbf20c0 --- /dev/null +++ b/ace/integrations/__init__.py @@ -0,0 +1,70 @@ +"""ACE integration steps — execute steps for external agentic frameworks. + +Each integration provides: + +1. **Result type** — integration-specific output (e.g. ``ClaudeCodeResult``) +2. **Execute step** — INJECT + EXECUTE, writes the result to ``ctx.trace`` +3. **ToTrace step** — converts the result to a standardised trace dict + for the learning tail (``ReflectStep``) + +Compose with ``learning_tail()``:: + + from ace.integrations import ClaudeCodeExecuteStep, ClaudeCodeToTrace + from ace.steps import learning_tail + + steps = [ + ClaudeCodeExecuteStep(working_dir="./project"), + ClaudeCodeToTrace(), + *learning_tail(reflector, skill_manager, skillbook), + ] + pipeline = Pipeline(steps) +""" + +from __future__ import annotations + +from ..implementations.prompts import wrap_skillbook_for_external_agent + +from .browser_use import BrowserExecuteStep, BrowserResult, BrowserToTrace +from .claude_code import ClaudeCodeExecuteStep, ClaudeCodeResult, ClaudeCodeToTrace +from .claude_sdk import ( + ClaudeSDKExecuteStep, + ClaudeSDKResult, + ClaudeSDKToTrace, + ToolCall, +) +from .langchain import LangChainExecuteStep, LangChainResult, LangChainToTrace +from .openclaw import OpenClawToTraceStep + + +def wrap_skillbook_context(skillbook) -> str: + """Format learned strategies for injection into external agents. + + Thin wrapper around the canonical implementation in + ``implementations.prompts``. + """ + return wrap_skillbook_for_external_agent(skillbook) + + +__all__ = [ + # Browser-use + "BrowserExecuteStep", + "BrowserResult", + "BrowserToTrace", + # Claude Code + "ClaudeCodeExecuteStep", + "ClaudeCodeResult", + "ClaudeCodeToTrace", + # Claude SDK + "ClaudeSDKExecuteStep", + "ClaudeSDKResult", + "ClaudeSDKToTrace", + "ToolCall", + # LangChain + "LangChainExecuteStep", + "LangChainResult", + "LangChainToTrace", + # OpenClaw + "OpenClawToTraceStep", + # Utility + "wrap_skillbook_context", +] diff --git a/ace/integrations/browser_use.py b/ace/integrations/browser_use.py new file mode 100644 index 0000000000000000000000000000000000000000..23ffc767ac9b2ef9eb9a9402be82787cdd56dd6a --- /dev/null +++ b/ace/integrations/browser_use.py @@ -0,0 +1,301 @@ +"""Browser-use integration — execute step, result type, and trace converter.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field +from typing import Any, List, Optional + +from ..core.context import ACEStepContext +from ..implementations.prompts import wrap_skillbook_for_external_agent + +logger = logging.getLogger(__name__) + +try: + from browser_use import Agent, Browser + + BROWSER_USE_AVAILABLE = True +except ImportError: + BROWSER_USE_AVAILABLE = False + Agent = None # type: ignore[misc,assignment] + Browser = None # type: ignore[misc,assignment] + + +# --------------------------------------------------------------------------- +# Input / Output types +# --------------------------------------------------------------------------- + + +@dataclass +class BrowserResult: + """Output from a browser-use execution. + + This is the integration-specific result — not yet in ACE trace format. + Use ``BrowserToTrace`` to convert to a standardised trace dict. + """ + + task: str + success: bool + output: str = "" + error: Optional[str] = None + steps_count: int = 0 + duration_seconds: Optional[float] = None + cited_skill_ids: List[str] = field(default_factory=list) + chronological_steps: List[dict] = field(default_factory=list) + raw_history: Any = None + + +# --------------------------------------------------------------------------- +# Execute step +# --------------------------------------------------------------------------- + + +class BrowserExecuteStep: + """INJECT skillbook context and EXECUTE via browser-use Agent. + + Reads a task string from ``ctx.sample``, writes a ``BrowserResult`` + to ``ctx.trace``. + + This is an **async** step — ``__call__`` is a coroutine because + browser-use is an async framework. + """ + + requires = frozenset({"sample", "skillbook"}) + provides = frozenset({"trace"}) + + def __init__( + self, browser_llm: Any, browser: Any = None, **agent_kwargs: Any + ) -> None: + if not BROWSER_USE_AVAILABLE: + raise ImportError( + "browser-use is not installed. Install with: " "pip install browser-use" + ) + self.browser_llm = browser_llm + self.browser = browser + self.agent_kwargs = agent_kwargs + + async def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + task: str = ctx.sample + + # -- INJECT -- + enhanced_task = self._inject(task, ctx.skillbook) + + # -- EXECUTE -- + agent_params: dict[str, Any] = { + **self.agent_kwargs, + "task": enhanced_task, + "llm": self.browser_llm, + } + if self.browser is not None: + agent_params["browser"] = self.browser + + success = False + error: Optional[str] = None + history: Any = None + try: + agent = Agent(**agent_params) + history = await agent.run() + success = True + except Exception as exc: + error = str(exc) + + result = self._build_result(task, history, success, error) + return ctx.replace(trace=result) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _inject(task: str, skillbook: Any) -> str: + if skillbook is None: + return task + context = wrap_skillbook_for_external_agent(skillbook) + if not context: + return task + return f"{task}\n\n{context}" + + @staticmethod + def _build_result( + task: str, + history: Any, + success: bool, + error: Optional[str], + ) -> BrowserResult: + if history is None: + return BrowserResult(task=task, success=success, error=error) + + # Extract basic info + try: + output = ( + history.final_result() if hasattr(history, "final_result") else "" + ) or "" + except Exception: + output = "" + + try: + steps_count = ( + history.number_of_steps() if hasattr(history, "number_of_steps") else 0 + ) + except Exception: + steps_count = 0 + + duration: Optional[float] = None + try: + if hasattr(history, "total_duration_seconds"): + duration = round(history.total_duration_seconds(), 2) + except Exception: + pass + + # Extract chronological step data + chronological: list[dict] = [] + try: + if hasattr(history, "history"): + for step_idx, step in enumerate(history.history, 1): + step_data: dict[str, Any] = {"step_number": step_idx} + + if step.model_output: + step_data["thought"] = { + "thinking": step.model_output.thinking, + "evaluation": step.model_output.evaluation_previous_goal, + "memory": step.model_output.memory, + "next_goal": step.model_output.next_goal, + } + if step.model_output.action: + step_data["actions"] = [ + {k: v for k, v in a.model_dump().items()} + for a in step.model_output.action + ] + + if step.result: + step_data["results"] = [ + { + "is_done": r.is_done, + "success": r.success, + "error": r.error, + "extracted_content": r.extracted_content, + } + for r in step.result + ] + + if step.state: + step_data["url"] = step.state.url + + chronological.append(step_data) + except Exception as exc: + logger.debug("Trace extraction error: %s", exc) + + # Extract cited skill IDs from agent thoughts + cited_ids: list[str] = [] + try: + if hasattr(history, "model_thoughts"): + thoughts = history.model_thoughts() + thoughts_text = "\n".join( + t.thinking + for t in thoughts + if hasattr(t, "thinking") and t.thinking + ) + from ..implementations.helpers import extract_cited_skill_ids + + cited_ids = extract_cited_skill_ids(thoughts_text) + except Exception: + pass + + return BrowserResult( + task=task, + success=success, + output=output, + error=error, + steps_count=steps_count, + duration_seconds=duration, + cited_skill_ids=cited_ids, + chronological_steps=chronological, + raw_history=history, + ) + + +# --------------------------------------------------------------------------- +# Convert step — BrowserResult → standardised trace dict +# --------------------------------------------------------------------------- + + +class BrowserToTrace: + """Convert a ``BrowserResult`` on ``ctx.trace`` to the standardised + trace dict that the learning tail (``ReflectStep``) expects. + """ + + requires = frozenset({"trace"}) + provides = frozenset({"trace"}) + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + r: BrowserResult = ctx.trace # type: ignore[assignment] + + # Build human-readable reasoning from chronological steps + parts: list[str] = [] + status = "succeeded" if r.success else "failed" + parts.append(f"Browser task {status} in {r.steps_count} steps") + if r.duration_seconds is not None: + parts.append(f"Duration: {r.duration_seconds}s") + if r.output: + preview = r.output[:150] + ("..." if len(r.output) > 150 else "") + parts.append(f"\nFinal output: {preview}") + if r.error: + parts.append(f"\nFailure reason: {r.error}") + + if r.chronological_steps: + parts.append("\n\n=== BROWSER EXECUTION TRACE (Chronological) ===") + for step in r.chronological_steps: + step_num = step["step_number"] + parts.append(f"\n--- Step {step_num} ---") + if "thought" in step: + thought = step["thought"] + if thought.get("thinking"): + parts.append(f"Thinking: {thought['thinking']}") + if thought.get("evaluation"): + parts.append(f" Evaluation: {thought['evaluation']}") + if thought.get("next_goal"): + parts.append(f" Next Goal: {thought['next_goal']}") + if "actions" in step: + for action in step["actions"]: + name = next(iter(action), "unknown") + parts.append(f"Action: {name}({action.get(name, {})})") + if "results" in step: + for res in step["results"]: + res_parts = [] + if res.get("success") is not None: + res_parts.append(f"success={res['success']}") + if res.get("error"): + res_parts.append(f"error={res['error']}") + if res.get("extracted_content"): + res_parts.append( + f"content={str(res['extracted_content'])[:200]}" + ) + parts.append(f"Result: {', '.join(res_parts)}") + if "url" in step: + parts.append(f"URL: {step['url']}") + parts.append("\n=== END EXECUTION TRACE ===") + + reasoning = "\n".join(parts) + + feedback = f"Browser task {status} in {r.steps_count} steps" + if r.duration_seconds is not None: + feedback += f" ({r.duration_seconds}s)" + if r.error: + feedback += f"\nError: {r.error}" + + trace: dict = { + "question": r.task, + "reasoning": reasoning, + "answer": r.output, + "skill_ids": r.cited_skill_ids, + "feedback": feedback, + "ground_truth": None, + } + return ctx.replace(trace=trace) + + +__all__ = [ + "BrowserExecuteStep", + "BrowserResult", + "BrowserToTrace", +] diff --git a/ace/integrations/claude_code.py b/ace/integrations/claude_code.py new file mode 100644 index 0000000000000000000000000000000000000000..cf7e005836954a66e0f26d67370ebbe30ed29285 --- /dev/null +++ b/ace/integrations/claude_code.py @@ -0,0 +1,253 @@ +"""Claude Code integration — execute step, result type, and trace converter.""" + +from __future__ import annotations + +import json +import logging +import os +import shutil +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Optional, Tuple + +from ..core.context import ACEStepContext +from ..implementations.prompts import wrap_skillbook_for_external_agent + +logger = logging.getLogger(__name__) + +CLAUDE_CODE_AVAILABLE = shutil.which("claude") is not None + + +# --------------------------------------------------------------------------- +# Input / Output types +# --------------------------------------------------------------------------- + + +@dataclass +class ClaudeCodeResult: + """Output from a Claude Code CLI execution. + + This is the integration-specific result — not yet in ACE trace format. + Use ``ClaudeCodeToTrace`` to convert to a standardised trace dict. + """ + + task: str + success: bool + output: str = "" + execution_trace: str = "" + returncode: int = 0 + error: Optional[str] = None + + +# --------------------------------------------------------------------------- +# Execute step +# --------------------------------------------------------------------------- + + +class ClaudeCodeExecuteStep: + """INJECT skillbook context and EXECUTE via Claude Code CLI. + + Reads a task string from ``ctx.sample``, writes a ``ClaudeCodeResult`` + to ``ctx.trace``. + """ + + requires = frozenset({"sample", "skillbook"}) + provides = frozenset({"trace"}) + + def __init__( + self, + working_dir: Optional[str] = None, + timeout: int = 600, + model: Optional[str] = None, + allowed_tools: Optional[list[str]] = None, + ) -> None: + if not CLAUDE_CODE_AVAILABLE: + raise RuntimeError( + "Claude Code CLI not found. Install from: https://claude.ai/code" + ) + self.working_dir = Path(working_dir).resolve() if working_dir else Path.cwd() + self.timeout = timeout + self.model = model + self.allowed_tools = allowed_tools + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + task: str = ctx.sample + + # -- INJECT -- + prompt = self._inject(task, ctx.skillbook) + + # -- EXECUTE -- + result = self._execute(task, prompt) + + return ctx.replace(trace=result) + + # ------------------------------------------------------------------ + # Injection + # ------------------------------------------------------------------ + + @staticmethod + def _inject(task: str, skillbook: Any) -> str: + if skillbook is None: + return task + context = wrap_skillbook_for_external_agent(skillbook) + if not context: + return task + return f"{task}\n\n{context}" + + # ------------------------------------------------------------------ + # Execution + # ------------------------------------------------------------------ + + def _execute(self, task: str, prompt: str) -> ClaudeCodeResult: + cmd: list[str] = [ + "claude", + "--print", + "--output-format=stream-json", + "--verbose", + "--dangerously-skip-permissions", + ] + if self.model: + cmd.extend(["--model", self.model]) + if self.allowed_tools: + for tool in self.allowed_tools: + cmd.extend(["--allowedTools", tool]) + + env = {k: v for k, v in os.environ.items() if k != "ANTHROPIC_API_KEY"} + + try: + result = subprocess.run( + cmd, + input=prompt, + text=True, + cwd=str(self.working_dir), + capture_output=True, + timeout=self.timeout, + env=env, + ) + execution_trace, summary = self._parse_stream_json(result.stdout) + return ClaudeCodeResult( + task=task, + success=result.returncode == 0, + output=summary, + execution_trace=execution_trace, + returncode=result.returncode, + error=result.stderr[:500] if result.returncode != 0 else None, + ) + except subprocess.TimeoutExpired: + return ClaudeCodeResult( + task=task, + success=False, + returncode=-1, + error=f"Execution timed out after {self.timeout}s", + ) + except Exception as exc: + return ClaudeCodeResult( + task=task, + success=False, + returncode=-1, + error=str(exc), + ) + + # ------------------------------------------------------------------ + # Stream-JSON parser + # ------------------------------------------------------------------ + + @staticmethod + def _parse_stream_json(stdout: str) -> Tuple[str, str]: + """Parse stream-json output. Returns ``(execution_trace, summary)``.""" + trace_parts: list[str] = [] + final_text = "" + step_num = 0 + + for line in stdout.split("\n"): + if not line.strip(): + continue + try: + event = json.loads(line) + except json.JSONDecodeError: + continue + + if event.get("type") != "assistant": + continue + + for block in event.get("message", {}).get("content", []): + if not isinstance(block, dict): + continue + block_type = block.get("type") + if block_type == "text": + text = block.get("text", "") + if text.strip(): + trace_parts.append(f"[Reasoning] {text[:300]}") + final_text = text + elif block_type == "tool_use": + step_num += 1 + tool_name = block.get("name", "unknown") + tool_input = block.get("input", {}) + if tool_name in ("Read", "Glob", "Grep"): + target = tool_input.get("file_path") or tool_input.get( + "pattern", "" + ) + trace_parts.append(f"[Step {step_num}] {tool_name}: {target}") + elif tool_name in ("Write", "Edit"): + target = tool_input.get("file_path", "") + trace_parts.append(f"[Step {step_num}] {tool_name}: {target}") + elif tool_name == "Bash": + cmd = tool_input.get("command", "")[:80] + trace_parts.append(f"[Step {step_num}] Bash: {cmd}") + else: + trace_parts.append(f"[Step {step_num}] {tool_name}") + + execution_trace = ( + "\n".join(trace_parts) if trace_parts else "(No trace captured)" + ) + + if final_text: + paragraphs = [p.strip() for p in final_text.split("\n\n") if p.strip()] + summary = paragraphs[-1][:300] if paragraphs else final_text[:300] + else: + summary = f"Completed {step_num} steps" + + return execution_trace, summary + + +# --------------------------------------------------------------------------- +# Convert step — ClaudeCodeResult → standardised trace dict +# --------------------------------------------------------------------------- + + +class ClaudeCodeToTrace: + """Convert a ``ClaudeCodeResult`` on ``ctx.trace`` to the standardised + trace dict that the learning tail (``ReflectStep``) expects. + + Trace dict keys: ``question``, ``reasoning``, ``answer``, ``skill_ids``, + ``feedback``, ``ground_truth``. + """ + + requires = frozenset({"trace"}) + provides = frozenset({"trace"}) + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + r: ClaudeCodeResult = ctx.trace # type: ignore[assignment] + + status = "succeeded" if r.success else "failed" + feedback = f"Claude Code task {status}" + if r.error: + feedback += f"\nError: {r.error}" + + trace: dict = { + "question": r.task, + "reasoning": r.execution_trace, + "answer": r.output, + "skill_ids": [], + "feedback": feedback, + "ground_truth": None, + } + return ctx.replace(trace=trace) + + +__all__ = [ + "ClaudeCodeExecuteStep", + "ClaudeCodeResult", + "ClaudeCodeToTrace", +] diff --git a/ace/integrations/claude_sdk.py b/ace/integrations/claude_sdk.py new file mode 100644 index 0000000000000000000000000000000000000000..38e1dab809b626260102a004bb3bfd8ee975ef51 --- /dev/null +++ b/ace/integrations/claude_sdk.py @@ -0,0 +1,525 @@ +"""Claude SDK integration — execute step, result type, and trace converter. + +Uses the ``anthropic`` Python SDK directly for full API access with +built-in observability (token tracking, latency, Logfire spans and +auto-instrumentation). + +Usage:: + + from ace.integrations import ClaudeSDKExecuteStep, ClaudeSDKToTrace + from ace.steps import learning_tail + + steps = [ + ClaudeSDKExecuteStep(model="claude-sonnet-4-20250514"), + ClaudeSDKToTrace(), + *learning_tail(reflector, skill_manager, skillbook), + ] + pipeline = Pipeline(steps) +""" + +from __future__ import annotations + +import logging +import time +from contextlib import contextmanager +from typing import Any, Dict, Iterator, List, Optional + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from ..core.context import ACEStepContext +from ..implementations.prompts import wrap_skillbook_for_external_agent + +logger = logging.getLogger(__name__) + + +def _get_logfire() -> Any: + """Return the ``logfire`` module if configured, else ``None``.""" + try: + from ace.observability import is_configured + + if is_configured(): + import logfire + + return logfire + except Exception: + pass + return None + + +@contextmanager +def _logfire_span(name: str, **attributes: Any) -> Iterator[Any]: + """Open a Logfire span if configured, otherwise yield a no-op object. + + Attributes can be set on the yielded object via ``set_attribute``. + When Logfire is not active the context manager yields a lightweight + stub so callers don't need conditional logic. + """ + lf = _get_logfire() + if lf is not None: + with lf.span(name, **attributes) as span: + yield span + else: + yield _NoOpSpan() + + +class _NoOpSpan: + """Stub returned when Logfire is not configured.""" + + def set_attribute(self, key: str, value: Any) -> None: # noqa: ARG002 + pass + + def record_exception(self, exc: BaseException) -> None: # noqa: ARG002 + pass + + +try: + import anthropic + + ANTHROPIC_SDK_AVAILABLE = True +except ImportError: + ANTHROPIC_SDK_AVAILABLE = False + anthropic = None # type: ignore[misc,assignment] + + +# --------------------------------------------------------------------------- +# Input / Output types +# --------------------------------------------------------------------------- + + +class ToolCall(BaseModel): + """A single tool call from the Claude API response.""" + + model_config = ConfigDict(extra="forbid") + + id: str = Field(..., description="Tool call ID (e.g. toolu_01...)") + name: str = Field(..., description="Tool name") + input: Dict[str, Any] = Field( + default_factory=dict, description="Tool input arguments" + ) + + +class _ClaudeSDKConfig(BaseModel): + """Validated configuration for :class:`ClaudeSDKExecuteStep`.""" + + model_config = ConfigDict(extra="forbid") + + model: str = Field(default="claude-sonnet-4-20250514", min_length=1) + system_prompt: Optional[str] = None + max_tokens: int = Field(default=4096, gt=0) + temperature: float = Field(default=0.0, ge=0.0, le=1.0) + tools: Optional[List[Dict[str, Any]]] = None + api_key: Optional[str] = None + base_url: Optional[str] = None + inject_skillbook: bool = True + + +class ClaudeSDKResult(BaseModel): + """Output from a direct Anthropic SDK call. + + This is the integration-specific result — not yet in ACE trace format. + Use ``ClaudeSDKToTrace`` to convert to a standardised trace dict. + + Includes validated observability data: token usage, latency, model info. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid") + + task: str = Field(..., description="Original task/question sent to the API") + success: bool = Field(..., description="Whether the API call succeeded") + output: str = Field(default="", description="Text output from the model") + error: Optional[str] = Field( + default=None, description="Error message if the call failed" + ) + model: str = Field(default="", description="Model ID used for the request") + stop_reason: Optional[str] = Field( + default=None, + description="Why the model stopped: end_turn, max_tokens, tool_use, etc.", + ) + # Observability — token usage + input_tokens: int = Field(default=0, ge=0, description="Prompt tokens consumed") + output_tokens: int = Field( + default=0, ge=0, description="Completion tokens generated" + ) + total_tokens: int = Field(default=0, ge=0, description="Total tokens (in + out)") + # Observability — latency + latency_seconds: float = Field( + default=0.0, ge=0.0, description="Wall-clock time for the API call" + ) + # Tool use tracking + tool_calls: List[ToolCall] = Field( + default_factory=list, description="Tool calls made by the model" + ) + cited_skill_ids: List[str] = Field( + default_factory=list, + description="Skill IDs cited in the output ([section-00001] patterns)", + ) + # Raw response for full access + raw_response: Any = Field( + default=None, + exclude=True, + description="Raw Anthropic API response object", + ) + + @model_validator(mode="after") + def _compute_total(self) -> "ClaudeSDKResult": + """Auto-compute total_tokens from input + output if left at default.""" + if self.total_tokens == 0 and (self.input_tokens or self.output_tokens): + self.total_tokens = self.input_tokens + self.output_tokens + return self + + +# --------------------------------------------------------------------------- +# Execute step +# --------------------------------------------------------------------------- + + +class ClaudeSDKExecuteStep: + """INJECT skillbook context and EXECUTE via the Anthropic Python SDK. + + Reads a task/question from ``ctx.sample``, calls the Claude Messages + API directly, and writes a ``ClaudeSDKResult`` to ``ctx.trace``. + + Observability is built in: + + - **Logfire spans** with structured attributes (model, tokens, + latency, stop_reason, tool_count) when Logfire is configured + - **Logfire auto-instrumentation** of the underlying Anthropic + client (child spans for each API call) + - **Token usage** (input/output/total) from the API response + - **Latency** (wall-clock time for the API call) + - **Structured logging** of per-call metrics (always active) + + Compose with the learning tail:: + + steps = [ + ClaudeSDKExecuteStep(model="claude-sonnet-4-20250514"), + ClaudeSDKToTrace(), + *learning_tail(reflector, skill_manager, skillbook), + ] + """ + + requires = frozenset({"sample", "skillbook"}) + provides = frozenset({"trace"}) + + def __init__( + self, + model: str = "claude-sonnet-4-20250514", + *, + system_prompt: Optional[str] = None, + max_tokens: int = 4096, + temperature: float = 0.0, + tools: Optional[List[Dict[str, Any]]] = None, + api_key: Optional[str] = None, + base_url: Optional[str] = None, + inject_skillbook: bool = True, + client: Any = None, + **client_kwargs: Any, + ) -> None: + config = _ClaudeSDKConfig( + model=model, + system_prompt=system_prompt, + max_tokens=max_tokens, + temperature=temperature, + tools=tools, + api_key=api_key, + base_url=base_url, + inject_skillbook=inject_skillbook, + ) + self.model = config.model + self.system_prompt = config.system_prompt + self.max_tokens = config.max_tokens + self.temperature = config.temperature + self.tools = config.tools + self.inject_skillbook = config.inject_skillbook + + if client is not None: + self._client = client + elif ANTHROPIC_SDK_AVAILABLE: + ckw: Dict[str, Any] = {**client_kwargs} + if config.api_key is not None: + ckw["api_key"] = config.api_key + if config.base_url is not None: + ckw["base_url"] = config.base_url + self._client = anthropic.Anthropic(**ckw) + else: + raise ImportError( + "anthropic SDK not installed. Install with: uv add " + '"ace-framework[claude-sdk]" or uv add anthropic' + ) + + self._try_instrument() + + # ------------------------------------------------------------------ + # Logfire auto-instrumentation + # ------------------------------------------------------------------ + + def _try_instrument(self) -> None: + """Auto-instrument the Anthropic client with Logfire if configured. + + Logfire instruments the client eagerly and returns an optional context + manager for later uninstrumentation. The instrumentation call itself is + sufficient here. + """ + try: + from ace.observability import is_configured + + if is_configured(): + import logfire + + logfire.instrument_anthropic(self._client) + logger.debug("ClaudeSDKExecuteStep: Logfire instrumentation active") + except Exception as exc: + logger.debug("Logfire instrumentation skipped: %s", exc) + + # ------------------------------------------------------------------ + # Step execution + # ------------------------------------------------------------------ + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + task = self._extract_task(ctx.sample) + system = self._build_system(ctx.skillbook) + messages: List[Dict[str, Any]] = [{"role": "user", "content": task}] + + with _logfire_span( + "ClaudeSDKExecuteStep", + model=self.model, + task=task[:200], + has_system=system is not None, + has_tools=bool(self.tools), + ) as span: + result = self._execute(task, system, messages) + span.set_attribute("success", result.success) + span.set_attribute("input_tokens", result.input_tokens) + span.set_attribute("output_tokens", result.output_tokens) + span.set_attribute("total_tokens", result.total_tokens) + span.set_attribute("latency_seconds", result.latency_seconds) + span.set_attribute("stop_reason", result.stop_reason or "") + span.set_attribute("tool_call_count", len(result.tool_calls)) + span.set_attribute("cited_skill_count", len(result.cited_skill_ids)) + if result.error: + span.set_attribute("error", result.error) + + return ctx.replace(trace=result) + + # ------------------------------------------------------------------ + # Task extraction + # ------------------------------------------------------------------ + + @staticmethod + def _extract_task(sample: Any) -> str: + """Extract task string from sample (string or ACESample).""" + if isinstance(sample, str): + return sample + if hasattr(sample, "question"): + parts = [sample.question] + if hasattr(sample, "context") and sample.context: + parts.append(f"\nContext: {sample.context}") + return "\n".join(parts) + return str(sample) + + # ------------------------------------------------------------------ + # System prompt with skillbook injection + # ------------------------------------------------------------------ + + def _build_system(self, skillbook: Any) -> Optional[str]: + """Build the system prompt, optionally injecting skillbook context.""" + parts: List[str] = [] + if self.system_prompt: + parts.append(self.system_prompt) + if self.inject_skillbook and skillbook is not None: + context = wrap_skillbook_for_external_agent(skillbook) + if context: + parts.append(context) + return "\n\n".join(parts) if parts else None + + # ------------------------------------------------------------------ + # API call + # ------------------------------------------------------------------ + + def _execute( + self, + task: str, + system: Optional[str], + messages: List[Dict[str, Any]], + ) -> ClaudeSDKResult: + """Call the Anthropic Messages API and build a result with metrics.""" + api_kwargs: Dict[str, Any] = { + "model": self.model, + "max_tokens": self.max_tokens, + "temperature": self.temperature, + "messages": messages, + } + if system is not None: + api_kwargs["system"] = system + if self.tools: + api_kwargs["tools"] = self.tools + + start = time.monotonic() + try: + response = self._client.messages.create(**api_kwargs) + latency = time.monotonic() - start + + text_parts: List[str] = [] + tool_calls: List[ToolCall] = [] + for block in response.content: + if block.type == "text": + text_parts.append(block.text) + elif block.type == "tool_use": + tool_calls.append( + ToolCall( + id=block.id, + name=block.name, + input=block.input or {}, + ) + ) + + output = "\n".join(text_parts) + cited_ids = self._extract_skill_ids(output) + + result = ClaudeSDKResult( + task=task, + success=True, + output=output, + model=response.model, + stop_reason=response.stop_reason, + input_tokens=response.usage.input_tokens, + output_tokens=response.usage.output_tokens, + total_tokens=( + response.usage.input_tokens + response.usage.output_tokens + ), + latency_seconds=round(latency, 3), + tool_calls=tool_calls, + cited_skill_ids=cited_ids, + raw_response=response, + ) + self._log_metrics(result) + return result + + except Exception as exc: + latency = time.monotonic() - start + logger.error( + "ClaudeSDKExecuteStep failed after %.2fs: %s", + latency, + exc, + ) + lf = _get_logfire() + if lf is not None: + lf.error( + "ClaudeSDK call failed", + error=str(exc), + model=self.model, + latency_seconds=round(latency, 3), + ) + return ClaudeSDKResult( + task=task, + success=False, + error=str(exc), + model=self.model, + latency_seconds=round(latency, 3), + ) + + # ------------------------------------------------------------------ + # Observability helpers + # ------------------------------------------------------------------ + + @staticmethod + def _log_metrics(result: ClaudeSDKResult) -> None: + """Log structured observability metrics via logging and Logfire.""" + logger.info( + "ClaudeSDK: model=%s tokens=%d/%d/%d latency=%.2fs stop=%s tools=%d", + result.model, + result.input_tokens, + result.output_tokens, + result.total_tokens, + result.latency_seconds, + result.stop_reason, + len(result.tool_calls), + ) + lf = _get_logfire() + if lf is not None: + lf.info( + "ClaudeSDK call completed", + model=result.model, + input_tokens=result.input_tokens, + output_tokens=result.output_tokens, + total_tokens=result.total_tokens, + latency_seconds=result.latency_seconds, + stop_reason=result.stop_reason, + tool_call_count=len(result.tool_calls), + ) + + @staticmethod + def _extract_skill_ids(text: str) -> List[str]: + """Extract cited skill IDs from output text.""" + try: + from ..implementations.helpers import extract_cited_skill_ids + + return extract_cited_skill_ids(text) + except Exception: + return [] + + +# --------------------------------------------------------------------------- +# Convert step — ClaudeSDKResult → standardised trace dict +# --------------------------------------------------------------------------- + + +class ClaudeSDKToTrace: + """Convert a ``ClaudeSDKResult`` on ``ctx.trace`` to the standardised + trace dict that the learning tail (``ReflectStep``) expects. + + Includes observability metadata (tokens, latency) in reasoning and + feedback for the reflector to consider. + """ + + requires = frozenset({"trace"}) + provides = frozenset({"trace"}) + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + r: ClaudeSDKResult = ctx.trace # type: ignore[assignment] + + parts: List[str] = [] + status = "succeeded" if r.success else "failed" + parts.append(f"Claude SDK call {status} ({r.model})") + parts.append( + f"Tokens: {r.input_tokens} in / {r.output_tokens} out / " + f"{r.total_tokens} total" + ) + parts.append(f"Latency: {r.latency_seconds}s") + if r.stop_reason: + parts.append(f"Stop reason: {r.stop_reason}") + if r.tool_calls: + parts.append(f"\nTool calls ({len(r.tool_calls)}):") + for tc in r.tool_calls: + parts.append(f" - {tc.name}({tc.input})") + if r.output: + parts.append(f"\nOutput:\n{r.output}") + if r.error: + parts.append(f"\nError: {r.error}") + reasoning = "\n".join(parts) + + feedback = f"Claude SDK call {status}" + if r.error: + feedback += f"\nError: {r.error}" + feedback += ( + f"\nTokens: {r.input_tokens}+{r.output_tokens}={r.total_tokens}" + f" | Latency: {r.latency_seconds}s" + ) + + trace: dict = { + "question": r.task, + "reasoning": reasoning, + "answer": r.output, + "skill_ids": r.cited_skill_ids, + "feedback": feedback, + "ground_truth": None, + } + return ctx.replace(trace=trace) + + +__all__ = [ + "ClaudeSDKExecuteStep", + "ClaudeSDKResult", + "ClaudeSDKToTrace", + "ToolCall", +] diff --git a/ace/integrations/langchain.py b/ace/integrations/langchain.py new file mode 100644 index 0000000000000000000000000000000000000000..912733d22792fca23814941259de8a3aa29ddcf2 --- /dev/null +++ b/ace/integrations/langchain.py @@ -0,0 +1,416 @@ +"""LangChain integration — execute step, result type, and trace converter.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass, field +from typing import Any, Callable, Dict, List, Optional, Tuple + +from ..core.context import ACEStepContext +from ..implementations.prompts import wrap_skillbook_for_external_agent + +logger = logging.getLogger(__name__) + +try: + from langchain_core.runnables import Runnable + + LANGCHAIN_AVAILABLE = True +except ImportError: + LANGCHAIN_AVAILABLE = False + Runnable = None # type: ignore + +try: + from langchain.agents import AgentExecutor + + AGENT_EXECUTOR_AVAILABLE = True +except ImportError: + AGENT_EXECUTOR_AVAILABLE = False + AgentExecutor = None # type: ignore + +try: + from langgraph.graph.state import CompiledStateGraph + + LANGGRAPH_AVAILABLE = True +except ImportError: + LANGGRAPH_AVAILABLE = False + CompiledStateGraph = None # type: ignore + + +# --------------------------------------------------------------------------- +# Input / Output types +# --------------------------------------------------------------------------- + + +@dataclass +class LangChainResult: + """Output from a LangChain Runnable execution. + + This is the integration-specific result — not yet in ACE trace format. + Use ``LangChainToTrace`` to convert to a standardised trace dict. + + ``result_type`` indicates the source variant: + - ``"simple"`` — basic chain (prompt | llm) + - ``"agent"`` — AgentExecutor with intermediate_steps + - ``"langgraph"`` — LangGraph CompiledStateGraph with messages + - ``"error"`` — execution failed + """ + + task: str + output: str = "" + result_type: str = "simple" + success: bool = True + error: Optional[str] = None + intermediate_steps: List[Tuple[Any, Any]] = field(default_factory=list) + messages: List[Any] = field(default_factory=list) + raw_result: Any = None + + +# --------------------------------------------------------------------------- +# Execute step +# --------------------------------------------------------------------------- + + +class LangChainExecuteStep: + """INJECT skillbook context and EXECUTE a LangChain Runnable. + + Reads input from ``ctx.sample`` (string, dict, or message list), + writes a ``LangChainResult`` to ``ctx.trace``. + + Handles three Runnable variants automatically: + - Simple chains (prompt | llm) + - AgentExecutor (intermediate_steps tracing) + - LangGraph CompiledStateGraph (message-based I/O) + """ + + requires = frozenset({"sample", "skillbook"}) + provides = frozenset({"trace"}) + + def __init__( + self, + runnable: Any, + output_parser: Optional[Callable[[Any], str]] = None, + ) -> None: + if not LANGCHAIN_AVAILABLE: + raise ImportError( + "LangChain is not installed. Install with: " + "pip install langchain-core" + ) + self.runnable = runnable + self.output_parser = output_parser or self._default_output_parser + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + task = self._get_task_str(ctx.sample) + + # -- INJECT -- + enhanced_input = self._inject_context(ctx.sample, ctx.skillbook) + + # -- EXECUTE -- + is_agent = self._is_agent_executor() + is_langgraph = self._is_langgraph() + + original_setting = False + if is_agent: + original_setting = getattr( + self.runnable, "return_intermediate_steps", False + ) + self.runnable.return_intermediate_steps = True + + try: + raw = self.runnable.invoke(enhanced_input) + except Exception as exc: + if is_agent: + self.runnable.return_intermediate_steps = original_setting + result = LangChainResult( + task=task, + output=f"Failed: {exc}", + result_type="error", + success=False, + error=str(exc), + ) + return ctx.replace(trace=result) + finally: + if is_agent: + self.runnable.return_intermediate_steps = original_setting + + # -- BUILD RESULT -- + if is_agent and isinstance(raw, dict) and "intermediate_steps" in raw: + result = self._build_agent_result(task, raw) + elif is_langgraph and isinstance(raw, dict) and "messages" in raw: + result = self._build_langgraph_result(task, raw) + else: + result = self._build_simple_result(task, raw) + + return ctx.replace(trace=result) + + # ------------------------------------------------------------------ + # Injection + # ------------------------------------------------------------------ + + @staticmethod + def _inject_context(original_input: Any, skillbook: Any) -> Any: + if skillbook is None: + return original_input + context = wrap_skillbook_for_external_agent(skillbook) + if not context: + return original_input + + if isinstance(original_input, str): + return f"{original_input}\n\n{context}" + + if isinstance(original_input, dict) and "messages" in original_input: + messages = original_input["messages"] + if messages and hasattr(messages[0], "content"): + enhanced = list(messages) + first = enhanced[0] + enhanced[0] = type(first)(content=f"{context}\n\n{first.content}") + return { + "messages": enhanced, + **{k: v for k, v in original_input.items() if k != "messages"}, + } + return original_input + + if isinstance(original_input, dict) and "input" in original_input: + enhanced_dict = original_input.copy() + enhanced_dict["input"] = f"{original_input['input']}\n\n{context}" + return enhanced_dict + + if isinstance(original_input, dict): + enhanced_dict = original_input.copy() + enhanced_dict["skillbook_context"] = context + return enhanced_dict + + return original_input + + # ------------------------------------------------------------------ + # Result builders + # ------------------------------------------------------------------ + + def _build_simple_result(self, task: str, raw: Any) -> LangChainResult: + return LangChainResult( + task=task, + output=self.output_parser(raw), + result_type="simple", + raw_result=raw, + ) + + def _build_agent_result(self, task: str, raw: Dict[str, Any]) -> LangChainResult: + output = raw.get("output", "") + steps = raw.get("intermediate_steps", []) + + intermediate: List[Tuple[Any, Any]] = [] + for step_tuple in steps: + if len(step_tuple) == 2: + intermediate.append(tuple(step_tuple)) # type: ignore[arg-type] + + return LangChainResult( + task=task, + output=str(output), + result_type="agent", + intermediate_steps=intermediate, + raw_result=raw, + ) + + def _build_langgraph_result( + self, task: str, raw: Dict[str, Any] + ) -> LangChainResult: + messages = raw.get("messages", []) + output = self._extract_langgraph_output(raw) + intermediate = self._extract_langgraph_steps(raw) + + return LangChainResult( + task=task, + output=output, + result_type="langgraph", + intermediate_steps=intermediate, + messages=list(messages), + raw_result=raw, + ) + + # ------------------------------------------------------------------ + # LangGraph helpers + # ------------------------------------------------------------------ + + @staticmethod + def _extract_langgraph_output(result: Dict[str, Any]) -> str: + for msg in reversed(result.get("messages", [])): + if hasattr(msg, "content") and msg.content: + msg_type = getattr(msg, "type", msg.__class__.__name__.lower()) + if msg_type != "tool": + return str(msg.content) + return "" + + @staticmethod + def _extract_langgraph_steps( + result: Dict[str, Any], + ) -> List[Tuple[Any, Any]]: + intermediate: List[Tuple[Any, Any]] = [] + for msg in result.get("messages", []): + msg_type = getattr(msg, "type", msg.__class__.__name__.lower()) + content = getattr(msg, "content", str(msg)) + if msg_type == "ai": + for tc in getattr(msg, "tool_calls", []): + intermediate.append((tc, None)) + elif msg_type == "tool": + for i in range(len(intermediate) - 1, -1, -1): + if intermediate[i][1] is None: + intermediate[i] = (intermediate[i][0], content) + break + return intermediate + + # ------------------------------------------------------------------ + # Utilities + # ------------------------------------------------------------------ + + def _is_agent_executor(self) -> bool: + if not AGENT_EXECUTOR_AVAILABLE or AgentExecutor is None: + return False + return isinstance(self.runnable, AgentExecutor) + + def _is_langgraph(self) -> bool: + if not LANGGRAPH_AVAILABLE or CompiledStateGraph is None: + return False + return isinstance(self.runnable, CompiledStateGraph) + + @staticmethod + def _get_task_str(original_input: Any) -> str: + if isinstance(original_input, str): + return original_input + if isinstance(original_input, dict): + if "messages" in original_input: + messages = original_input["messages"] + if messages and hasattr(messages[0], "content"): + return str(messages[0].content) + return ( + original_input.get("input") + or original_input.get("question") + or original_input.get("query") + or str(original_input) + ) + return str(original_input) + + @staticmethod + def _default_output_parser(result: Any) -> str: + if isinstance(result, str): + return result + if hasattr(result, "content"): + return str(result.content) + if isinstance(result, dict): + for key in ("output", "answer", "result", "text"): + if key in result: + return str(result[key]) + return str(result) + return str(result) + + +# --------------------------------------------------------------------------- +# Convert step — LangChainResult → standardised trace dict +# --------------------------------------------------------------------------- + + +class LangChainToTrace: + """Convert a ``LangChainResult`` on ``ctx.trace`` to the standardised + trace dict that the learning tail (``ReflectStep``) expects. + """ + + requires = frozenset({"trace"}) + provides = frozenset({"trace"}) + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + r: LangChainResult = ctx.trace # type: ignore[assignment] + + reasoning = self._build_reasoning(r) + + if r.success: + feedback = self._build_feedback(r) + else: + feedback = f"Chain execution failed. Error: {r.error}" + + trace: dict = { + "question": r.task, + "reasoning": reasoning, + "answer": r.output, + "skill_ids": [], + "feedback": feedback, + "ground_truth": None, + } + return ctx.replace(trace=trace) + + # ------------------------------------------------------------------ + # Reasoning formatters per result type + # ------------------------------------------------------------------ + + @staticmethod + def _build_reasoning(r: LangChainResult) -> str: + if r.result_type == "error": + return ( + f"Question/Task: {r.task}\n\n" + f"Execution Result: FAILED\nError: {r.error}" + ) + + if r.result_type == "agent": + parts = [f"Question/Task: {r.task}", ""] + parts.append( + f"=== AGENT EXECUTION TRACE ({len(r.intermediate_steps)} steps) ===" + ) + for i, (action, observation) in enumerate(r.intermediate_steps, 1): + parts.append(f"\n--- Step {i} ---") + if hasattr(action, "log") and action.log: + parts.append(f"Thought: {action.log}") + if hasattr(action, "tool"): + parts.append(f"Action: {action.tool}") + parts.append(f"Action Input: {str(action.tool_input)[:300]}") + parts.append(f"Observation: {str(observation)[:300]}") + parts.append("\n=== END TRACE ===") + parts.append(f"\nFinal Answer: {r.output}") + return "\n".join(parts) + + if r.result_type == "langgraph": + msg_parts: list[str] = [] + for msg in r.messages: + msg_type = getattr(msg, "type", msg.__class__.__name__.lower()) + content = getattr(msg, "content", str(msg)) + if msg_type == "human": + msg_parts.append(f"Human: {str(content)[:300]}") + elif msg_type == "ai": + if content: + msg_parts.append(f"Assistant: {str(content)[:300]}") + for tc in getattr(msg, "tool_calls", []): + name = ( + tc.get("name", "unknown") + if isinstance(tc, dict) + else getattr(tc, "name", "unknown") + ) + msg_parts.append(f" Tool Call: {name}") + elif msg_type == "tool": + msg_parts.append(f"Tool Result: {str(content)[:300]}") + + trace_str = "\n".join(msg_parts) + return ( + f"Question/Task: {r.task}\n\n" + f"=== LANGGRAPH EXECUTION TRACE ({len(r.messages)} messages) ===\n" + f"{trace_str}\n" + f"=== END TRACE ===\n\n" + f"Final Answer: {r.output}" + ) + + # simple + return ( + f"Question/Task: {r.task}\n\n" + f"Chain Output: {r.output}\n\n" + f"Note: External LangChain chain execution." + ) + + @staticmethod + def _build_feedback(r: LangChainResult) -> str: + if r.result_type == "agent": + return f"Agent completed task in {len(r.intermediate_steps)} steps" + if r.result_type == "langgraph": + return f"LangGraph agent completed in {len(r.messages)} messages" + return f"External chain completed for task: {r.task[:200]}" + + +__all__ = [ + "LangChainExecuteStep", + "LangChainResult", + "LangChainToTrace", +] diff --git a/ace/integrations/mcp/__init__.py b/ace/integrations/mcp/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ea7232d6c4f719b3b76ddf280abbf2c766de3649 --- /dev/null +++ b/ace/integrations/mcp/__init__.py @@ -0,0 +1 @@ +"""ACE MCP Server Integration.""" diff --git a/ace/integrations/mcp/adapters.py b/ace/integrations/mcp/adapters.py new file mode 100644 index 0000000000000000000000000000000000000000..ae8c5c0fd379934900abebbd739e2bfd87243a1d --- /dev/null +++ b/ace/integrations/mcp/adapters.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import json +from importlib import import_module +from typing import Any + +from ace.integrations.mcp.handlers import MCPHandlers +from ace.integrations.mcp.models import ( + AskRequest, + LearnFeedbackRequest, + LearnSampleRequest, + SkillbookGetRequest, + SkillbookLoadRequest, + SkillbookSaveRequest, +) +from ace.integrations.mcp.errors import map_error_to_mcp + +_MCP_INSTALL_HINT = ( + "ACE MCP support is optional. Install it with " + '`pip install "ace-framework[mcp]"` or `uv add "ace-framework[mcp]"`.' +) + + +def _load_mcp_types(): + try: + return import_module("mcp.types") + except ModuleNotFoundError as exc: + if (exc.name or "").split(".")[0] == "mcp": + raise RuntimeError(_MCP_INSTALL_HINT) from exc + raise + + +def _mcp_schema(model: Any) -> dict[str, Any]: + """Return an MCP-friendly JSON schema for a Pydantic model. + + Some MCP clients (e.g. the Inspector) don't resolve ``$defs``/``$ref`` + correctly and reject valid input. This helper inlines all ``$ref`` + pointers so the schema is self-contained. + + We keep ``additionalProperties: false`` in the published schema so + clients know that extra fields will be rejected by validation. + """ + schema = model.model_json_schema() + defs = schema.pop("$defs", {}) + + def _resolve(obj: Any) -> Any: + if isinstance(obj, dict): + if "$ref" in obj: + ref_name = obj["$ref"].rsplit("/", 1)[-1] + return _resolve(defs.get(ref_name, {})) + return {k: _resolve(v) for k, v in obj.items()} + if isinstance(obj, list): + return [_resolve(item) for item in obj] + return obj + + return _resolve(schema) + + +# ── Tool dispatch table ──────────────────────────────────────────── + +_TOOL_DISPATCH: dict[str, tuple[type, str]] = { + "ace.ask": (AskRequest, "handle_ask"), + "ace.learn.sample": (LearnSampleRequest, "handle_learn_sample"), + "ace.learn.feedback": (LearnFeedbackRequest, "handle_learn_feedback"), + "ace.skillbook.get": (SkillbookGetRequest, "handle_skillbook_get"), + "ace.skillbook.save": (SkillbookSaveRequest, "handle_skillbook_save"), + "ace.skillbook.load": (SkillbookLoadRequest, "handle_skillbook_load"), +} + + +def register_tools(server: Any, handlers: MCPHandlers) -> None: + types = _load_mcp_types() + + @server.list_tools() + async def handle_list_tools(): + return [ + types.Tool( + name="ace.ask", + description="Ask a question and get a response from ACE.", + inputSchema=_mcp_schema(AskRequest), + ), + types.Tool( + name="ace.learn.sample", + description="Provide sample questions/answers for ACE to learn from.", + inputSchema=_mcp_schema(LearnSampleRequest), + ), + types.Tool( + name="ace.learn.feedback", + description="Provide feedback on an ACE answer.", + inputSchema=_mcp_schema(LearnFeedbackRequest), + ), + types.Tool( + name="ace.skillbook.get", + description="Get statistics and skills from the active skillbook.", + inputSchema=_mcp_schema(SkillbookGetRequest), + ), + types.Tool( + name="ace.skillbook.save", + description="Save the active skillbook to disk.", + inputSchema=_mcp_schema(SkillbookSaveRequest), + ), + types.Tool( + name="ace.skillbook.load", + description="Load a skillbook from disk into the session.", + inputSchema=_mcp_schema(SkillbookLoadRequest), + ), + ] + + @server.call_tool() + async def handle_call_tool(name: str, arguments: dict | None): + args = arguments or {} + try: + entry = _TOOL_DISPATCH.get(name) + if entry is None: + raise ValueError(f"Unknown tool: {name}") + + request_cls, handler_method = entry + req = request_cls(**args) + resp = await getattr(handlers, handler_method)(req) + return types.CallToolResult( + content=[types.TextContent(type="text", text=resp.model_dump_json())], + ) + + except Exception as e: + mcp_err = map_error_to_mcp(e) + return types.CallToolResult( + isError=True, + content=[types.TextContent(type="text", text=json.dumps(mcp_err))], + ) diff --git a/ace/integrations/mcp/config.py b/ace/integrations/mcp/config.py new file mode 100644 index 0000000000000000000000000000000000000000..97dba9bcca0d281e0f933a6f646fa726a2956a1e --- /dev/null +++ b/ace/integrations/mcp/config.py @@ -0,0 +1,21 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic import Field + + +class MCPServerConfig(BaseSettings): + """Configuration for the ACE MCP Server.""" + + default_model: str = Field(default="gpt-4o-mini") + safe_mode: bool = Field(default=False) + max_samples_per_call: int = Field(default=25) + max_prompt_chars: int = Field(default=100_000) + session_ttl_seconds: int = Field(default=3600) + allow_save_load: bool = Field(default=True) + learn_timeout_seconds: int = Field(default=300) + skillbook_root: str | None = Field(default=None) + log_level: str = Field(default="INFO") + + model_config = SettingsConfigDict( + env_prefix="ACE_MCP_", + case_sensitive=False, + ) diff --git a/ace/integrations/mcp/errors.py b/ace/integrations/mcp/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..76ebef5ff2b4bb37f9532f89efd16778fe2a94bc --- /dev/null +++ b/ace/integrations/mcp/errors.py @@ -0,0 +1,65 @@ +class ACEMCPError(Exception): + """Base exception for ACE MCP extensions.""" + + def __init__(self, message: str, code: str, details: dict | None = None): + super().__init__(message) + self.message = message + self.code = code + self.details = details or {} + + +class ValidationError(ACEMCPError): + def __init__(self, message: str, details: dict | None = None): + super().__init__(message, "ACE_MCP_VALIDATION_ERROR", details) + + +class SessionNotFoundError(ACEMCPError): + def __init__(self, session_id: str): + super().__init__( + f"Session not found: {session_id}", + "ACE_MCP_SESSION_NOT_FOUND", + {"session_id": session_id}, + ) + + +class ForbiddenInSafeModeError(ACEMCPError): + def __init__(self, tool_name: str): + super().__init__( + f"Tool {tool_name} is forbidden in safe mode", + "ACE_MCP_FORBIDDEN_IN_SAFE_MODE", + {"tool_name": tool_name}, + ) + + +class SaveLoadDisabledError(ACEMCPError): + def __init__(self, tool_name: str): + super().__init__( + f"Tool {tool_name} is disabled (allow_save_load=false)", + "ACE_MCP_SAVE_LOAD_DISABLED", + {"tool_name": tool_name}, + ) + + +class ProviderError(ACEMCPError): + def __init__(self, message: str, details: dict | None = None): + super().__init__(message, "ACE_MCP_PROVIDER_ERROR", details) + + +class TimeoutError(ACEMCPError): + def __init__(self, message: str = "Operation timed out"): + super().__init__(message, "ACE_MCP_TIMEOUT") + + +class InternalError(ACEMCPError): + def __init__(self, message: str, details: dict | None = None): + super().__init__(message, "ACE_MCP_INTERNAL_ERROR", details) + + +def map_error_to_mcp(err: Exception) -> dict: + if isinstance(err, ACEMCPError): + return {"code": err.code, "message": err.message, "details": err.details} + return { + "code": "ACE_MCP_INTERNAL_ERROR", + "message": f"An unexpected error occurred: {str(err)}", + "details": {"type": type(err).__name__}, + } diff --git a/ace/integrations/mcp/handlers.py b/ace/integrations/mcp/handlers.py new file mode 100644 index 0000000000000000000000000000000000000000..73d36712f9800d7fb1841c78e9abf9c46c9b23bc --- /dev/null +++ b/ace/integrations/mcp/handlers.py @@ -0,0 +1,339 @@ +from typing import Any +import asyncio +from pathlib import Path + +from ace.integrations.mcp.registry import SessionRegistry +from ace.integrations.mcp.models import ( + AskRequest, + AskResponse, + LearnSampleRequest, + LearnSampleResponse, + LearnFeedbackRequest, + LearnFeedbackResponse, + SkillbookGetRequest, + SkillbookGetResponse, + SkillbookSaveRequest, + SkillbookSaveResponse, + SkillbookLoadRequest, + SkillbookLoadResponse, + SkillItem, +) +from ace.integrations.mcp.config import MCPServerConfig +from ace.integrations.mcp.errors import ( + ACEMCPError, + ForbiddenInSafeModeError, + InternalError, + SaveLoadDisabledError, + TimeoutError as MCPTimeoutError, + ValidationError, +) +from ace.core.environments import Sample + + +class MCPHandlers: + def __init__(self, registry: SessionRegistry, config: MCPServerConfig): + self.registry = registry + self.config = config + + def _get_session_kwargs(self, config_model) -> tuple[str | None, dict[str, Any]]: + target_model = None + kwargs: dict[str, Any] = {} + if config_model: + target_model = config_model.model # may be None per contract + if config_model.temperature is not None: + kwargs["temperature"] = config_model.temperature + if config_model.max_tokens is not None: + kwargs["max_tokens"] = config_model.max_tokens + return target_model, kwargs + + def _enforce_prompt_limit(self, char_count: int, field_name: str) -> None: + if char_count > self.config.max_prompt_chars: + raise ValidationError( + f"{field_name} exceeds max_prompt_chars ({self.config.max_prompt_chars})", + details={ + "field": field_name, + "char_count": char_count, + "max_prompt_chars": self.config.max_prompt_chars, + }, + ) + + def _resolve_skillbook_path(self, path: str) -> str: + """Resolve a user-provided path and validate it against skillbook_root. + + Returns the resolved absolute path string so callers use the + validated path — not the raw user input — for file operations, + eliminating TOCTOU races with symlinks or ``..`` components. + """ + resolved = str(Path(path).expanduser().resolve()) + + if not self.config.skillbook_root: + return resolved + + root = Path(self.config.skillbook_root).expanduser().resolve() + try: + Path(resolved).relative_to(root) + except ValueError as exc: + raise ValidationError( + "Path is outside configured skillbook_root", + details={ + "path": resolved, + "skillbook_root": str(root), + }, + ) from exc + + return resolved + + async def handle_ask(self, request: AskRequest) -> AskResponse: + self._enforce_prompt_limit(len(request.question) + len(request.context), "ask") + + target_model, kwargs = self._get_session_kwargs(request.session_config) + session = await self.registry.get_or_create( + request.session_id, model=target_model, **kwargs + ) + + async with session.lock: + try: + answer = await asyncio.to_thread( + session.runner.ask, request.question, request.context + ) + skill_count = len(session.runner.skillbook.skills()) + + return AskResponse( + session_id=request.session_id, + answer=str(answer), + skill_count=skill_count, + ) + except ACEMCPError: + raise + except Exception as e: + raise InternalError(str(e)) + + async def handle_skillbook_get( + self, request: SkillbookGetRequest + ) -> SkillbookGetResponse: + session = await self.registry.get(request.session_id) + + async with session.lock: + try: + skillbook = session.runner.skillbook + skills = skillbook.skills(include_invalid=request.include_invalid) + + limited_skills: list[SkillItem] = [] + for s in skills: + content = getattr(s, "insight", None) or getattr(s, "issue", None) + limited_skills.append( + SkillItem( + id=getattr(s, "id", str(len(limited_skills))), + content=content if content is not None else str(s), + topic=getattr(s, "section", None), + helpful=getattr(s, "helpful_count", None), + harmful=getattr(s, "harmful_count", None), + neutral=getattr(s, "neutral_count", None), + ) + ) + + limited_skills = limited_skills[: request.limit] + + stats = skillbook.stats() + + return SkillbookGetResponse( + session_id=request.session_id, stats=stats, skills=limited_skills + ) + except ACEMCPError: + raise + except Exception as e: + raise InternalError(str(e)) + + async def handle_learn_sample( + self, request: LearnSampleRequest + ) -> LearnSampleResponse: + if self.config.safe_mode: + raise ForbiddenInSafeModeError("ace.learn.sample") + + if len(request.samples) > self.config.max_samples_per_call: + raise ValidationError( + f"samples exceeds max_samples_per_call ({self.config.max_samples_per_call})", + details={ + "sample_count": len(request.samples), + "max_samples_per_call": self.config.max_samples_per_call, + }, + ) + + for idx, s in enumerate(request.samples): + self._enforce_prompt_limit( + len(s.question) + len(s.context), + f"samples[{idx}]", + ) + + target_model, kwargs = self._get_session_kwargs(request.session_config) + session = await self.registry.get_or_create( + request.session_id, model=target_model, **kwargs + ) + + async with session.lock: + try: + samples = [] + for s in request.samples: + samples.append( + Sample( + question=s.question, + context=s.context, + ground_truth=s.ground_truth, + metadata=s.metadata or {}, + ) + ) + + count_before = len(session.runner.skillbook.skills()) + + results = await asyncio.wait_for( + asyncio.to_thread( + session.runner.learn, + samples, + None, + request.epochs, + ), + timeout=self.config.learn_timeout_seconds, + ) + + failed = sum(1 for r in results if r.error is not None) + count_after = len(session.runner.skillbook.skills()) + + return LearnSampleResponse( + session_id=request.session_id, + processed=len(samples) - failed, + failed=failed, + skill_count_before=count_before, + skill_count_after=count_after, + new_skill_count=max(0, count_after - count_before), + ) + except ACEMCPError: + raise + except asyncio.TimeoutError: + raise MCPTimeoutError( + f"learn.sample timed out after {self.config.learn_timeout_seconds}s" + ) + except Exception as e: + raise InternalError(str(e)) + + async def handle_learn_feedback( + self, request: LearnFeedbackRequest + ) -> LearnFeedbackResponse: + if self.config.safe_mode: + raise ForbiddenInSafeModeError("ace.learn.feedback") + + self._enforce_prompt_limit( + len(request.question) + + len(request.context) + + len(request.answer) + + len(request.feedback) + + len(request.ground_truth or ""), + "learn.feedback", + ) + + target_model, kwargs = self._get_session_kwargs(request.session_config) + session = await self.registry.get_or_create( + request.session_id, model=target_model, **kwargs + ) + + async with session.lock: + try: + count_before = len(session.runner.skillbook.skills()) + + # Prefer the direct feedback path when a prior ask exists; + # fall back to learn_from_traces for standalone feedback. + timeout = self.config.learn_timeout_seconds + learned = await asyncio.wait_for( + asyncio.to_thread( + session.runner.learn_from_feedback, + request.feedback, + request.ground_truth or None, + ), + timeout=timeout, + ) + + if not learned: + # No prior ask interaction — build a trace and learn + trace: dict[str, object] = { + "question": request.question, + "context": request.context, + "answer": request.answer, + "skill_ids": [], + "feedback": request.feedback, + "ground_truth": request.ground_truth, + } + await asyncio.wait_for( + asyncio.to_thread(session.runner.learn_from_traces, [trace]), + timeout=timeout, + ) + + count_after = len(session.runner.skillbook.skills()) + + return LearnFeedbackResponse( + session_id=request.session_id, + learned=True, + skill_count_before=count_before, + skill_count_after=count_after, + new_skill_count=max(0, count_after - count_before), + ) + except ACEMCPError: + raise + except asyncio.TimeoutError: + raise MCPTimeoutError( + f"learn.feedback timed out after {self.config.learn_timeout_seconds}s" + ) + except Exception as e: + raise InternalError(str(e)) + + async def handle_skillbook_save( + self, request: SkillbookSaveRequest + ) -> SkillbookSaveResponse: + if self.config.safe_mode: + raise ForbiddenInSafeModeError("ace.skillbook.save") + if not self.config.allow_save_load: + raise SaveLoadDisabledError("ace.skillbook.save") + + resolved = self._resolve_skillbook_path(request.path) + + session = await self.registry.get(request.session_id) + + async with session.lock: + try: + await asyncio.to_thread(session.runner.save, resolved) + skill_count = len(session.runner.skillbook.skills()) + + return SkillbookSaveResponse( + session_id=request.session_id, + path=resolved, + saved_skill_count=skill_count, + ) + except ACEMCPError: + raise + except Exception as e: + raise InternalError(str(e)) + + async def handle_skillbook_load( + self, request: SkillbookLoadRequest + ) -> SkillbookLoadResponse: + if self.config.safe_mode: + raise ForbiddenInSafeModeError("ace.skillbook.load") + if not self.config.allow_save_load: + raise SaveLoadDisabledError("ace.skillbook.load") + + resolved = self._resolve_skillbook_path(request.path) + + session = await self.registry.get_or_create(request.session_id) + + async with session.lock: + try: + await asyncio.to_thread(session.runner.load, resolved) + skill_count = len(session.runner.skillbook.skills()) + + return SkillbookLoadResponse( + session_id=request.session_id, + path=resolved, + skill_count=skill_count, + ) + except ACEMCPError: + raise + except Exception as e: + raise InternalError(str(e)) diff --git a/ace/integrations/mcp/models.py b/ace/integrations/mcp/models.py new file mode 100644 index 0000000000000000000000000000000000000000..f0b3410f48382cdcbea46e4c1706d84121d7f7db --- /dev/null +++ b/ace/integrations/mcp/models.py @@ -0,0 +1,144 @@ +from typing import Any +from pydantic import BaseModel, ConfigDict, Field + + +class SessionConfig(BaseModel): + model: str | None = Field(default=None, min_length=1) + temperature: float | None = Field(default=None, ge=0, le=2) + max_tokens: int | None = Field(default=None, ge=1) + + model_config = ConfigDict(extra="forbid") + + +class ErrorEnvelope(BaseModel): + code: str + message: str + details: dict[str, Any] | None = None + + model_config = ConfigDict(extra="forbid") + + +class AskRequest(BaseModel): + session_id: str = Field(min_length=1) + question: str = Field(min_length=1, max_length=100000) + context: str = Field(default="") + session_config: SessionConfig | None = None + metadata: dict[str, Any] | None = None + + model_config = ConfigDict(extra="forbid") + + +class AskResponse(BaseModel): + session_id: str + answer: str + skill_count: int = Field(ge=0) + + model_config = ConfigDict(extra="forbid") + + +class SampleItem(BaseModel): + question: str = Field(min_length=1) + context: str = Field(default="") + ground_truth: str | None = Field(default=None) + metadata: dict[str, Any] | None = None + + model_config = ConfigDict(extra="forbid") + + +class LearnSampleRequest(BaseModel): + session_id: str = Field(min_length=1) + samples: list[SampleItem] = Field(min_length=1, max_length=25) + epochs: int = Field(default=1, ge=1, le=20) + session_config: SessionConfig | None = None + + model_config = ConfigDict(extra="forbid") + + +class LearnSampleResponse(BaseModel): + session_id: str + processed: int = Field(ge=0) + failed: int = Field(default=0, ge=0) + skill_count_before: int = Field(ge=0) + skill_count_after: int = Field(ge=0) + new_skill_count: int = Field(ge=0) + + model_config = ConfigDict(extra="forbid") + + +class LearnFeedbackRequest(BaseModel): + session_id: str = Field(min_length=1) + question: str = Field(min_length=1) + answer: str = Field(min_length=1) + feedback: str = Field(min_length=1) + context: str = Field(default="") + ground_truth: str | None = Field(default=None) + session_config: SessionConfig | None = None + + model_config = ConfigDict(extra="forbid") + + +class LearnFeedbackResponse(BaseModel): + session_id: str + learned: bool + skill_count_before: int = Field(ge=0) + skill_count_after: int = Field(ge=0) + new_skill_count: int = Field(ge=0) + + model_config = ConfigDict(extra="forbid") + + +class SkillbookGetRequest(BaseModel): + session_id: str = Field(min_length=1) + limit: int = Field(default=20, ge=1, le=200) + include_invalid: bool = Field(default=False) + + model_config = ConfigDict(extra="forbid") + + +class SkillItem(BaseModel): + id: str + content: str + topic: str | None = None + helpful: int | None = None + harmful: int | None = None + neutral: int | None = None + + model_config = ConfigDict(extra="allow") + + +class SkillbookGetResponse(BaseModel): + session_id: str + stats: dict[str, Any] + skills: list[SkillItem] + + model_config = ConfigDict(extra="forbid") + + +class SkillbookSaveRequest(BaseModel): + session_id: str = Field(min_length=1) + path: str = Field(min_length=1) + + model_config = ConfigDict(extra="forbid") + + +class SkillbookSaveResponse(BaseModel): + session_id: str + path: str + saved_skill_count: int = Field(ge=0) + + model_config = ConfigDict(extra="forbid") + + +class SkillbookLoadRequest(BaseModel): + session_id: str = Field(min_length=1) + path: str = Field(min_length=1) + + model_config = ConfigDict(extra="forbid") + + +class SkillbookLoadResponse(BaseModel): + session_id: str + path: str + skill_count: int = Field(ge=0) + + model_config = ConfigDict(extra="forbid") diff --git a/ace/integrations/mcp/registry.py b/ace/integrations/mcp/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..fb47153d3f147fbbde739ff5a2570b047ba64250 --- /dev/null +++ b/ace/integrations/mcp/registry.py @@ -0,0 +1,100 @@ +import logging +import time +import asyncio +from dataclasses import dataclass, field +from typing import Dict, Any + +from ace.runners import ACELiteLLM +from ace.integrations.mcp.config import MCPServerConfig +from ace.integrations.mcp.errors import SessionNotFoundError + +logger = logging.getLogger(__name__) + + +@dataclass +class Session: + session_id: str + runner: ACELiteLLM + created_at: float = field(default_factory=time.time) + last_accessed: float = field(default_factory=time.time) + lock: asyncio.Lock = field(default_factory=asyncio.Lock) + + +class SessionRegistry: + def __init__(self, config: MCPServerConfig): + self.config = config + self._sessions: Dict[str, Session] = {} + self._registry_lock = asyncio.Lock() + + async def get_or_create( + self, session_id: str, model: str | None = None, **runner_kwargs: Any + ) -> Session: + """Get an existing session or create a new one, sweeping expired sessions first.""" + async with self._registry_lock: + expired = self._collect_expired() + + if session_id in self._sessions: + session = self._sessions[session_id] + session.last_accessed = time.time() + result = session + else: + # Create new runner + target_model = model or self.config.default_model + runner = ACELiteLLM.from_model(target_model, **runner_kwargs) + session = Session(session_id=session_id, runner=runner) + self._sessions[session_id] = session + result = session + + # Drain outside the lock so other callers aren't blocked + self._drain_sessions(expired) + return result + + async def get(self, session_id: str) -> Session: + """Get an existing session. Raises SessionNotFoundError if not found.""" + async with self._registry_lock: + expired = self._collect_expired() + + if session_id not in self._sessions: + # Drain before raising so we don't leak + self._drain_sessions(expired) + raise SessionNotFoundError(session_id) + + session = self._sessions[session_id] + session.last_accessed = time.time() + + self._drain_sessions(expired) + return session + + async def delete(self, session_id: str) -> None: + """Delete a session if it exists.""" + async with self._registry_lock: + session = self._sessions.pop(session_id, None) + + if session is not None: + self._drain_sessions([session]) + + def _collect_expired(self) -> list[Session]: + """Remove and return sessions that have exceeded the TTL. + + Must be called while holding ``_registry_lock``. + """ + now = time.time() + ttl = self.config.session_ttl_seconds + + expired_ids = [ + sid + for sid, session in self._sessions.items() + if now - session.last_accessed > ttl + ] + return [self._sessions.pop(sid) for sid in expired_ids] + + @staticmethod + def _drain_sessions(sessions: list[Session]) -> None: + """Best-effort wait for any in-progress background learning.""" + for session in sessions: + try: + session.runner.wait_for_background(timeout=2.0) + except Exception: + logger.debug( + "Failed to drain session %s", session.session_id, exc_info=True + ) diff --git a/ace/integrations/mcp/server.py b/ace/integrations/mcp/server.py new file mode 100644 index 0000000000000000000000000000000000000000..4bdb638e07b8cb823431d7b18c7bf96f0fc30500 --- /dev/null +++ b/ace/integrations/mcp/server.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import asyncio +import logging +import sys +from importlib import import_module +from typing import Any + +from ace.integrations.mcp.config import MCPServerConfig +from ace.integrations.mcp.handlers import MCPHandlers +from ace.integrations.mcp.registry import SessionRegistry + +_MCP_INSTALL_HINT = ( + "ACE MCP support is optional. Install it with " + '`pip install "ace-framework[mcp]"` or `uv add "ace-framework[mcp]"`.' +) + + +def _load_mcp_server_runtime() -> tuple[type[Any], Any]: + try: + server_module = import_module("mcp.server") + stdio_module = import_module("mcp.server.stdio") + except ModuleNotFoundError as exc: + if (exc.name or "").split(".")[0] == "mcp": + raise RuntimeError(_MCP_INSTALL_HINT) from exc + raise + return server_module.Server, stdio_module.stdio_server + + +def _load_register_tools(): + try: + from ace.integrations.mcp.adapters import register_tools + except ModuleNotFoundError as exc: + if (exc.name or "").split(".")[0] == "mcp": + raise RuntimeError(_MCP_INSTALL_HINT) from exc + raise + return register_tools + + +def create_server() -> Any: + Server, _ = _load_mcp_server_runtime() + register_tools = _load_register_tools() + config = MCPServerConfig() + + logging.basicConfig( + stream=sys.stderr, + level=getattr(logging, config.log_level.upper(), logging.INFO), + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + logger = logging.getLogger("ace_mcp_server") + logger.info("Starting ACE MCP Server...") + logger.info(f"Safe mode: {config.safe_mode}") + logger.info(f"Default model: {config.default_model}") + + registry = SessionRegistry(config) + handlers = MCPHandlers(registry, config) + + server = Server("ace-mcp-server") + register_tools(server, handlers) + + return server + + +async def run_server() -> None: + try: + _, stdio_server = _load_mcp_server_runtime() + server = create_server() + async with stdio_server() as (read_stream, write_stream): + await server.run( + read_stream, + write_stream, + server.create_initialization_options(), + ) + except Exception as e: + print(f"Failed to start ACE MCP Server: {e}", file=sys.stderr) + sys.exit(1) + + +def main() -> None: + """CLI Entrypoint for ace-mcp.""" + asyncio.run(run_server()) + + +if __name__ == "__main__": + main() diff --git a/ace/integrations/openclaw/__init__.py b/ace/integrations/openclaw/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..106103a6689e574667d79bba986c3807811936db --- /dev/null +++ b/ace/integrations/openclaw/__init__.py @@ -0,0 +1,9 @@ +"""OpenClaw integration — convert session transcripts to ACE traces.""" + +from __future__ import annotations + +from .to_trace import OpenClawToTraceStep + +__all__ = [ + "OpenClawToTraceStep", +] diff --git a/ace/integrations/openclaw/to_trace.py b/ace/integrations/openclaw/to_trace.py new file mode 100644 index 0000000000000000000000000000000000000000..62176e3f6de0ac27f05037659fdd72157bc0e013 --- /dev/null +++ b/ace/integrations/openclaw/to_trace.py @@ -0,0 +1,131 @@ +"""OpenClawToTraceStep — convert raw JSONL events to a structured trace dict.""" + +from __future__ import annotations + +from typing import Any + +from ...core.context import ACEStepContext + + +class OpenClawToTraceStep: + """Convert raw OpenClaw JSONL events into a structured trace dict. + + This step receives ``ctx.trace`` as a ``list[dict]`` of raw JSONL events + (placed by ``LoadTracesStep``) and converts them into the trace dict + format expected by ``ReflectStep``:: + + { + "question": str, # reconstructed conversation + "reasoning": str, # full execution trace (thinking + tool calls) + "answer": str, # last assistant text + "skill_ids": list, # always [] for OpenClaw + "feedback": str, # session summary + "ground_truth": None, + } + + Follows the same pattern as ``BrowserToTrace``, ``LangChainToTrace``, + and ``ClaudeCodeToTrace``. + """ + + requires = frozenset({"trace"}) + provides = frozenset({"trace"}) + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + events: list[dict[str, Any]] = ctx.trace # type: ignore[assignment] + if not events: + return ctx + + trace_dict = _events_to_trace(events) + return ctx.replace(trace=trace_dict) + + +def _events_to_trace(events: list[dict[str, Any]]) -> dict[str, Any]: + """Convert a list of OpenClaw JSONL events into the standardised trace dict.""" + user_messages: list[str] = [] + assistant_texts: list[str] = [] + reasoning_parts: list[str] = [] + model = "" + total_tokens = 0 + + for event in events: + etype = event.get("type") + + if etype == "session": + model = event.get("cwd", "") + continue + + if etype == "custom": + data = event.get("data", {}) + if data.get("modelId"): + model = data["modelId"] + continue + + if etype != "message": + continue + + msg = event.get("message", {}) + role = msg.get("role") + content_blocks = msg.get("content", []) + + # Track token usage + usage = msg.get("usage", {}) + total_tokens += usage.get("totalTokens", 0) + + # Track model + if msg.get("model"): + model = msg["model"] + + if role == "user": + for block in content_blocks: + if block.get("type") == "text": + user_messages.append(block["text"]) + + elif role == "assistant": + for block in content_blocks: + btype = block.get("type") + if btype == "thinking": + reasoning_parts.append(f"[thinking] {block.get('thinking', '')}") + elif btype == "text": + text = block.get("text", "") + assistant_texts.append(text) + reasoning_parts.append(f"[response] {text}") + elif btype == "toolCall": + name = block.get("name", "unknown") + args = block.get("arguments", {}) + reasoning_parts.append(f"[tool:{name}] {args}") + + elif role == "toolResult": + tool_name = msg.get("toolName", "unknown") + for block in content_blocks: + if block.get("type") == "text": + text = block["text"] + # Truncate long tool results + if len(text) > 500: + text = text[:500] + "..." + reasoning_parts.append(f"[tool_result:{tool_name}] {text}") + + # Build the conversation as the "question" + question = "\n\n".join(f"User: {m}" for m in user_messages) if user_messages else "" + + # Last assistant text as the "answer" + answer = assistant_texts[-1] if assistant_texts else "" + + # Build feedback summary + n_user = len(user_messages) + n_assistant = len(assistant_texts) + feedback = ( + f"OpenClaw session: {n_user} user messages, {n_assistant} assistant responses" + ) + if model: + feedback += f", model: {model}" + if total_tokens: + feedback += f", {total_tokens} tokens" + + return { + "question": question, + "reasoning": "\n".join(reasoning_parts), + "answer": answer, + "skill_ids": [], + "feedback": feedback, + "ground_truth": None, + } diff --git a/ace/observability/__init__.py b/ace/observability/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..50fb2052b42a202b09f89770a9bb0e51bde9c31a --- /dev/null +++ b/ace/observability/__init__.py @@ -0,0 +1,75 @@ +"""Observability utilities for ACE. + +Provides opt-in Logfire integration that auto-instruments all PydanticAI +agents (Agent, Reflector, SkillManager, RR). + +Usage:: + + from ace.observability import configure_logfire + + if configure_logfire(): + print("Logfire active") + +Or via runner:: + + ace = ACELiteLLM.from_model("gpt-4o-mini", logfire=True) +""" + +from __future__ import annotations + +import logging + +logger = logging.getLogger(__name__) + +_logfire_configured = False + + +def configure_logfire() -> bool: + """Configure Logfire and instrument PydanticAI agents. + + Reads ``LOGFIRE_TOKEN`` from the environment. Set + ``LOGFIRE_SEND_TO_LOGFIRE=false`` to disable sending in CI/local dev. + + Returns: + ``True`` if Logfire was configured successfully, ``False`` if the + ``logfire`` package is not installed. + + Raises: + No exceptions — returns False on ImportError. + """ + global _logfire_configured + if _logfire_configured: + return True + + try: + import logfire + + def scrubbing_callback(m: logfire.ScrubMatch): + if m.path == ("attributes", "trace", "reasoning"): + return m.value + if m.path == ("attributes", "trace", "answer"): + return m.value + if "messages" in m.path and "content" in m.path: + return m.value + if "payment_id" in m.path: + return m.value + if "tool_arguments" in m.path: + return m.value + if "tool_response" in m.path: + return m.value + + logfire.configure( + scrubbing=logfire.ScrubbingOptions(callback=scrubbing_callback) + ) + logfire.instrument_pydantic_ai() + _logfire_configured = True + logger.info("Logfire configured — PydanticAI agents instrumented") + return True + except ImportError: + logger.debug("logfire not installed — skipping instrumentation") + return False + + +def is_configured() -> bool: + """Return ``True`` if :func:`configure_logfire` has been called successfully.""" + return _logfire_configured diff --git a/ace/protocols/__init__.py b/ace/protocols/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c4521032455ee8dee4defa3106caf717e04ede77 --- /dev/null +++ b/ace/protocols/__init__.py @@ -0,0 +1,14 @@ +"""Public contracts — protocols that steps depend on, not concrete classes.""" + +from .agent import AgentLike +from .deduplication import DeduplicationConfig, DeduplicationManagerLike +from .reflector import ReflectorLike +from .skill_manager import SkillManagerLike + +__all__ = [ + "AgentLike", + "DeduplicationConfig", + "DeduplicationManagerLike", + "ReflectorLike", + "SkillManagerLike", +] diff --git a/ace/protocols/agent.py b/ace/protocols/agent.py new file mode 100644 index 0000000000000000000000000000000000000000..2dd5b4f56bc3b5ec1b679777ece3c76839e55050 --- /dev/null +++ b/ace/protocols/agent.py @@ -0,0 +1,26 @@ +"""Protocol defining what steps need from an Agent implementation.""" + +from __future__ import annotations + +from typing import Any, Optional, Protocol, runtime_checkable + +from ..core.outputs import AgentOutput + + +@runtime_checkable +class AgentLike(Protocol): + """Structural interface for Agent-like objects. + + Any object with a matching ``generate`` method satisfies this — + ``ace.roles.Agent`` and ``ace.roles.ReplayAgent`` both do. + """ + + def generate( + self, + *, + question: str, + context: Optional[str], + skillbook: Any, + reflection: Optional[str] = ..., + **kwargs: Any, + ) -> AgentOutput: ... diff --git a/ace/protocols/deduplication.py b/ace/protocols/deduplication.py new file mode 100644 index 0000000000000000000000000000000000000000..99cc40c361d3f301ab66f6043359163ea046b0bf --- /dev/null +++ b/ace/protocols/deduplication.py @@ -0,0 +1,32 @@ +"""Protocol and config for skill deduplication.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal, Optional, Protocol, runtime_checkable + +if TYPE_CHECKING: + from ..core.skillbook import Skillbook + + +@dataclass +class DeduplicationConfig: + """Configuration for skill deduplication.""" + + enabled: bool = True + embedding_model: str = "text-embedding-3-small" + embedding_provider: Literal["litellm", "sentence_transformers"] = "litellm" + similarity_threshold: float = 0.85 + min_pairs_to_report: int = 1 + within_section_only: bool = True + local_model_name: str = "all-MiniLM-L6-v2" + + +@runtime_checkable +class DeduplicationManagerLike(Protocol): + """Structural interface for deduplication managers. + + The concrete ``ace.deduplication.DeduplicationManager`` satisfies this. + """ + + def get_similarity_report(self, skillbook: "Skillbook") -> Optional[str]: ... diff --git a/ace/protocols/reflector.py b/ace/protocols/reflector.py new file mode 100644 index 0000000000000000000000000000000000000000..ecd04ddf4d6626efe50f7a6715bdbf84a5421ad6 --- /dev/null +++ b/ace/protocols/reflector.py @@ -0,0 +1,27 @@ +"""Protocol defining what steps need from a Reflector implementation.""" + +from __future__ import annotations + +from typing import Any, Optional, Protocol, runtime_checkable + +from ..core.outputs import AgentOutput, ReflectorOutput + + +@runtime_checkable +class ReflectorLike(Protocol): + """Structural interface for Reflector-like objects. + + Any object with a matching ``reflect`` method satisfies this — + ``ace.roles.Reflector`` does. + """ + + def reflect( + self, + *, + question: str, + agent_output: AgentOutput, + skillbook: Any, + ground_truth: Optional[str] = ..., + feedback: Optional[str] = ..., + **kwargs: Any, + ) -> ReflectorOutput: ... diff --git a/ace/protocols/skill_manager.py b/ace/protocols/skill_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..d9a79534ad8827d400621d6a70a7fd444311f963 --- /dev/null +++ b/ace/protocols/skill_manager.py @@ -0,0 +1,26 @@ +"""Protocol defining what steps need from a SkillManager implementation.""" + +from __future__ import annotations + +from typing import Any, Protocol, runtime_checkable + +from ..core.outputs import ReflectorOutput, SkillManagerOutput + + +@runtime_checkable +class SkillManagerLike(Protocol): + """Structural interface for SkillManager-like objects. + + Any object with a matching ``update_skills`` method satisfies this — + ``ace.roles.SkillManager`` does. + """ + + def update_skills( + self, + *, + reflections: tuple[ReflectorOutput, ...], + skillbook: Any, + question_context: str, + progress: str, + **kwargs: Any, + ) -> SkillManagerOutput: ... diff --git a/ace/providers/__init__.py b/ace/providers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cfc3d769382bafa6a2d580ec40126678cd56c914 --- /dev/null +++ b/ace/providers/__init__.py @@ -0,0 +1,56 @@ +"""ACE LLM providers — PydanticAI model resolution and configuration. + +- ``resolve_model`` — resolve a model string to a PydanticAI model +- ``settings_from_config`` — build ModelSettings from ACEModelConfig +- ``ModelConfig`` / ``ACEModelConfig`` — configuration types +- ``validate_connection`` / ``search_models`` — model registry helpers +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +# Config is lightweight — always available eagerly. +from .config import ACEModelConfig, ModelConfig, load_config, save_config + +if TYPE_CHECKING: + from .pydantic_ai import resolve_model, settings_from_config + from .registry import ValidationResult, search_models, validate_connection + +_LAZY_IMPORTS: dict[str, tuple[str, str]] = { + # PydanticAI helpers + "resolve_model": ("ace.providers.pydantic_ai", "resolve_model"), + "settings_from_config": ("ace.providers.pydantic_ai", "settings_from_config"), + # Registry + "ValidationResult": ("ace.providers.registry", "ValidationResult"), + "validate_connection": ("ace.providers.registry", "validate_connection"), + "search_models": ("ace.providers.registry", "search_models"), +} + + +def __getattr__(name: str) -> object: + if name in _LAZY_IMPORTS: + module_path, attr = _LAZY_IMPORTS[name] + import importlib + + module = importlib.import_module(module_path) + value = getattr(module, attr) + globals()[name] = value + return value + raise AttributeError(f"module 'ace.providers' has no attribute {name!r}") + + +__all__ = [ + # Config + "ModelConfig", + "ACEModelConfig", + "load_config", + "save_config", + # PydanticAI helpers + "resolve_model", + "settings_from_config", + # Registry + "ValidationResult", + "validate_connection", + "search_models", +] diff --git a/ace/providers/config.py b/ace/providers/config.py new file mode 100644 index 0000000000000000000000000000000000000000..14f597eed8639b7f194e8fb82dd226ced9428e05 --- /dev/null +++ b/ace/providers/config.py @@ -0,0 +1,209 @@ +"""Model configuration — secrets-free config for ACE roles. + +``ModelConfig`` describes which model to use and how. No API keys — +those come from the environment (via ``.env`` or exported variables). + +``ACEModelConfig`` maps ACE roles (agent, reflector, skill_manager) +to individual ``ModelConfig`` instances, enabling per-role model selection. + +Config is persisted in ``ace.toml`` (committable, no secrets). +Keys are persisted in ``.env`` (gitignored). +""" + +from __future__ import annotations + +import logging +import tomllib +from dataclasses import dataclass, fields +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + +CONFIG_FILENAME = "ace.toml" +ENV_FILENAME = ".env" + + +# --------------------------------------------------------------------------- +# ModelConfig +# --------------------------------------------------------------------------- + + +@dataclass +class ModelConfig: + """Configuration for a single LLM role. No secrets.""" + + model: str + temperature: float = 0.0 + max_tokens: int = 2048 + extra_params: dict[str, Any] | None = None + + def to_dict(self) -> dict[str, Any]: + """Serialise to a dict, omitting None/default values.""" + d: dict[str, Any] = {"model": self.model} + if self.temperature != 0.0: + d["temperature"] = self.temperature + if self.max_tokens != 2048: + d["max_tokens"] = self.max_tokens + if self.extra_params: + d["extra_params"] = self.extra_params + return d + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> ModelConfig: + known = {f.name for f in fields(cls)} + return cls(**{k: v for k, v in d.items() if k in known}) + + +# --------------------------------------------------------------------------- +# ACEModelConfig +# --------------------------------------------------------------------------- + + +@dataclass +class ACEModelConfig: + """Model selection per ACE role. No secrets — keys come from env.""" + + default: ModelConfig + agent: ModelConfig | None = None + reflector: ModelConfig | None = None + skill_manager: ModelConfig | None = None + + def for_role(self, role: str) -> ModelConfig: + """Return the ModelConfig for *role*, falling back to default.""" + explicit = getattr(self, role, None) + if explicit is not None: + return explicit + return self.default + + # -- Serialisation -------------------------------------------------------- + + def to_dict(self) -> dict[str, Any]: + d: dict[str, Any] = {"default": self.default.to_dict()} + for role in ("agent", "reflector", "skill_manager"): + cfg = getattr(self, role) + if cfg is not None: + d[role] = cfg.to_dict() + return d + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> ACEModelConfig: + default = ModelConfig.from_dict(d["default"]) + agent = ModelConfig.from_dict(d["agent"]) if "agent" in d else None + reflector = ModelConfig.from_dict(d["reflector"]) if "reflector" in d else None + skill_manager = ( + ModelConfig.from_dict(d["skill_manager"]) if "skill_manager" in d else None + ) + return cls( + default=default, + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + ) + + +# --------------------------------------------------------------------------- +# TOML persistence +# --------------------------------------------------------------------------- + + +def _to_toml(config: ACEModelConfig) -> str: + """Serialise ACEModelConfig to TOML string.""" + lines: list[str] = [] + for section_name in ("default", "agent", "reflector", "skill_manager"): + cfg = getattr(config, section_name) + if cfg is None: + continue + lines.append(f"[{section_name}]") + d = cfg.to_dict() + for key, value in d.items(): + if key == "extra_params": + # Inline table for extra_params + inner = ", ".join(f"{k} = {_toml_value(v)}" for k, v in value.items()) + lines.append(f"extra_params = {{ {inner} }}") + else: + lines.append(f"{key} = {_toml_value(value)}") + lines.append("") + return "\n".join(lines) + + +def _toml_value(v: Any) -> str: + """Format a Python value as a TOML literal.""" + if isinstance(v, str): + return f'"{v}"' + if isinstance(v, bool): + return "true" if v else "false" + if isinstance(v, float): + return str(v) + if isinstance(v, int): + return str(v) + return repr(v) + + +def save_config(config: ACEModelConfig, directory: str | Path = ".") -> Path: + """Write ace.toml to *directory*.""" + path = Path(directory) / CONFIG_FILENAME + path.write_text(_to_toml(config), encoding="utf-8") + logger.info("Saved config to %s", path) + return path + + +def load_config(directory: str | Path = ".") -> ACEModelConfig: + """Load ace.toml from *directory*. + + Raises: + FileNotFoundError: If ace.toml does not exist. + """ + path = Path(directory) / CONFIG_FILENAME + if not path.exists(): + raise FileNotFoundError( + f"No {CONFIG_FILENAME} found in {Path(directory).resolve()}. " + "Run `ace setup` to create one." + ) + data = tomllib.loads(path.read_text(encoding="utf-8")) + return ACEModelConfig.from_dict(data) + + +def find_config(start: str | Path = ".") -> Path | None: + """Walk up from *start* looking for ace.toml. Return path or None.""" + current = Path(start).resolve() + for parent in [current, *current.parents]: + candidate = parent / CONFIG_FILENAME + if candidate.exists(): + return candidate + return None + + +# --------------------------------------------------------------------------- +# .env helpers +# --------------------------------------------------------------------------- + + +def load_dotenv() -> bool: + """Load .env if python-dotenv is installed. Return True if loaded.""" + try: + from dotenv import load_dotenv as _load + + return _load() + except ImportError: + return False + + +def save_env_var(key: str, value: str, directory: str | Path = ".") -> None: + """Append or update a key in .env file.""" + path = Path(directory) / ENV_FILENAME + lines: list[str] = [] + found = False + + if path.exists(): + for line in path.read_text(encoding="utf-8").splitlines(): + if line.startswith(f"{key}="): + lines.append(f'{key}="{value}"') + found = True + else: + lines.append(line) + + if not found: + lines.append(f'{key}="{value}"') + + path.write_text("\n".join(lines) + "\n", encoding="utf-8") diff --git a/ace/providers/pydantic_ai.py b/ace/providers/pydantic_ai.py new file mode 100644 index 0000000000000000000000000000000000000000..6213baafb4149de85bc239a2d690df2d125793c3 --- /dev/null +++ b/ace/providers/pydantic_ai.py @@ -0,0 +1,225 @@ +"""PydanticAI model resolution helpers. + +Converts ACE model identifiers (which follow LiteLLM conventions) into +PydanticAI model strings or provider objects. + +Resolution strategy (see ``resolve_model`` for details): + +1. Already has a PydanticAI provider prefix (``openai:gpt-4o``) -> pass through. +2. Starts with ``bedrock/`` and ``AWS_BEARER_TOKEN_BEDROCK`` is set -> + create a ``BedrockProvider(api_key=...)`` with the correct model. +3. Starts with a LiteLLM prefix that has a PydanticAI native equivalent + (``bedrock/model``) -> rewrite to ``bedrock:model``. +4. Everything else -> prepend ``litellm:`` for the LiteLLM proxy provider. + +Why not always use ``litellm:``? PydanticAI's LiteLLM provider is an +OpenAI-compatible HTTP client. Providers that aren't OpenAI-compatible +(Bedrock via SigV4, Anthropic's native API, etc.) need PydanticAI's +native provider instead. + +Provider SDK requirements +~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default ACE installs ``pydantic-ai-slim[litellm]`` — LiteLLM is the +only provider backend available out of the box. To use a PydanticAI +**native** provider (faster, no litellm proxy overhead, uses the +provider's own API key env vars directly), install the corresponding +``pydantic-ai-slim`` extra and its SDK: + +============ ==================================== ======================== +Provider Install API key env var +============ ==================================== ======================== +Anthropic ``pip install pydantic-ai-slim[anthropic]`` ``ANTHROPIC_API_KEY`` +OpenAI ``pip install pydantic-ai-slim[openai]`` ``OPENAI_API_KEY`` +Bedrock ``pip install pydantic-ai-slim[bedrock]`` AWS credentials / ``AWS_BEARER_TOKEN_BEDROCK`` +Google ``pip install pydantic-ai-slim[google]`` ``GEMINI_API_KEY`` +============ ==================================== ======================== + +Without the native extra, models that match a known prefix (e.g. +``openai/gpt-4o-mini``, ``anthropic/claude-...``) are rewritten to +the native PydanticAI prefix — but will fail at runtime if the SDK +package is missing. Models with **no** recognized prefix fall through +to ``litellm:<model>`` automatically. + +.. warning:: + + LiteLLM may override API keys if proxy-related env vars (e.g. + ``LITELLM_API_KEY``, ``SPH_LITELLM_KEY``) are set. When using + native providers, ensure these are unset or scoped to avoid key + conflicts. +""" + +from __future__ import annotations + +import os +from typing import Any, Union + +from pydantic_ai.settings import ModelSettings + +from .config import ModelConfig + +# PydanticAI provider names accepted as ``<provider>:<model>`` prefixes. +_PYDANTIC_AI_PROVIDERS: frozenset[str] = frozenset( + { + "anthropic", + "azure", + "bedrock", + "cerebras", + "cohere", + "deepseek", + "google", + "google-gla", + "google-vertex", + "grok", + "groq", + "litellm", + "mistral", + "openai", + "openai-chat", + "openai-responses", + "openrouter", + "vercel", + "vertexai", + } +) + +# LiteLLM uses ``provider/model`` while PydanticAI uses ``provider:model``. +# When the first path segment of a LiteLLM string matches a PydanticAI +# native provider, we rewrite ``/`` -> ``:`` so PydanticAI uses its own +# provider (with proper auth, API format, etc.) instead of the generic +# OpenAI-compatible LiteLLM proxy. +_LITELLM_PREFIX_TO_NATIVE: frozenset[str] = frozenset( + { + "anthropic", + "azure", + "azure_ai", + "bedrock", + "cohere", + "deepseek", + "groq", + "mistral", + "openrouter", + "vertex_ai", + } +) + + +def resolve_model(model: str) -> Any: + """Resolve an ACE/LiteLLM model string for PydanticAI. + + Returns either a string (for PydanticAI's auto-provider detection) + or a ``(provider, model_name)`` tuple when explicit provider + configuration is needed (e.g. Bedrock API key auth). + + Resolution paths: + + 1. **PydanticAI-native prefix** -- Already starts with a known + PydanticAI provider prefix (e.g. ``openai:gpt-4o``). Returned + unchanged. + + 2. **Bedrock with API key** -- Starts with ``bedrock/`` and + ``AWS_BEARER_TOKEN_BEDROCK`` is set. Returns a ``BedrockModel`` + configured with bearer-token auth. + + 3. **LiteLLM prefix with native equivalent** -- First path segment + matches a PydanticAI native provider (e.g. ``bedrock/model-id:0``). + Rewrites ``/`` to ``:`` for PydanticAI's native provider (SigV4). + + 4. **Fallback** -- Prepend ``litellm:`` for the LiteLLM proxy. + + Examples:: + + resolve_model("gpt-4o-mini") + # -> "litellm:gpt-4o-mini" + + resolve_model("bedrock/us.anthropic.claude-haiku-4-5-v1:0") + # -> BedrockModel (if AWS_BEARER_TOKEN_BEDROCK set) + # -> "bedrock:us.anthropic.claude-haiku-4-5-v1:0" (otherwise) + + resolve_model("openai:gpt-4o") + # -> "openai:gpt-4o" (unchanged) + + Args: + model: Model identifier -- LiteLLM convention + (``"gpt-4o-mini"``, ``"bedrock/model-id:0"``) or + PydanticAI convention (``"openai:gpt-4o"``). + + Returns: + PydanticAI model string or model object. + """ + # Path 1: already has a PydanticAI provider prefix + if ":" in model: + prefix = model.split(":", 1)[0] + if prefix in _PYDANTIC_AI_PROVIDERS: + return model + + # Path 2: Bedrock with API key (bearer token auth) + if "/" in model: + litellm_prefix = model.split("/", 1)[0] + if litellm_prefix == "bedrock": + bedrock_api_key = os.environ.get("AWS_BEARER_TOKEN_BEDROCK") + if bedrock_api_key: + return _create_bedrock_model(model, bedrock_api_key) + + # Path 3: LiteLLM prefix with a native PydanticAI equivalent + if "/" in model: + litellm_prefix = model.split("/", 1)[0] + if litellm_prefix in _LITELLM_PREFIX_TO_NATIVE: + rest = model.split("/", 1)[1] + pydantic_prefix = litellm_prefix + # Normalize LiteLLM aliases to PydanticAI names + if litellm_prefix == "vertex_ai": + pydantic_prefix = "google-vertex" + elif litellm_prefix == "azure_ai": + pydantic_prefix = "azure" + return f"{pydantic_prefix}:{rest}" + + # Path 4: no recognized prefix -> route through LiteLLM provider + return f"litellm:{model}" + + +def _create_bedrock_model(model: str, api_key: str) -> Any: + """Create a PydanticAI BedrockModel with API key (bearer token) auth. + + This is used when ``AWS_BEARER_TOKEN_BEDROCK`` is set, bypassing + boto3 SigV4 auth entirely. + + Args: + model: Full LiteLLM model string (e.g. + ``"bedrock/us.anthropic.claude-haiku-4-5-v1:0"``). + api_key: Bedrock API key (bearer token). + + Returns: + PydanticAI ``BedrockModel`` configured with bearer auth. + """ + from pydantic_ai.models.bedrock import BedrockConverseModel + from pydantic_ai.providers.bedrock import BedrockProvider + + # Extract model ID: "bedrock/us.anthropic.claude-..." -> "us.anthropic.claude-..." + model_id = model.split("/", 1)[1] + + # Infer region from inference profile prefix + region = "us-east-1" + if model_id.startswith("eu."): + region = "eu-west-1" + + provider = BedrockProvider(api_key=api_key, region_name=region) + return BedrockConverseModel(model_name=model_id, provider=provider) + + +def settings_from_config(config: ModelConfig) -> ModelSettings: + """Create ``ModelSettings`` from a ``ModelConfig``. + + Maps ACE configuration (temperature, max_tokens) to PydanticAI's + model settings. + + Args: + config: ACE model configuration. + + Returns: + PydanticAI ``ModelSettings`` with temperature and max_tokens. + """ + return ModelSettings( + temperature=config.temperature, + max_tokens=config.max_tokens, + ) diff --git a/ace/providers/registry.py b/ace/providers/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..e97e6e8562009892cb78bfa97d9f0c6f27641dea --- /dev/null +++ b/ace/providers/registry.py @@ -0,0 +1,328 @@ +"""Model registry — discovery, validation, and provider detection. + +Delegates entirely to LiteLLM for provider detection, environment +validation, and model discovery. No external API calls for discovery — +only ``validate_connection`` makes a real (tiny) LLM call. +""" + +from __future__ import annotations + +import logging +import os +import time +from dataclasses import dataclass +from typing import Any + +logger = logging.getLogger(__name__) + + +def _litellm(): + """Return the litellm module, importing it on first call.""" + global _litellm_mod + try: + return _litellm_mod # type: ignore[name-defined] + except NameError: + pass + try: + import litellm as _mod + + _litellm_mod = _mod + return _mod + except ImportError: + _litellm_mod = None + return None + + +# Example model strings per provider (for user guidance in the CLI) +PROVIDER_MODEL_EXAMPLES: dict[str, str] = { + "openai": "gpt-4o-mini", + "anthropic": "claude-sonnet-4-20250514", + "gemini": "gemini/gemini-2.0-flash", + "deepseek": "deepseek/deepseek-chat", + "groq": "groq/llama-3.1-70b", + "bedrock": "us.anthropic.claude-haiku-4-5-20251001-v1:0", + "ollama": "ollama/llama2", + "azure": "azure/gpt-4", + "openrouter": "openrouter/anthropic/claude-3.5-sonnet", +} + + +# --------------------------------------------------------------------------- +# Data types +# --------------------------------------------------------------------------- + + +@dataclass +class ValidationResult: + """Result of a model + key validation.""" + + success: bool + model: str = "" + provider: str = "" + latency_ms: int = 0 + error: str = "" + + +@dataclass +class ModelInfo: + """Metadata about a model from LiteLLM's registry.""" + + model: str + provider: str + max_input_tokens: int | None = None + max_output_tokens: int | None = None + input_cost_per_m: float | None = None # per million tokens + output_cost_per_m: float | None = None + key_found: bool = False + + +# --------------------------------------------------------------------------- +# Provider detection (delegated to LiteLLM) +# --------------------------------------------------------------------------- + + +def get_provider(model: str) -> str: + """Return the provider name for a model string, or 'unknown'.""" + ll = _litellm() + if ll is None: + raise ImportError("LiteLLM is required for model validation.") + + try: + _, provider, _, _ = ll.get_llm_provider(model) + except Exception as e: + logger.debug( + "Could not detect provider for %r (%s): %s", model, type(e).__name__, e + ) + provider = "unknown" + + return provider + + +def get_missing_keys(model: str) -> list[str]: + """Return env var names that LiteLLM says are missing for *model*.""" + ll = _litellm() + if ll is None: + return [] + + try: + result = ll.validate_environment(model=model) + return result.get("missing_keys", []) + except Exception as e: + logger.debug( + "Could not validate environment for %r (%s): %s", model, type(e).__name__, e + ) + return [] + + +def keys_are_set(model: str) -> bool: + """Check whether the required keys for *model* are in the environment.""" + return len(get_missing_keys(model)) == 0 + + +# --------------------------------------------------------------------------- +# Connection validation +# --------------------------------------------------------------------------- + + +def validate_connection(model: str, api_key: str | None = None) -> ValidationResult: + """Make a minimal LLM call to verify model + key work. + + Sends a 3-token request ("Say 'ok'") to confirm authentication, + model availability, and network connectivity. + + Args: + model: LiteLLM model string. + api_key: Explicit key, or None to use environment. + """ + ll = _litellm() + if ll is None: + return ValidationResult( + success=False, model=model, error="LiteLLM is not installed." + ) + + call_params: dict[str, Any] = { + "model": model, + "messages": [{"role": "user", "content": "Say 'ok'"}], + "max_tokens": 3, + "temperature": 0.0, + "timeout": 15, + } + if api_key: + call_params["api_key"] = api_key + + # Suppress LiteLLM's noisy debug output during validation + prev_verbose = getattr(ll, "suppress_debug_info", False) + ll.suppress_debug_info = True + + start = time.monotonic() + try: + response = ll.completion(**call_params) + elapsed_ms = int((time.monotonic() - start) * 1000) + + provider = "unknown" + if hasattr(response, "_hidden_params"): + provider = response._hidden_params.get("custom_llm_provider", "unknown") + + return ValidationResult( + success=True, + model=model, + provider=provider, + latency_ms=elapsed_ms, + ) + except ll.AuthenticationError: + return ValidationResult(success=False, model=model, error="Invalid API key.") + except ll.NotFoundError: + return ValidationResult( + success=False, + model=model, + error=f"Model '{model}' not found at the provider.", + ) + except ll.APIConnectionError: + return ValidationResult( + success=False, + model=model, + error="Could not connect to the provider.", + ) + except Exception as e: + return ValidationResult(success=False, model=model, error=str(e)) + finally: + ll.suppress_debug_info = prev_verbose + + +# --------------------------------------------------------------------------- +# Model search / discovery +# --------------------------------------------------------------------------- + + +PROVIDER_KEY_ENV: dict[str, str | list[str]] = { + "openai": "OPENAI_API_KEY", + "anthropic": "ANTHROPIC_API_KEY", + "azure": "AZURE_API_KEY", + "gemini": "GEMINI_API_KEY", + "deepseek": "DEEPSEEK_API_KEY", + "groq": "GROQ_API_KEY", + "bedrock": ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION_NAME"], + "bedrock_converse": [ + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_REGION_NAME", + ], + "vertex_ai": "GOOGLE_APPLICATION_CREDENTIALS", + "cohere": "COHERE_API_KEY", + "mistral": "MISTRAL_API_KEY", + "openrouter": "OPENROUTER_API_KEY", + "together_ai": "TOGETHERAI_API_KEY", + "fireworks_ai": "FIREWORKS_AI_API_KEY", + "replicate": "REPLICATE_API_KEY", + "huggingface": "HUGGINGFACE_API_KEY", + "perplexity": "PERPLEXITYAI_API_KEY", + "anyscale": "ANYSCALE_API_KEY", +} + + +_PROVIDER_ALT_KEYS: dict[str, list[str]] = { + "bedrock_converse": ["AWS_BEARER_TOKEN_BEDROCK"], +} + + +def _quick_key_check(provider: str) -> bool: + """Fast check: are the required env vars set for this provider?""" + env_var = PROVIDER_KEY_ENV.get(provider) + if env_var is not None: + if isinstance(env_var, list): + if all(bool(os.environ.get(v)) for v in env_var): + return True + elif bool(os.environ.get(env_var)): + return True + + # Alternative auth (e.g. bearer token for Bedrock) + alt_vars = _PROVIDER_ALT_KEYS.get(provider) + if alt_vars: + return any(bool(os.environ.get(v)) for v in alt_vars) + + return False + + +def search_models( + query: str = "", + provider: str | None = None, + chat_only: bool = True, + limit: int = 20, +) -> tuple[list[ModelInfo], int]: + """Search LiteLLM's model registry. + + Args: + query: Substring to match against model names. + provider: Filter to a specific provider. + chat_only: Only return chat/completion models. + limit: Maximum results. + + Returns: + (results, total_matches) — results capped at *limit*, + total_matches is the full count of matching models. + """ + ll = _litellm() + if ll is None: + return [], 0 + + results: list[ModelInfo] = [] + total = 0 + terms = query.lower().split() if query else [] + for model_id, info in ll.model_cost.items(): + if chat_only and info.get("mode") != "chat": + continue + if provider and info.get("litellm_provider") != provider: + continue + model_lower = model_id.lower() + if terms and not all(t in model_lower for t in terms): + continue + + total += 1 + + if len(results) >= limit: + continue # keep counting total + + prov = info.get("litellm_provider", "unknown") + input_cost = info.get("input_cost_per_token") + output_cost = info.get("output_cost_per_token") + + # Fast key check — just look for the provider's standard env var. + # We avoid litellm.validate_environment() here because it's slow + # and some providers (e.g. GitHub Copilot) trigger interactive auth. + key_found = _quick_key_check(prov) + + results.append( + ModelInfo( + model=model_id, + provider=prov, + max_input_tokens=info.get("max_input_tokens"), + max_output_tokens=info.get("max_output_tokens"), + input_cost_per_m=input_cost * 1_000_000 if input_cost else None, + output_cost_per_m=output_cost * 1_000_000 if output_cost else None, + key_found=key_found, + ) + ) + + return results, total + + +def suggest_models(typo: str, limit: int = 5) -> list[str]: + """Return model names similar to *typo* (simple substring matching).""" + ll = _litellm() + if ll is None: + return [] + + candidates: list[str] = [] + typo_lower = typo.lower() + + for model_id, info in ll.model_cost.items(): + if info.get("mode") != "chat": + continue + if model_id.lower().startswith(typo_lower): + candidates.append(model_id) + elif typo_lower in model_id.lower(): + candidates.append(model_id) + if len(candidates) >= limit: + break + + return candidates diff --git a/ace/runners/__init__.py b/ace/runners/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e988f7191fa98f7a3a39b248e3b4ac0e9f8240aa --- /dev/null +++ b/ace/runners/__init__.py @@ -0,0 +1,21 @@ +"""ACE runners — compose pipelines and manage the epoch loop.""" + +from .ace import ACE +from .base import ACERunner +from .browser_use import BrowserUse +from .claude_code import ClaudeCode +from .langchain import LangChain +from .litellm import ACELiteLLM +from .trace_analyser import TraceAnalyser + +__all__ = [ + # Runners + "ACE", + "ACERunner", + "BrowserUse", + "ClaudeCode", + "LangChain", + "TraceAnalyser", + # Convenience wrapper + "ACELiteLLM", +] diff --git a/ace/runners/ace.py b/ace/runners/ace.py new file mode 100644 index 0000000000000000000000000000000000000000..16a8450bfd0b820d7ff52d3dd68189e7510e6d2e --- /dev/null +++ b/ace/runners/ace.py @@ -0,0 +1,203 @@ +"""ACE — full adaptive pipeline runner.""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from pathlib import Path +from types import MappingProxyType +from typing import Any + +from pipeline import Pipeline +from pipeline.protocol import SampleResult, StepProtocol + +from ..core.context import ACEStepContext, SkillbookView +from ..core.environments import Sample, TaskEnvironment +from ..core.insight_source import TRACE_IDENTITY_METADATA_KEY, infer_trace_identity +from ..protocols import ( + AgentLike, + DeduplicationManagerLike, + ReflectorLike, + SkillManagerLike, +) +from ..core.skillbook import Skillbook +from ..steps import AgentStep, EvaluateStep, learning_tail +from .base import ACERunner + + +class ACE(ACERunner): + """Live adaptive pipeline: Agent -> Evaluate -> Reflect -> Tag -> Update -> Apply. + + The full ACE loop. An agent executes, the environment evaluates, the + reflector analyses, and the skill manager updates the skillbook. + + A single class handles both single-pass (``epochs=1``) and multi-epoch + batch training (``epochs > 1``). + + Use when you are building a new agent from scratch and want + closed-loop learning where the agent improves in real time. + """ + + @classmethod + def build_steps( + cls, + *, + agent: AgentLike, + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + environment: TaskEnvironment | None = None, + skillbook: Skillbook | None = None, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, + extra_steps: list[StepProtocol] | None = None, + ) -> list[StepProtocol]: + """Return the steps that ``from_roles()`` would compose. + + Use this to inspect, modify, or extend the pipeline before + constructing it yourself:: + + steps = ACE.build_steps(agent=agent, reflector=reflector, ...) + steps.insert(2, MyCustomStep()) + pipe = Pipeline(steps) + runner = ACERunner(pipeline=pipe, skillbook=skillbook) + + Args: + agent: Agent role for producing answers. + reflector: Reflector role for analysing execution. + skill_manager: SkillManager role for update operations. + environment: Optional task environment for evaluation feedback. + skillbook: Starting skillbook. Creates an empty one if ``None``. + dedup_manager: Optional deduplication manager. + dedup_interval: Samples between deduplication runs. + checkpoint_dir: Directory for checkpoint files. + checkpoint_interval: Samples between checkpoint saves. + extra_steps: Additional steps appended after the learning + tail (e.g. ``OpikStep``). + """ + skillbook = skillbook or Skillbook() + steps: list[StepProtocol[ACEStepContext]] = [ + AgentStep(agent, skillbook), + EvaluateStep(environment), + *learning_tail( + reflector, + skill_manager, + skillbook, + dedup_manager=dedup_manager, + dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, + checkpoint_interval=checkpoint_interval, + ), + ] + if extra_steps: + steps.extend(extra_steps) + return steps + + @classmethod + def from_roles( + cls, + *, + agent: AgentLike, + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + environment: TaskEnvironment | None = None, + skillbook: Skillbook | None = None, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, + extra_steps: list[StepProtocol] | None = None, + ) -> ACE: + """Construct from pre-built role instances. + + Args: + agent: Agent role for producing answers. + reflector: Reflector role for analysing execution. + skill_manager: SkillManager role for update operations. + environment: Optional task environment for evaluation feedback. + When provided, ``EvaluateStep`` generates feedback that + enriches the trace. When omitted, the trace still contains + the agent's output, question, context, and ground truth. + skillbook: Starting skillbook. Creates an empty one if ``None``. + dedup_manager: Optional deduplication manager. + dedup_interval: Samples between deduplication runs. + checkpoint_dir: Directory for checkpoint files. + checkpoint_interval: Samples between checkpoint saves. + extra_steps: Additional steps appended after the learning + tail (e.g. ``OpikStep``). + """ + skillbook = skillbook or Skillbook() + steps = cls.build_steps( + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + environment=environment, + skillbook=skillbook, + dedup_manager=dedup_manager, + dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, + checkpoint_interval=checkpoint_interval, + extra_steps=extra_steps, + ) + return cls(pipeline=Pipeline(steps), skillbook=skillbook) + + def run( + self, + samples: Sequence[Sample] | Iterable[Sample], + epochs: int = 1, + *, + wait: bool = True, + ) -> list[SampleResult]: + """Run the adaptive pipeline over samples. + + Args: + samples: Input samples. Must be a ``Sequence`` for + ``epochs > 1``. ``Iterable`` is accepted when + ``epochs=1`` (consumed once). + epochs: Number of passes over the samples. + wait: If ``True``, block until background learning completes. + + Returns: + List of ``SampleResult``, one per sample per epoch. + + Raises: + ValueError: If ``epochs > 1`` and *samples* is not a + ``Sequence``. + """ + return self._run(samples, epochs=epochs, wait=wait) + + def _build_context( # type: ignore[override] + self, + sample: Sample, + *, + epoch: int, + total_epochs: int, + index: int, + total: int | None, + global_sample_index: int, + **_: Any, + ) -> ACEStepContext: + """Map a ``Sample`` to an ``ACEStepContext`` for the full pipeline. + + Sets ``sample`` and ``skillbook`` on the context. The environment + (if any) is injected into ``EvaluateStep`` at construction time — + it does not appear on the context. + """ + return ACEStepContext( + sample=sample, + metadata=MappingProxyType( + { + TRACE_IDENTITY_METADATA_KEY: infer_trace_identity( + sample=sample, + metadata=sample.metadata, + default_source_system="sample", + ).to_dict() + } + ), + skillbook=SkillbookView(self.skillbook), + epoch=epoch, + total_epochs=total_epochs, + step_index=index, + total_steps=total, + global_sample_index=global_sample_index, + ) diff --git a/ace/runners/base.py b/ace/runners/base.py new file mode 100644 index 0000000000000000000000000000000000000000..da6a5b8532371d9a4fb6a48000e02186dcd01273 --- /dev/null +++ b/ace/runners/base.py @@ -0,0 +1,167 @@ +"""ACERunner — shared runner infrastructure for all ACE runners.""" + +from __future__ import annotations + +import logging +from collections.abc import Iterable, Sequence +from typing import Any + +from pipeline import Pipeline +from pipeline.errors import CancellationToken +from pipeline.protocol import SampleResult + +from ..core.context import ACEStepContext, SkillbookView +from ..core.skillbook import Skillbook + +logger = logging.getLogger(__name__) + + +class ACERunner: + """Shared runner infrastructure for all ACE runners. + + Composes a ``Pipeline`` (does not extend it). Manages the epoch loop + and delegates per-sample iteration, error isolation, foreground/background + split, and concurrent workers to ``Pipeline.run()``. + + Subclasses override two methods: + + - ``run()`` — public API with a subclass-specific signature. + - ``_build_context()`` — maps a single input item to ``ACEStepContext``. + + You can also construct an ``ACERunner`` directly with a hand-composed + pipeline:: + + from ace import Pipeline, ACERunner, AgentStep, learning_tail + + pipe = Pipeline([AgentStep(agent, sb), *learning_tail(reflector, sm, sb)]) + runner = ACERunner(pipeline=pipe, skillbook=sb) + + Attributes: + pipeline: The composed ``Pipeline`` instance. Accessible for + inspection after construction. + skillbook: The ``Skillbook`` this runner operates on. + """ + + def __init__( + self, + pipeline: Pipeline, + skillbook: Skillbook, + ) -> None: + self.pipeline = pipeline + self.skillbook = skillbook + + # ------------------------------------------------------------------ + # Lifecycle helpers + # ------------------------------------------------------------------ + + def save(self, path: str) -> None: + """Save the current skillbook to disk.""" + self.skillbook.save_to_file(path) + + def load(self, path: str) -> None: + """Load a skillbook from disk, replacing the current one.""" + self.skillbook = Skillbook.load_from_file(path) + + def wait_for_background(self, timeout: float | None = None) -> None: + """Block until all background learning tasks complete. + + Delegates to ``Pipeline.wait_for_background()``. Call after + ``run(wait=False)`` before saving the skillbook or reading final + results. + """ + self.pipeline.wait_for_background(timeout) + + @property + def learning_stats(self) -> dict[str, int]: + """Return background learning progress. + + Delegates to ``Pipeline.background_stats()``. + """ + return self.pipeline.background_stats() + + # ------------------------------------------------------------------ + # Generic epoch loop (called by subclasses) + # ------------------------------------------------------------------ + + def _run( + self, + items: Sequence[Any] | Iterable[Any], + *, + epochs: int, + wait: bool = True, + cancel_token: CancellationToken | None = None, + **kwargs: Any, + ) -> list[SampleResult]: + """Generic run loop handling epochs and Iterable validation. + + Returns when ``wait=True`` (default). Returns after foreground + steps when ``wait=False`` — background learning continues. + + Args: + cancel_token: Optional cancellation signal. Forwarded to + ``Pipeline.run()`` — checked between steps and inside + LLM calls (via contextvar). + + Raises ``ValueError`` if ``epochs > 1`` and *items* is not a + ``Sequence``. + """ + if epochs > 1 and not isinstance(items, Sequence): + raise ValueError( + "Multi-epoch requires a Sequence, not a consumed Iterable." + ) + + results: list[SampleResult] = [] + n: int | None = len(items) if isinstance(items, Sequence) else None + + for epoch in range(1, epochs + 1): + if cancel_token is not None and cancel_token.is_cancelled: + break + logger.info( + "Epoch %d/%d: processing %s samples", + epoch, + epochs, + n if n is not None else "unknown", + ) + contexts: list[ACEStepContext] = [ + self._build_context( + item, + epoch=epoch, + total_epochs=epochs, + index=idx, + total=n, + global_sample_index=( + (epoch - 1) * n + idx if n is not None else idx + ), + **kwargs, + ) + for idx, item in enumerate(items, start=1) + ] + epoch_results = self.pipeline.run(contexts, cancel_token=cancel_token) + results.extend(epoch_results) + + if wait: + self.pipeline.wait_for_background() + + return results + + # ------------------------------------------------------------------ + # Subclass interface + # ------------------------------------------------------------------ + + def _build_context( + self, + item: Any, + *, + epoch: int, + total_epochs: int, + index: int, + total: int | None, + global_sample_index: int, + **kwargs: Any, + ) -> ACEStepContext: + """Map a single input item to an ``ACEStepContext``. + + Must be overridden by subclasses. Stateless — depends only on + the item and the provided counters. + """ + raise NotImplementedError diff --git a/ace/runners/browser_use.py b/ace/runners/browser_use.py new file mode 100644 index 0000000000000000000000000000000000000000..478e26ffb5a0de7432a12ac2d965eff39c83360b --- /dev/null +++ b/ace/runners/browser_use.py @@ -0,0 +1,260 @@ +"""BrowserUse — browser-use agent with ACE learning.""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from pathlib import Path +from typing import Any, Optional + +from pydantic_ai.settings import ModelSettings + +from pipeline import Pipeline +from pipeline.protocol import SampleResult, StepProtocol + +from ..core.context import ACEStepContext, SkillbookView +from ..core.skillbook import Skillbook +from ..integrations import wrap_skillbook_context +from ..integrations.browser_use import BrowserExecuteStep, BrowserToTrace +from ..protocols import ( + DeduplicationConfig, + DeduplicationManagerLike, + ReflectorLike, + SkillManagerLike, +) +from ..steps import learning_tail +from .base import ACERunner + + +class BrowserUse(ACERunner): + """Browser-use agent with ACE learning pipeline. + + INJECT skillbook -> EXECUTE browser-use -> LEARN (Reflect -> Tag -> Update -> Apply). + + Two construction paths: + + 1. ``BrowserUse.from_roles(browser_llm, reflector, skill_manager, ...)`` + — pre-built roles. + 2. ``BrowserUse.from_model(browser_llm, ace_model="gpt-4o-mini", ...)`` + — builds ACE roles from a model string. + + Example:: + + runner = BrowserUse.from_model( + browser_llm=ChatOpenAI(model="gpt-4o"), + ace_model="gpt-4o-mini", + ) + results = runner.run(["Find top HN post", "Check weather in NYC"]) + runner.save("browser_expert.json") + """ + + @classmethod + def build_steps( + cls, + *, + browser_llm: Any, + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + skillbook: Skillbook | None = None, + skillbook_path: Optional[str] = None, + browser: Any = None, + agent_kwargs: dict[str, Any] | None = None, + dedup_config: Optional[DeduplicationConfig] = None, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, + ) -> list[StepProtocol]: + """Return the steps that ``from_roles()`` would compose. + + Use this to inspect, modify, or extend the pipeline before + constructing it yourself:: + + steps = BrowserUse.build_steps(browser_llm=llm, reflector=r, ...) + steps.insert(2, MyCustomStep()) + pipe = Pipeline(steps) + runner = ACERunner(pipeline=pipe, skillbook=skillbook) + + Args: + browser_llm: LLM for browser-use execution. + reflector: Reflector role for analysing execution traces. + skill_manager: SkillManager role for update operations. + skillbook: Starting skillbook. Creates an empty one if ``None``. + skillbook_path: Path to load skillbook from. + browser: Optional browser-use Browser instance. + agent_kwargs: Extra kwargs forwarded to browser-use Agent. + dedup_config: Deduplication configuration. + dedup_manager: Optional pre-built deduplication manager. + dedup_interval: Samples between deduplication runs. + checkpoint_dir: Directory for checkpoint files. + checkpoint_interval: Samples between checkpoint saves. + """ + # Resolve skillbook + if skillbook_path: + skillbook = Skillbook.load_from_file(skillbook_path) + elif skillbook is None: + skillbook = Skillbook() + + # Resolve dedup manager + dm = dedup_manager + if dm is None and dedup_config is not None: + from ..deduplication import DeduplicationManager + + dm = DeduplicationManager(dedup_config) + + steps: list[StepProtocol[ACEStepContext]] = [ + BrowserExecuteStep(browser_llm, browser=browser, **(agent_kwargs or {})), # type: ignore[list-item] # async step + BrowserToTrace(), + *learning_tail( + reflector, + skill_manager, + skillbook, + dedup_manager=dm, + dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, + checkpoint_interval=checkpoint_interval, + ), + ] + return steps + + @classmethod + def from_roles( + cls, + *, + browser_llm: Any, + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + skillbook: Skillbook | None = None, + skillbook_path: Optional[str] = None, + browser: Any = None, + agent_kwargs: dict[str, Any] | None = None, + dedup_config: Optional[DeduplicationConfig] = None, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, + ) -> BrowserUse: + """Construct from pre-built role instances. + + Args: + browser_llm: LLM for browser-use execution. + reflector: Reflector role for analysing execution traces. + skill_manager: SkillManager role for update operations. + skillbook: Starting skillbook. Creates an empty one if ``None``. + skillbook_path: Path to load skillbook from. + browser: Optional browser-use Browser instance. + agent_kwargs: Extra kwargs forwarded to browser-use Agent. + dedup_config: Deduplication configuration. + dedup_manager: Optional pre-built deduplication manager. + dedup_interval: Samples between deduplication runs. + checkpoint_dir: Directory for checkpoint files. + checkpoint_interval: Samples between checkpoint saves. + """ + # Resolve skillbook (must match build_steps resolution) + if skillbook_path: + skillbook = Skillbook.load_from_file(skillbook_path) + elif skillbook is None: + skillbook = Skillbook() + + steps = cls.build_steps( + browser_llm=browser_llm, + reflector=reflector, + skill_manager=skill_manager, + skillbook=skillbook, + browser=browser, + agent_kwargs=agent_kwargs, + dedup_config=dedup_config, + dedup_manager=dedup_manager, + dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, + checkpoint_interval=checkpoint_interval, + ) + return cls(pipeline=Pipeline(steps), skillbook=skillbook) + + @classmethod + def from_model( + cls, + browser_llm: Any, + *, + ace_model: str = "gpt-4o-mini", + ace_max_tokens: int = 2048, + ace_temperature: float = 0.0, + **kwargs: Any, + ) -> BrowserUse: + """Build ACE roles from a model string. + + Args: + browser_llm: LLM for browser-use execution. + ace_model: Model identifier for ACE roles. + ace_max_tokens: Max tokens for ACE LLM responses. + ace_temperature: Sampling temperature for ACE roles. + **kwargs: Forwarded to :meth:`from_roles`. + """ + from ..implementations import Reflector, SkillManager + + model_settings = ModelSettings( + temperature=ace_temperature, + max_tokens=ace_max_tokens, + ) + + return cls.from_roles( + browser_llm=browser_llm, + reflector=Reflector(ace_model, model_settings=model_settings), + skill_manager=SkillManager(ace_model, model_settings=model_settings), + **kwargs, + ) + + def run( + self, + tasks: Sequence[str] | Iterable[str] | str, + epochs: int = 1, + *, + wait: bool = True, + ) -> list[SampleResult]: + """Run browser tasks with learning. + + Args: + tasks: Single task string or list of task strings. + Must be a ``Sequence`` for ``epochs > 1``. + epochs: Number of passes over all tasks. + wait: If ``True``, block until background learning completes. + """ + if isinstance(tasks, str): + tasks = [tasks] + return self._run(tasks, epochs=epochs, wait=wait) + + def _build_context( # type: ignore[override] + self, + task: str, + *, + epoch: int, + total_epochs: int, + index: int, + total: int | None, + global_sample_index: int, + **_: Any, + ) -> ACEStepContext: + """Place a raw task string on ``ctx.sample``.""" + return ACEStepContext( + sample=task, + skillbook=SkillbookView(self.skillbook), + epoch=epoch, + total_epochs=total_epochs, + step_index=index, + total_steps=total, + global_sample_index=global_sample_index, + ) + + # ------------------------------------------------------------------ + # Convenience lifecycle methods + # ------------------------------------------------------------------ + + def get_strategies(self) -> str: + """Return formatted skillbook strategies for display.""" + if not self.skillbook.skills(): + return "" + return wrap_skillbook_context(self.skillbook) + + # Backward-compat aliases + save_skillbook = ACERunner.save + load_skillbook = ACERunner.load + wait_for_learning = ACERunner.wait_for_background diff --git a/ace/runners/claude_code.py b/ace/runners/claude_code.py new file mode 100644 index 0000000000000000000000000000000000000000..6122939e21e1baf7c360fb389d52c31258768405 --- /dev/null +++ b/ace/runners/claude_code.py @@ -0,0 +1,270 @@ +"""ClaudeCode — Claude Code CLI runner with ACE learning.""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from pathlib import Path +from typing import Any, Optional + +from pydantic_ai.settings import ModelSettings + +from pipeline import Pipeline +from pipeline.protocol import SampleResult, StepProtocol + +from ..core.context import ACEStepContext, SkillbookView +from ..core.skillbook import Skillbook +from ..integrations import wrap_skillbook_context +from ..integrations.claude_code import ClaudeCodeExecuteStep, ClaudeCodeToTrace +from ..protocols import ( + DeduplicationConfig, + DeduplicationManagerLike, + ReflectorLike, + SkillManagerLike, +) +from ..steps import learning_tail +from .base import ACERunner + + +class ClaudeCode(ACERunner): + """Claude Code CLI with ACE learning pipeline. + + INJECT skillbook → EXECUTE Claude Code → LEARN (Reflect → Tag → Update → Apply). + + Two construction paths: + + 1. ``ClaudeCode.from_roles(reflector, skill_manager, working_dir=..., ...)`` + — pre-built roles. + 2. ``ClaudeCode.from_model(working_dir=..., ace_model="gpt-4o-mini", ...)`` + — builds ACE roles from a model string. + + Example:: + + runner = ClaudeCode.from_model(working_dir="./my_project") + results = runner.run([ + "Add unit tests for utils.py", + "Refactor the auth module", + ]) + runner.save("code_expert.json") + """ + + @classmethod + def build_steps( + cls, + *, + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + skillbook: Skillbook | None = None, + skillbook_path: Optional[str] = None, + working_dir: Optional[str] = None, + timeout: int = 600, + model: Optional[str] = None, + allowed_tools: Optional[list[str]] = None, + dedup_config: Optional[DeduplicationConfig] = None, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, + ) -> list[StepProtocol]: + """Return the steps that ``from_roles()`` would compose. + + Use this to inspect, modify, or extend the pipeline before + constructing it yourself:: + + steps = ClaudeCode.build_steps(reflector=r, skill_manager=sm, ...) + steps.insert(2, MyCustomStep()) + pipe = Pipeline(steps) + runner = ACERunner(pipeline=pipe, skillbook=skillbook) + + Args: + reflector: Reflector role for analysing execution traces. + skill_manager: SkillManager role for update operations. + skillbook: Starting skillbook. Creates an empty one if ``None``. + skillbook_path: Path to load skillbook from. + working_dir: Directory where Claude Code executes. + timeout: Execution timeout in seconds. + model: Optional Claude model override. + allowed_tools: Optional list of allowed tools. + dedup_config: Deduplication configuration. + dedup_manager: Optional pre-built deduplication manager. + dedup_interval: Samples between deduplication runs. + checkpoint_dir: Directory for checkpoint files. + checkpoint_interval: Samples between checkpoint saves. + """ + # Resolve skillbook + if skillbook_path: + skillbook = Skillbook.load_from_file(skillbook_path) + elif skillbook is None: + skillbook = Skillbook() + + # Resolve dedup manager + dm = dedup_manager + if dm is None and dedup_config is not None: + from ..deduplication import DeduplicationManager + + dm = DeduplicationManager(dedup_config) + + steps: list[StepProtocol[ACEStepContext]] = [ + ClaudeCodeExecuteStep( + working_dir=working_dir, + timeout=timeout, + model=model, + allowed_tools=allowed_tools, + ), + ClaudeCodeToTrace(), + *learning_tail( + reflector, + skill_manager, + skillbook, + dedup_manager=dm, + dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, + checkpoint_interval=checkpoint_interval, + ), + ] + return steps + + @classmethod + def from_roles( + cls, + *, + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + skillbook: Skillbook | None = None, + skillbook_path: Optional[str] = None, + working_dir: Optional[str] = None, + timeout: int = 600, + model: Optional[str] = None, + allowed_tools: Optional[list[str]] = None, + dedup_config: Optional[DeduplicationConfig] = None, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, + ) -> ClaudeCode: + """Construct from pre-built role instances. + + Args: + reflector: Reflector role for analysing execution traces. + skill_manager: SkillManager role for update operations. + skillbook: Starting skillbook. Creates an empty one if ``None``. + skillbook_path: Path to load skillbook from. + working_dir: Directory where Claude Code executes. + timeout: Execution timeout in seconds. + model: Optional Claude model override. + allowed_tools: Optional list of allowed tools. + dedup_config: Deduplication configuration. + dedup_manager: Optional pre-built deduplication manager. + dedup_interval: Samples between deduplication runs. + checkpoint_dir: Directory for checkpoint files. + checkpoint_interval: Samples between checkpoint saves. + """ + # Resolve skillbook (must match build_steps resolution) + if skillbook_path: + skillbook = Skillbook.load_from_file(skillbook_path) + elif skillbook is None: + skillbook = Skillbook() + + steps = cls.build_steps( + reflector=reflector, + skill_manager=skill_manager, + skillbook=skillbook, + working_dir=working_dir, + timeout=timeout, + model=model, + allowed_tools=allowed_tools, + dedup_config=dedup_config, + dedup_manager=dedup_manager, + dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, + checkpoint_interval=checkpoint_interval, + ) + return cls(pipeline=Pipeline(steps), skillbook=skillbook) + + @classmethod + def from_model( + cls, + *, + working_dir: Optional[str] = None, + ace_model: str = "gpt-4o-mini", + ace_max_tokens: int = 2048, + ace_temperature: float = 0.0, + **kwargs: Any, + ) -> ClaudeCode: + """Build ACE roles from a model string. + + Args: + working_dir: Directory where Claude Code executes. + ace_model: Model identifier for ACE roles. + ace_max_tokens: Max tokens for ACE LLM responses. + ace_temperature: Sampling temperature for ACE roles. + **kwargs: Forwarded to :meth:`from_roles`. + """ + from ..implementations import Reflector, SkillManager + + model_settings = ModelSettings( + temperature=ace_temperature, + max_tokens=ace_max_tokens, + ) + + return cls.from_roles( + reflector=Reflector(ace_model, model_settings=model_settings), + skill_manager=SkillManager(ace_model, model_settings=model_settings), + working_dir=working_dir, + **kwargs, + ) + + def run( + self, + tasks: Sequence[str] | Iterable[str] | str, + epochs: int = 1, + *, + wait: bool = True, + ) -> list[SampleResult]: + """Run coding tasks with learning. + + Args: + tasks: Single task string or list of task strings. + Must be a ``Sequence`` for ``epochs > 1``. + epochs: Number of passes over all tasks. + wait: If ``True``, block until background learning completes. + """ + if isinstance(tasks, str): + tasks = [tasks] + return self._run(tasks, epochs=epochs, wait=wait) + + def _build_context( # type: ignore[override] + self, + task: str, + *, + epoch: int, + total_epochs: int, + index: int, + total: int | None, + global_sample_index: int, + **_: Any, + ) -> ACEStepContext: + """Place a raw task string on ``ctx.sample``.""" + return ACEStepContext( + sample=task, + skillbook=SkillbookView(self.skillbook), + epoch=epoch, + total_epochs=total_epochs, + step_index=index, + total_steps=total, + global_sample_index=global_sample_index, + ) + + # ------------------------------------------------------------------ + # Convenience lifecycle methods + # ------------------------------------------------------------------ + + def get_strategies(self) -> str: + """Return formatted skillbook strategies for display.""" + if not self.skillbook.skills(): + return "" + return wrap_skillbook_context(self.skillbook) + + # Backward-compat aliases + save_skillbook = ACERunner.save + load_skillbook = ACERunner.load + wait_for_learning = ACERunner.wait_for_background diff --git a/ace/runners/langchain.py b/ace/runners/langchain.py new file mode 100644 index 0000000000000000000000000000000000000000..e25299cddc55d467bd1211486a3bae397f91f0ec --- /dev/null +++ b/ace/runners/langchain.py @@ -0,0 +1,264 @@ +"""LangChain — LangChain Runnable with ACE learning.""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from pathlib import Path +from typing import Any, Callable, Optional + +from pydantic_ai.settings import ModelSettings + +from pipeline import Pipeline +from pipeline.protocol import SampleResult, StepProtocol + +from ..core.context import ACEStepContext, SkillbookView +from ..core.skillbook import Skillbook +from ..integrations import wrap_skillbook_context +from ..integrations.langchain import LangChainExecuteStep, LangChainToTrace +from ..protocols import ( + DeduplicationConfig, + DeduplicationManagerLike, + ReflectorLike, + SkillManagerLike, +) +from ..steps import learning_tail +from .base import ACERunner + + +class LangChain(ACERunner): + """LangChain Runnable with ACE learning pipeline. + + INJECT skillbook -> EXECUTE runnable -> LEARN (Reflect -> Tag -> Update -> Apply). + + Handles simple chains, AgentExecutor, and LangGraph graphs automatically. + + Two construction paths: + + 1. ``LangChain.from_roles(runnable, reflector, skill_manager, ...)`` + — pre-built roles. + 2. ``LangChain.from_model(runnable, ace_model="gpt-4o-mini", ...)`` + — builds ACE roles from a model string. + + Example:: + + runner = LangChain.from_model(my_chain, ace_model="gpt-4o-mini") + results = runner.run([ + {"input": "What is ACE?"}, + {"input": "Explain skillbooks"}, + ]) + runner.save("chain_expert.json") + """ + + @classmethod + def build_steps( + cls, + *, + runnable: Any, + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + skillbook: Skillbook | None = None, + skillbook_path: Optional[str] = None, + output_parser: Optional[Callable[[Any], str]] = None, + dedup_config: Optional[DeduplicationConfig] = None, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, + ) -> list[StepProtocol]: + """Return the steps that ``from_roles()`` would compose. + + Use this to inspect, modify, or extend the pipeline before + constructing it yourself:: + + steps = LangChain.build_steps(runnable=chain, reflector=r, ...) + steps.insert(2, MyCustomStep()) + pipe = Pipeline(steps) + runner = ACERunner(pipeline=pipe, skillbook=skillbook) + + Args: + runnable: Any LangChain Runnable (chain, AgentExecutor, LangGraph). + reflector: Reflector role for analysing execution traces. + skill_manager: SkillManager role for update operations. + skillbook: Starting skillbook. Creates an empty one if ``None``. + skillbook_path: Path to load skillbook from. + output_parser: Custom function to extract a string from runnable output. + dedup_config: Deduplication configuration. + dedup_manager: Optional pre-built deduplication manager. + dedup_interval: Samples between deduplication runs. + checkpoint_dir: Directory for checkpoint files. + checkpoint_interval: Samples between checkpoint saves. + """ + # Resolve skillbook + if skillbook_path: + skillbook = Skillbook.load_from_file(skillbook_path) + elif skillbook is None: + skillbook = Skillbook() + + # Resolve dedup manager + dm = dedup_manager + if dm is None and dedup_config is not None: + from ..deduplication import DeduplicationManager + + dm = DeduplicationManager(dedup_config) + + steps: list[StepProtocol[ACEStepContext]] = [ + LangChainExecuteStep(runnable, output_parser=output_parser), + LangChainToTrace(), + *learning_tail( + reflector, + skill_manager, + skillbook, + dedup_manager=dm, + dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, + checkpoint_interval=checkpoint_interval, + ), + ] + return steps + + @classmethod + def from_roles( + cls, + *, + runnable: Any, + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + skillbook: Skillbook | None = None, + skillbook_path: Optional[str] = None, + output_parser: Optional[Callable[[Any], str]] = None, + dedup_config: Optional[DeduplicationConfig] = None, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, + ) -> LangChain: + """Construct from a LangChain Runnable and pre-built role instances. + + Args: + runnable: Any LangChain Runnable (chain, AgentExecutor, LangGraph). + reflector: Reflector role for analysing execution traces. + skill_manager: SkillManager role for update operations. + skillbook: Starting skillbook. Creates an empty one if ``None``. + skillbook_path: Path to load skillbook from. + output_parser: Custom function to extract a string from runnable output. + dedup_config: Deduplication configuration. + dedup_manager: Optional pre-built deduplication manager. + dedup_interval: Samples between deduplication runs. + checkpoint_dir: Directory for checkpoint files. + checkpoint_interval: Samples between checkpoint saves. + """ + # Resolve skillbook (must match build_steps resolution) + if skillbook_path: + skillbook = Skillbook.load_from_file(skillbook_path) + elif skillbook is None: + skillbook = Skillbook() + + steps = cls.build_steps( + runnable=runnable, + reflector=reflector, + skill_manager=skill_manager, + skillbook=skillbook, + output_parser=output_parser, + dedup_config=dedup_config, + dedup_manager=dedup_manager, + dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, + checkpoint_interval=checkpoint_interval, + ) + return cls(pipeline=Pipeline(steps), skillbook=skillbook) + + @classmethod + def from_model( + cls, + runnable: Any, + *, + ace_model: str = "gpt-4o-mini", + ace_max_tokens: int = 2048, + ace_temperature: float = 0.0, + **kwargs: Any, + ) -> LangChain: + """Build ACE roles from a model string. + + Args: + runnable: Any LangChain Runnable (chain, AgentExecutor, LangGraph). + ace_model: Model identifier for ACE roles. + ace_max_tokens: Max tokens for ACE LLM responses. + ace_temperature: Sampling temperature for ACE roles. + **kwargs: Forwarded to :meth:`from_roles`. + """ + from ..implementations import Reflector, SkillManager + + model_settings = ModelSettings( + temperature=ace_temperature, + max_tokens=ace_max_tokens, + ) + + return cls.from_roles( + runnable=runnable, + reflector=Reflector(ace_model, model_settings=model_settings), + skill_manager=SkillManager(ace_model, model_settings=model_settings), + **kwargs, + ) + + def run( + self, + inputs: Sequence[Any] | Iterable[Any], + epochs: int = 1, + *, + wait: bool = True, + ) -> list[SampleResult]: + """Run inputs through the chain with learning. + + Args: + inputs: Raw inputs (strings, dicts, message lists). + Must be a ``Sequence`` for ``epochs > 1``. + epochs: Number of passes over all inputs. + wait: If ``True``, block until background learning completes. + """ + return self._run(inputs, epochs=epochs, wait=wait) + + def invoke(self, input: Any, **kwargs: Any) -> list[SampleResult]: + """Single-input convenience — wraps in a list and delegates to :meth:`run`. + + Args: + input: A single chain input. + **kwargs: Forwarded to :meth:`run`. + """ + return self.run([input], **kwargs) + + def _build_context( # type: ignore[override] + self, + raw_input: Any, + *, + epoch: int, + total_epochs: int, + index: int, + total: int | None, + global_sample_index: int, + **_: Any, + ) -> ACEStepContext: + """Place a raw input on ``ctx.sample``.""" + return ACEStepContext( + sample=raw_input, + skillbook=SkillbookView(self.skillbook), + epoch=epoch, + total_epochs=total_epochs, + step_index=index, + total_steps=total, + global_sample_index=global_sample_index, + ) + + # ------------------------------------------------------------------ + # Convenience lifecycle methods + # ------------------------------------------------------------------ + + def get_strategies(self) -> str: + """Return formatted skillbook strategies for display.""" + if not self.skillbook.skills(): + return "" + return wrap_skillbook_context(self.skillbook) + + # Backward-compat aliases + save_skillbook = ACERunner.save + load_skillbook = ACERunner.load + wait_for_learning = ACERunner.wait_for_background diff --git a/ace/runners/litellm.py b/ace/runners/litellm.py new file mode 100644 index 0000000000000000000000000000000000000000..e224f5c836b0c2992d81630281874d075d1a27cf --- /dev/null +++ b/ace/runners/litellm.py @@ -0,0 +1,534 @@ +"""ACELiteLLM — batteries-included conversational agent with ACE learning.""" + +from __future__ import annotations + +import logging +from collections.abc import Sequence +from pathlib import Path +from typing import Any, Optional, Union + +from pydantic_ai.settings import ModelSettings + +from pipeline.protocol import SampleResult + +from ..core.context import ACEStepContext, SkillbookView +from ..core.environments import Sample, TaskEnvironment +from ..core.outputs import AgentOutput +from ..core.skillbook import Skillbook +from ..implementations import Agent, Reflector, SkillManager +from ..integrations import wrap_skillbook_context +from ..protocols import ( + AgentLike, + DeduplicationConfig, + DeduplicationManagerLike, + ReflectorLike, + SkillManagerLike, +) +from ..providers.pydantic_ai import resolve_model, settings_from_config +from ..steps import learning_tail +from .ace import ACE +from .trace_analyser import TraceAnalyser + +logger = logging.getLogger(__name__) + + +class ACELiteLLM: + """PydanticAI-powered conversational agent with ACE learning. + + Bundles Agent, Reflector, SkillManager, and Skillbook into a simple + interface. Delegates to :class:`ACE` for batch learning and + :class:`TraceAnalyser` for trace-based learning. + + Two construction paths: + + 1. ``ACELiteLLM("gpt-4o-mini", ...)`` — builds PydanticAI-backed + roles from a model string. + 2. ``ACELiteLLM.from_model("gpt-4o-mini", ...)`` — explicit factory + with full parameter control. + + Example:: + + ace = ACELiteLLM.from_model("gpt-4o-mini") + answer = ace.ask("What is 2+2?") + ace.learn(samples, environment=SimpleEnvironment(), epochs=3) + ace.save("learned.json") + """ + + def __init__( + self, + model: str, + *, + skillbook: Skillbook | None = None, + skillbook_path: str | None = None, + environment: TaskEnvironment | None = None, + agent: AgentLike | None = None, + reflector: ReflectorLike | None = None, + skill_manager: SkillManagerLike | None = None, + model_settings: ModelSettings | None = None, + dedup_config: DeduplicationConfig | None = None, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, + is_learning: bool = True, + logfire: bool = False, + ) -> None: + # Resolve skillbook + if skillbook_path: + self._skillbook = Skillbook.load_from_file(skillbook_path) + elif skillbook is not None: + self._skillbook = skillbook + else: + self._skillbook = Skillbook() + + # Build roles (use provided or create PydanticAI-backed defaults) + self.agent: AgentLike = agent or Agent(model, model_settings=model_settings) + self.reflector: ReflectorLike = reflector or Reflector( + model, model_settings=model_settings + ) + self.skill_manager: SkillManagerLike = skill_manager or SkillManager( + model, model_settings=model_settings + ) + + self.environment = environment + self.is_learning = is_learning + + # Resolve dedup manager + if dedup_manager is not None: + self._dedup_manager: DeduplicationManagerLike | None = dedup_manager + elif dedup_config is not None: + from ..deduplication import DeduplicationManager + + self._dedup_manager = DeduplicationManager(dedup_config) + else: + self._dedup_manager = None + + self._dedup_interval = dedup_interval + self._checkpoint_dir = checkpoint_dir + self._checkpoint_interval = checkpoint_interval + + # Logfire observability (explicit opt-in — fail loudly) + if logfire: + from ..observability import configure_logfire + + if not configure_logfire(): + raise ImportError( + "logfire=True requires the 'logfire' package. " + "Install it with: pip install ace-framework[logfire]" + ) + + # Lazy-init caches + self._ace: ACE | None = None + self._analyser: TraceAnalyser | None = None + + # Last interaction for learn_from_feedback() + self._last_interaction: tuple[str, AgentOutput] | None = None + + # ------------------------------------------------------------------ + # Alternative constructors + # ------------------------------------------------------------------ + + @classmethod + def from_setup( + cls, + *, + config_dir: str | Path | None = None, + validate: bool = False, + **kwargs: Any, + ) -> ACELiteLLM: + """Build from ace.toml + .env (created by ``ace setup``). + + Looks for ``ace.toml`` in the current directory and parents. + Loads ``.env`` for API keys. Optionally validates each model + connection before proceeding. + + Args: + config_dir: Explicit directory containing ace.toml. + If None, searches current directory and parents. + validate: If True, run a test LLM call for each configured + model before building. Raises on failure. + **kwargs: Extra kwargs forwarded to the constructor + (skillbook, environment, etc.). + + Raises: + FileNotFoundError: If no ace.toml is found. + ConnectionError: If validate=True and a model fails. + """ + from ..providers.config import ( + find_config, + load_config, + load_dotenv, + ) + + # Load .env first so keys are available + load_dotenv() + + # Find and load ace.toml + if config_dir is not None: + config = load_config(config_dir) + else: + config_path = find_config() + if config_path is None: + raise FileNotFoundError( + "No ace.toml found. Run `ace setup` to create one, " + "or use ACELiteLLM.from_model() / ACELiteLLM.from_config()." + ) + config = load_config(config_path.parent) + + return cls.from_config(config, validate=validate, **kwargs) + + @classmethod + def from_config( + cls, + config: Any, # ACEModelConfig — Any to avoid circular import at module level + *, + validate: bool = False, + **kwargs: Any, + ) -> ACELiteLLM: + """Build from an ``ACEModelConfig`` with per-role model selection. + + API keys are resolved from the environment (not from config). + Each role gets its own PydanticAI-backed implementation, + allowing different models for Agent, Reflector, and SkillManager. + + Args: + config: An ``ACEModelConfig`` mapping roles to models. + validate: If True, validate each model connection first. + **kwargs: Extra kwargs forwarded to the constructor + (skillbook, environment, etc.). + + Example:: + + from ace.providers.config import ACEModelConfig, ModelConfig + + config = ACEModelConfig( + default=ModelConfig(model="gpt-4o-mini"), + agent=ModelConfig(model="claude-sonnet-4-20250514"), + ) + ace = ACELiteLLM.from_config(config) + """ + from ..providers.config import ACEModelConfig + + if not isinstance(config, ACEModelConfig): + raise TypeError(f"Expected ACEModelConfig, got {type(config).__name__}") + + if validate: + from ..providers.registry import validate_connection + + seen: set[str] = set() + for role in ("agent", "reflector", "skill_manager"): + mc = config.for_role(role) + if mc.model in seen: + continue + seen.add(mc.model) + result = validate_connection(mc.model) + if not result.success: + raise ConnectionError( + f"Model '{mc.model}' (for {role}) failed validation: " + f"{result.error}" + ) + + agent_config = config.for_role("agent") + reflector_config = config.for_role("reflector") + sm_config = config.for_role("skill_manager") + + return cls( + agent_config.model, + agent=Agent( + agent_config.model, + model_settings=settings_from_config(agent_config), + ), + reflector=Reflector( + reflector_config.model, + model_settings=settings_from_config(reflector_config), + ), + skill_manager=SkillManager( + sm_config.model, + model_settings=settings_from_config(sm_config), + ), + **kwargs, + ) + + @classmethod + def from_model( + cls, + model: str = "gpt-4o-mini", + *, + max_tokens: int = 2048, + temperature: float = 0.0, + skillbook: Skillbook | None = None, + skillbook_path: Optional[str] = None, + environment: Optional[TaskEnvironment] = None, + dedup_config: Optional[DeduplicationConfig] = None, + dedup_interval: int = 10, + checkpoint_dir: Optional[Union[str, Path]] = None, + checkpoint_interval: int = 10, + is_learning: bool = True, + logfire: bool = False, + ) -> ACELiteLLM: + """Build from a model string. + + Args: + model: LiteLLM model identifier (e.g. ``"gpt-4o-mini"``). + max_tokens: Max tokens for LLM responses. + temperature: Sampling temperature. + skillbook: Starting skillbook. + skillbook_path: Path to load skillbook from. + environment: Task environment for evaluation. + dedup_config: Deduplication configuration. + dedup_interval: Samples between deduplication runs. + checkpoint_dir: Directory for checkpoint files. + checkpoint_interval: Samples between checkpoint saves. + is_learning: Whether learning is enabled. + logfire: Enable Logfire observability (auto-instruments PydanticAI). + """ + model_settings = ModelSettings( + temperature=temperature, + max_tokens=max_tokens, + ) + return cls( + model, + model_settings=model_settings, + skillbook=skillbook, + skillbook_path=skillbook_path, + environment=environment, + dedup_config=dedup_config, + dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, + checkpoint_interval=checkpoint_interval, + is_learning=is_learning, + logfire=logfire, + ) + + # ------------------------------------------------------------------ + # Lazy-init runners + # ------------------------------------------------------------------ + + def _get_extra_steps(self) -> list[Any] | None: + """Return extra pipeline steps or None.""" + return None + + def _get_ace(self, environment: TaskEnvironment | None = None) -> ACE: + """Return (or build) the cached ACE runner.""" + env = environment or self.environment + # Invalidate if environment changed + if self._ace is not None and env is not self.environment: + self._ace = None + self.environment = env + if self._ace is None: + self._ace = ACE.from_roles( + agent=self.agent, + reflector=self.reflector, + skill_manager=self.skill_manager, + environment=env, + skillbook=self._skillbook, + dedup_manager=self._dedup_manager, + dedup_interval=self._dedup_interval, + checkpoint_dir=self._checkpoint_dir, + checkpoint_interval=self._checkpoint_interval, + extra_steps=self._get_extra_steps(), + ) + return self._ace + + def _get_analyser(self) -> TraceAnalyser: + """Return (or build) the cached TraceAnalyser.""" + if self._analyser is None: + self._analyser = TraceAnalyser.from_roles( + reflector=self.reflector, + skill_manager=self.skill_manager, + skillbook=self._skillbook, + dedup_manager=self._dedup_manager, + dedup_interval=self._dedup_interval, + checkpoint_dir=self._checkpoint_dir, + checkpoint_interval=self._checkpoint_interval, + extra_steps=self._get_extra_steps(), + ) + return self._analyser + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def ask(self, question: str, context: str = "") -> str: + """Ask a question using the current skillbook. + + Direct Agent call — does not go through the pipeline. Stores + the interaction for optional :meth:`learn_from_feedback`. + + Args: + question: The question to answer. + context: Optional context for the question. + + Returns: + The agent's final answer. + """ + output = self.agent.generate( + question=question, + context=context, + skillbook=self._skillbook, + ) + self._last_interaction = (question, output) + return output.final_answer + + def learn( + self, + samples: Sequence[Sample], + environment: TaskEnvironment | None = None, + epochs: int = 1, + *, + wait: bool = True, + ) -> list[SampleResult]: + """Run the full ACE learning pipeline over samples. + + Args: + samples: Training samples with questions and ground truth. + environment: Task environment for evaluation. Falls back + to the environment set at construction time. + epochs: Number of passes over the samples. + wait: If ``True``, block until background learning completes. + + Returns: + List of ``SampleResult``, one per sample per epoch. + + Raises: + RuntimeError: If learning is disabled. + """ + if not self.is_learning: + raise RuntimeError("Learning is disabled. Call enable_learning() first.") + return self._get_ace(environment).run(samples, epochs=epochs, wait=wait) + + def learn_from_traces( + self, + traces: Sequence[Any], + epochs: int = 1, + *, + wait: bool = True, + ) -> list[SampleResult]: + """Learn from pre-recorded execution traces. + + Args: + traces: Raw trace objects (dicts, framework results, etc.). + epochs: Number of passes over the traces. + wait: If ``True``, block until background learning completes. + + Returns: + List of ``SampleResult``, one per trace per epoch. + + Raises: + RuntimeError: If learning is disabled. + """ + if not self.is_learning: + raise RuntimeError("Learning is disabled. Call enable_learning() first.") + return self._get_analyser().run(traces, epochs=epochs, wait=wait) + + def learn_from_feedback( + self, + feedback: str, + ground_truth: str | None = None, + ) -> bool: + """Learn from the last :meth:`ask` interaction. + + Runs the standard ``learning_tail`` pipeline (ReflectStep, + UpdateStep) on the most recent ``ask()`` call with the provided + feedback. The agentic SkillManager mutates the skillbook directly + — there is no separate apply step. + + Args: + feedback: User feedback about the answer quality. + ground_truth: Optional correct answer. + + Returns: + ``True`` if learning was applied, ``False`` if no prior + interaction exists or learning is disabled. + """ + if not self.is_learning or self._last_interaction is None: + return False + + question, agent_output = self._last_interaction + + # Build synthetic trace (same format EvaluateStep produces) + trace = { + "question": question, + "context": "", + "ground_truth": ground_truth, + "reasoning": agent_output.reasoning, + "answer": agent_output.final_answer, + "skill_ids": agent_output.skill_ids, + "feedback": feedback, + } + + ctx = ACEStepContext( + skillbook=SkillbookView(self._skillbook), + trace=trace, + ) + + # Same learning pipeline as learn() and learn_from_traces() + from pipeline import Pipeline + + steps = learning_tail( + self.reflector, + self.skill_manager, + self._skillbook, + ) + pipe = Pipeline(steps) + results = pipe.run([ctx]) + pipe.wait_for_background() # ReflectStep has async_boundary=True + return len(results) > 0 and results[0].error is None + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + + @property + def skillbook(self) -> Skillbook: + """The current skillbook.""" + return self._skillbook + + def save(self, path: str) -> None: + """Save the skillbook to disk.""" + self._skillbook.save_to_file(path) + + def load(self, path: str) -> None: + """Load a skillbook from disk. + + Invalidates cached runners (they hold stale skillbook refs). + """ + self._skillbook = Skillbook.load_from_file(path) + self._ace = None + self._analyser = None + + def enable_learning(self) -> None: + """Enable learning.""" + self.is_learning = True + + def disable_learning(self) -> None: + """Disable learning.""" + self.is_learning = False + + def get_strategies(self) -> str: + """Return formatted skillbook strategies for display.""" + if not self._skillbook.skills(): + return "" + return wrap_skillbook_context(self._skillbook) + + def wait_for_background(self, timeout: float | None = None) -> None: + """Block until all background learning completes.""" + if self._ace is not None: + self._ace.wait_for_background(timeout) + if self._analyser is not None: + self._analyser.wait_for_background(timeout) + + @property + def learning_stats(self) -> dict[str, int]: + """Return background learning progress.""" + stats: dict[str, int] = {} + if self._ace is not None: + stats.update(self._ace.learning_stats) + if self._analyser is not None: + stats.update(self._analyser.learning_stats) + return stats + + # Backward-compat aliases + save_skillbook = save + load_skillbook = load + wait_for_learning = wait_for_background diff --git a/ace/runners/trace_analyser.py b/ace/runners/trace_analyser.py new file mode 100644 index 0000000000000000000000000000000000000000..8f08d8fe74d558b33041ecfef329ba91e9b7b9a1 --- /dev/null +++ b/ace/runners/trace_analyser.py @@ -0,0 +1,187 @@ +"""TraceAnalyser — batch learning from pre-recorded execution traces.""" + +from __future__ import annotations + +from collections.abc import Sequence +from pathlib import Path +from types import MappingProxyType +from typing import Any + +from pipeline import Pipeline +from pipeline.errors import CancellationToken +from pipeline.protocol import SampleResult, StepProtocol + +from ..core.context import ACEStepContext, SkillbookView +from ..core.insight_source import TRACE_IDENTITY_METADATA_KEY, infer_trace_identity +from ..protocols import ( + DeduplicationManagerLike, + ReflectorLike, + SkillManagerLike, +) +from ..core.skillbook import Skillbook +from ..steps import learning_tail +from .base import ACERunner + + +class TraceAnalyser(ACERunner): + """Analyse pre-recorded traces to build a skillbook. + + Runs the learning tail only — Reflect and Update — with optional + deduplication and checkpoint steps. No AgentStep, no EvaluateStep. + The agentic SkillManager mutates the skillbook directly through its + tools, so no ApplyStep is needed. + + Accepts raw trace objects of any type. They are placed directly on + ``ctx.trace`` for the Reflector to interpret. + + Use when you have execution logs from an external system (browser-use + ``AgentHistoryList``, LangChain intermediate steps, Claude Code + transcripts) and want to build or refine a skillbook from historical + data. + """ + + @classmethod + def build_steps( + cls, + *, + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + skillbook: Skillbook | None = None, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, + extra_steps: list[StepProtocol] | None = None, + ) -> list[StepProtocol]: + """Return the steps that ``from_roles()`` would compose. + + Use this to inspect, modify, or extend the pipeline before + constructing it yourself:: + + steps = TraceAnalyser.build_steps(reflector=r, skill_manager=sm, ...) + steps.append(MyCustomStep()) + pipe = Pipeline(steps) + runner = ACERunner(pipeline=pipe, skillbook=skillbook) + + Args: + reflector: Reflector role for analysing traces. + skill_manager: SkillManager role for producing update operations. + skillbook: Starting skillbook. Creates an empty one if ``None``. + dedup_manager: Optional deduplication manager. + dedup_interval: Samples between deduplication runs. + checkpoint_dir: Directory for checkpoint files. + checkpoint_interval: Samples between checkpoint saves. + extra_steps: Additional steps appended after the learning + tail (e.g. ``OpikStep``). + """ + skillbook = skillbook or Skillbook() + steps = learning_tail( + reflector, + skill_manager, + skillbook, + dedup_manager=dedup_manager, + dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, + checkpoint_interval=checkpoint_interval, + ) + if extra_steps: + steps.extend(extra_steps) + return steps + + @classmethod + def from_roles( + cls, + *, + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + skillbook: Skillbook | None = None, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, + extra_steps: list[StepProtocol] | None = None, + ) -> TraceAnalyser: + """Construct from pre-built role instances. + + Args: + reflector: Reflector role for analysing traces. + skill_manager: SkillManager role for producing update operations. + skillbook: Starting skillbook. Creates an empty one if ``None``. + dedup_manager: Optional deduplication manager. Appends a + ``DeduplicateStep`` when provided. + dedup_interval: Samples between deduplication runs. + checkpoint_dir: Directory for checkpoint files. Appends a + ``CheckpointStep`` when provided. + checkpoint_interval: Samples between checkpoint saves. + extra_steps: Additional steps appended after the learning + tail (e.g. ``OpikStep``). + """ + skillbook = skillbook or Skillbook() + steps = cls.build_steps( + reflector=reflector, + skill_manager=skill_manager, + skillbook=skillbook, + dedup_manager=dedup_manager, + dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, + checkpoint_interval=checkpoint_interval, + extra_steps=extra_steps, + ) + return cls(pipeline=Pipeline(steps), skillbook=skillbook) + + def run( + self, + traces: Sequence[Any], + epochs: int = 1, + *, + wait: bool = True, + cancel_token: CancellationToken | None = None, + ) -> list[SampleResult]: + """Analyse traces and evolve the skillbook. + + Args: + traces: Sequence of raw trace objects (any type). + epochs: Number of passes over all traces. + wait: If ``True``, block until background learning completes. + cancel_token: Optional cancellation signal. Forwarded to + ``Pipeline.run()`` — checked between steps and inside + LLM calls (via contextvar). + + Returns: + List of ``SampleResult``, one per trace per epoch. + """ + return self._run(traces, epochs=epochs, wait=wait, cancel_token=cancel_token) + + def _build_context( # type: ignore[override] + self, + raw_trace: Any, + *, + epoch: int, + total_epochs: int, + index: int, + total: int | None, + global_sample_index: int, + **_: Any, + ) -> ACEStepContext: + """Place a raw trace directly on the context. + + No extraction, no conversion — the Reflector receives the trace + as-is and has full freedom to analyse it. + """ + return ACEStepContext( + skillbook=SkillbookView(self.skillbook), + trace=raw_trace, + metadata=MappingProxyType( + { + TRACE_IDENTITY_METADATA_KEY: infer_trace_identity( + trace=raw_trace, + default_source_system="trace", + ).to_dict() + } + ), + epoch=epoch, + total_epochs=total_epochs, + step_index=index, + total_steps=total, + global_sample_index=global_sample_index, + ) diff --git a/ace/steps/__init__.py b/ace/steps/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f97c9751cd166cd58965b341239a1e6d5384f2ff --- /dev/null +++ b/ace/steps/__init__.py @@ -0,0 +1,87 @@ +"""ACE pipeline steps — one class per file, plus the learning_tail helper.""" + +from __future__ import annotations + +from pathlib import Path + +from pipeline.protocol import StepProtocol + +from ..core.context import ACEStepContext +from ..protocols import ( + DeduplicationManagerLike, + ReflectorLike, + SkillManagerLike, +) +from ..core.skillbook import Skillbook + +from .agent import AgentStep +from .checkpoint import CheckpointStep +from .deduplicate import DeduplicateStep +from .evaluate import EvaluateStep +from .export_markdown import ExportSkillbookMarkdownStep +from .load_traces import LoadTracesStep +from .observability import ObservabilityStep +from .persist import PersistStep +from .reflect import ReflectStep +from .update import UpdateStep + +__all__ = [ + "AgentStep", + "CheckpointStep", + "DeduplicateStep", + "EvaluateStep", + "ExportSkillbookMarkdownStep", + "LoadTracesStep", + "ObservabilityStep", + "PersistStep", + "ReflectStep", + "UpdateStep", + "learning_tail", +] + + +def _reflect_step(reflector: ReflectorLike) -> StepProtocol[ACEStepContext]: + provides = getattr(reflector, "provides", ()) + if callable(reflector) and "reflections" in provides: + return reflector # type: ignore[return-value] + return ReflectStep(reflector) + + +def learning_tail( + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + skillbook: Skillbook, + *, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, +) -> list[StepProtocol[ACEStepContext]]: + """Return the standard ACE learning steps. + + Use this when building custom integrations that provide their own + execute step(s) but want the standard learning pipeline:: + + steps = [ + MyCustomExecuteStep(my_agent), + *learning_tail(reflector, skill_manager, skillbook), + ] + + The returned list starts with either ``ReflectStep`` or the provided + reflector itself when it already satisfies the step protocol and exposes + ``provides = {'reflections'}``, followed by ``UpdateStep``. The agentic + SkillManager mutates the skillbook directly through its tools, so no + ``ApplyStep`` follows. Optional ``DeduplicateStep`` and ``CheckpointStep`` + are appended when configured. + """ + steps: list[StepProtocol[ACEStepContext]] = [ + _reflect_step(reflector), + UpdateStep(skill_manager, skillbook), + ] + if dedup_manager: + steps.append(DeduplicateStep(dedup_manager, skillbook, interval=dedup_interval)) + if checkpoint_dir: + steps.append( + CheckpointStep(checkpoint_dir, skillbook, interval=checkpoint_interval) + ) + return steps diff --git a/ace/steps/agent.py b/ace/steps/agent.py new file mode 100644 index 0000000000000000000000000000000000000000..a477dbd2b86340ccf097733dc12525c32beac271 --- /dev/null +++ b/ace/steps/agent.py @@ -0,0 +1,42 @@ +"""AgentStep — runs the Agent role to produce an answer.""" + +from __future__ import annotations + +from ..core.context import ACEStepContext +from ..core.outputs import AgentOutput +from ..core.skillbook import Skillbook +from ..protocols import AgentLike + + +class AgentStep: + """Execute the Agent role against the current sample and skillbook. + + Reads the skillbook via ``ctx.skillbook`` (a ``SkillbookView``). Records + the set of skills rendered into the agent prompt this run as + ``ctx.injected_skill_ids`` and bumps ``used_count`` on each — this is the + ground-truth attribution scope consumed by downstream steps (Reflector/RR, + SkillManager). No other upstream counter is touched. + """ + + requires = frozenset({"sample", "skillbook"}) + provides = frozenset({"agent_output", "injected_skill_ids"}) + + def __init__(self, agent: AgentLike, skillbook: Skillbook) -> None: + self.agent = agent + self.skillbook = skillbook + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + injected_ids = tuple(s.id for s in self.skillbook.skills()) + + agent_output: AgentOutput = self.agent.generate( + question=ctx.sample.question, + context=ctx.sample.context, + skillbook=ctx.skillbook, + ) + + self.skillbook.mark_used(injected_ids) + + return ctx.replace( + agent_output=agent_output, + injected_skill_ids=injected_ids, + ) diff --git a/ace/steps/checkpoint.py b/ace/steps/checkpoint.py new file mode 100644 index 0000000000000000000000000000000000000000..da50f0fd194c59109520ddcb727e7f5b64faa536 --- /dev/null +++ b/ace/steps/checkpoint.py @@ -0,0 +1,57 @@ +"""CheckpointStep — periodically saves the skillbook to disk.""" + +from __future__ import annotations + +import logging +from pathlib import Path + +from ..core.skillbook import Skillbook + +from ..core.context import ACEStepContext + +logger = logging.getLogger(__name__) + + +class CheckpointStep: + """Save the skillbook to disk at a configurable interval. + + Optional tail step appended by factory methods when ``checkpoint_dir`` + is provided. + + Stateless — uses ``ctx.global_sample_index`` for interval logic. + Saves both a numbered checkpoint and a ``latest.json`` that is + always overwritten with the most recent state. + """ + + requires: frozenset[str] = frozenset({"global_sample_index"}) + provides: frozenset[str] = frozenset() + + def __init__( + self, + directory: str | Path, + skillbook: Skillbook, + *, + interval: int = 10, + ) -> None: + self.directory = Path(directory) + self.skillbook = skillbook + self.interval = interval + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + if ctx.global_sample_index % self.interval != 0: + return ctx + + self.directory.mkdir(parents=True, exist_ok=True) + + numbered = self.directory / f"checkpoint_{ctx.global_sample_index}.json" + latest = self.directory / "latest.json" + + self.skillbook.save_to_file(str(numbered)) + self.skillbook.save_to_file(str(latest)) + + logger.info( + "CheckpointStep: saved checkpoint at sample %d → %s", + ctx.global_sample_index, + numbered, + ) + return ctx diff --git a/ace/steps/deduplicate.py b/ace/steps/deduplicate.py new file mode 100644 index 0000000000000000000000000000000000000000..38385daaf0ef8a91afb232fb07e9e899012b47ce --- /dev/null +++ b/ace/steps/deduplicate.py @@ -0,0 +1,52 @@ +"""DeduplicateStep — periodically consolidates similar skills.""" + +from __future__ import annotations + +import logging + +from ..core.context import ACEStepContext +from ..protocols import DeduplicationManagerLike +from ..core.skillbook import Skillbook + +logger = logging.getLogger(__name__) + + +class DeduplicateStep: + """Consolidate similar skills in the skillbook at a configurable interval. + + Optional side-effect step — appended to the pipeline by factory methods + when ``dedup_config`` is provided. + + Stateless — uses ``ctx.global_sample_index`` with ``self.interval`` to + skip most invocations. Deduplication involves O(n^2) similarity + comparisons, so running on every sample would be expensive. + """ + + requires: frozenset[str] = frozenset({"global_sample_index"}) + provides: frozenset[str] = frozenset() + + max_workers = 1 + + def __init__( + self, + manager: DeduplicationManagerLike, + skillbook: Skillbook, + *, + interval: int = 10, + ) -> None: + self.manager = manager + self.skillbook = skillbook + self.interval = interval + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + if ctx.global_sample_index % self.interval != 0: + return ctx + + report = self.manager.get_similarity_report(self.skillbook) + if report: + logger.info( + "DeduplicateStep: similarity report at sample %d:\n%s", + ctx.global_sample_index, + report, + ) + return ctx diff --git a/ace/steps/evaluate.py b/ace/steps/evaluate.py new file mode 100644 index 0000000000000000000000000000000000000000..1eb345bc6ab2b8e85155db8074b977ba37fc80ae --- /dev/null +++ b/ace/steps/evaluate.py @@ -0,0 +1,46 @@ +"""EvaluateStep — bridges the execute head to the learning tail.""" + +from __future__ import annotations + +from ..core.context import ACEStepContext +from ..core.environments import TaskEnvironment + + +class EvaluateStep: + """Bundle agent output into a trace dict, optionally evaluating with an environment. + + Always produces a ``trace`` dict with the structured fields from the + execute head (question, context, ground_truth, reasoning, answer, + skill_ids). When an environment is provided, its feedback is included. + + The environment is injected at construction time — not on the context — + to keep the context free of per-runner dependencies. + """ + + requires = frozenset({"sample", "agent_output"}) + provides = frozenset({"trace"}) + + def __init__(self, environment: TaskEnvironment | None = None) -> None: + self.environment = environment + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + if ctx.agent_output is None: + raise ValueError( + "EvaluateStep requires agent_output to be set on the context" + ) + + trace: dict = { + "question": ctx.sample.question, + "context": ctx.sample.context, + "ground_truth": ctx.sample.ground_truth, + "reasoning": ctx.agent_output.reasoning, + "answer": ctx.agent_output.final_answer, + "skill_ids": ctx.agent_output.skill_ids, + } + if self.environment: + result = self.environment.evaluate( + sample=ctx.sample, + agent_output=ctx.agent_output, + ) + trace["feedback"] = result.feedback + return ctx.replace(trace=trace) diff --git a/ace/steps/export_markdown.py b/ace/steps/export_markdown.py new file mode 100644 index 0000000000000000000000000000000000000000..4ec6b8bee73c3a9a58a9d2661666c6543c8c119d --- /dev/null +++ b/ace/steps/export_markdown.py @@ -0,0 +1,63 @@ +"""ExportSkillbookMarkdownStep — writes the skillbook as a human-readable markdown file.""" + +from __future__ import annotations + +from collections import defaultdict +from pathlib import Path + +from ..core.context import ACEStepContext +from ..core.skillbook import Skillbook + + +class ExportSkillbookMarkdownStep: + """Export the skillbook as a markdown file after each learning cycle. + + Rewrites the file from scratch on every invocation so the markdown + always reflects the current state of the skillbook. + """ + + requires: frozenset[str] = frozenset({"skillbook"}) + provides: frozenset[str] = frozenset() + + def __init__(self, path: str | Path, skillbook: Skillbook) -> None: + self.path = Path(path) + self.skillbook = skillbook + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + skills = self.skillbook.skills() + if not skills: + return ctx + + by_section: dict[str, list] = defaultdict(list) + for skill in skills: + by_section[skill.section].append(skill) + + lines: list[str] = ["# ACE Skillbook", ""] + + for section in sorted(by_section): + lines.append(f"## {section}") + lines.append("") + for skill in by_section[section]: + tags = ( + f"helpful={skill.helpful_count}, " + f"harmful={skill.harmful_count}, " + f"neutral={skill.neutral_count}" + ) + lines.append(f"### `{skill.id}`") + lines.append("") + lines.append(f"**Keywords:** {', '.join(skill.keywords)}") + lines.append("") + lines.append(f"**Issue:** {skill.issue}") + lines.append("") + if skill.insight: + lines.append(f"**Insight:** {skill.insight}") + lines.append("") + if skill.occurrences: + lines.append(f"**Occurrences:** {len(skill.occurrences)}") + lines.append("") + lines.append(f"*Tags: {tags}*") + lines.append("") + + self.path.parent.mkdir(parents=True, exist_ok=True) + self.path.write_text("\n".join(lines)) + return ctx diff --git a/ace/steps/load_traces.py b/ace/steps/load_traces.py new file mode 100644 index 0000000000000000000000000000000000000000..b29f7d63e10b6d19bf42cf239a53a6d7be5850cf --- /dev/null +++ b/ace/steps/load_traces.py @@ -0,0 +1,51 @@ +"""LoadTracesStep — generic file-to-trace loader for JSONL files.""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path + +from ..core.context import ACEStepContext + +logger = logging.getLogger(__name__) + + +class LoadTracesStep: + """Read a JSONL file from disk and place parsed events on ``ctx.trace``. + + Reads the file at ``ctx.sample`` (a ``str`` or ``Path``), parses each + line as JSON, and places the resulting ``list[dict]`` on ``ctx.trace``. + Unparseable lines are silently skipped. + + If the file is empty or missing, ``ctx.trace`` is set to an empty list. + """ + + requires = frozenset({"sample"}) + provides = frozenset({"trace"}) + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + path = Path(ctx.sample) + events: list[dict] = [] + + if not path.exists(): + logger.warning("Trace file not found: %s", path) + return ctx.replace(trace=events) + + try: + text = path.read_text(encoding="utf-8") + except OSError as exc: + logger.warning("Failed to read trace file %s: %s", path, exc) + return ctx.replace(trace=events) + + for line_num, line in enumerate(text.splitlines(), start=1): + line = line.strip() + if not line: + continue + try: + events.append(json.loads(line)) + except json.JSONDecodeError: + logger.debug("Skipping unparseable line %d in %s", line_num, path.name) + continue + + return ctx.replace(trace=events) diff --git a/ace/steps/observability.py b/ace/steps/observability.py new file mode 100644 index 0000000000000000000000000000000000000000..fe751daced0d0624c3033a722926508279436eeb --- /dev/null +++ b/ace/steps/observability.py @@ -0,0 +1,35 @@ +"""ObservabilityStep — logs pipeline metrics to Opik.""" + +from __future__ import annotations + +import logging + +from ..core.context import ACEStepContext + +logger = logging.getLogger(__name__) + + +class ObservabilityStep: + """Log pipeline metrics to the observability backend. + + Optional side-effect step — only requires ``skillbook`` (always present). + Reads other context fields optionally so the same step works in both + ACE and TraceAnalyser pipelines. + """ + + requires: frozenset[str] = frozenset({"skillbook"}) + provides: frozenset[str] = frozenset() + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + metrics: dict = {"skill_count": len(ctx.skillbook) if ctx.skillbook else 0} + + if ctx.reflections: + metrics["key_insight"] = ctx.reflections[-1].key_insight + metrics["reflections_count"] = len(ctx.reflections) + if ctx.skill_manager_output: + metrics["operations_count"] = len(ctx.skill_manager_output.operations) + if ctx.trace: + metrics["trace_type"] = type(ctx.trace).__name__ + + logger.info("ObservabilityStep: %s", metrics) + return ctx diff --git a/ace/steps/persist.py b/ace/steps/persist.py new file mode 100644 index 0000000000000000000000000000000000000000..03366edb8729e1a1555a652217bd207cdb2d9f06 --- /dev/null +++ b/ace/steps/persist.py @@ -0,0 +1,31 @@ +"""PersistStep — writes the skillbook to an external file.""" + +from __future__ import annotations + +from pathlib import Path + +from ..core.skillbook import Skillbook + +from ..core.context import ACEStepContext + + +class PersistStep: + """Write the current skillbook to a target file after each sample. + + Integration-specific side-effect step — used by ClaudeCodeACE to + persist learned strategies into the project's CLAUDE.md. + + Unlike CheckpointStep (which saves full JSON at intervals), PersistStep + runs on every sample and writes in whatever format the target expects. + """ + + requires: frozenset[str] = frozenset({"skillbook"}) + provides: frozenset[str] = frozenset() + + def __init__(self, target_path: str | Path, skillbook: Skillbook) -> None: + self.target_path = Path(target_path) + self.skillbook = skillbook + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + self.skillbook.save_to_file(str(self.target_path)) + return ctx diff --git a/ace/steps/reflect.py b/ace/steps/reflect.py new file mode 100644 index 0000000000000000000000000000000000000000..73e031b19e0a75a3227ec024e7139115e6b941a4 --- /dev/null +++ b/ace/steps/reflect.py @@ -0,0 +1,87 @@ +"""ReflectStep — analyses a trace to produce a ReflectorOutput.""" + +from __future__ import annotations + +import logging + +from ..core.context import ACEStepContext +from ..core.outputs import AgentOutput +from ..protocols import ReflectorLike + +logger = logging.getLogger(__name__) + + +class ReflectStep: + """Run the Reflector role against the trace and current skillbook. + + Receives ``ctx.trace`` — a dict from EvaluateStep (standard ACE pipeline), + a raw object from TraceAnalyser, or any integration-produced trace. When + the trace is a dict with known keys, the step extracts them and calls the + Reflector's existing API. For raw/opaque traces, it passes them as + keyword arguments for the Reflector to handle. + + Declares ``async_boundary = True`` — everything from this step onward + runs in a background thread pool when the pipeline has background + execution enabled. + + Pure — produces a reflection object, no side effects. + """ + + requires = frozenset({"trace", "skillbook"}) + provides = frozenset({"reflections"}) + + async_boundary = True + max_workers = 3 + + def __init__(self, reflector: ReflectorLike) -> None: + self.reflector = reflector + + @staticmethod + def _is_batch_container(trace: dict) -> bool: + for key in ("items", "tasks"): + if isinstance(trace.get(key), list): + return True + + steps = trace.get("steps") + return ( + isinstance(steps, list) + and bool(steps) + and all( + isinstance(step, dict) + and step.get("role") == "conversation" + and isinstance(step.get("content"), dict) + for step in steps + ) + ) + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + trace = ctx.trace + + if isinstance(trace, dict) and not self._is_batch_container(trace): + # Structured trace from EvaluateStep — extract known fields + agent_output = AgentOutput( + reasoning=trace.get("reasoning", ""), + final_answer=trace.get("answer", ""), + ) + reflection = self.reflector.reflect( + question=trace.get("question", ""), + agent_output=agent_output, + skillbook=ctx.skillbook, + ground_truth=trace.get("ground_truth"), + feedback=trace.get("feedback"), + injected_skill_ids=ctx.injected_skill_ids, + mode=ctx.mode, + ) + else: + # Raw trace from TraceAnalyser or integration — pass as-is + # The Reflector must handle the trace type via **kwargs + reflection = self.reflector.reflect( + question="", + agent_output=AgentOutput(reasoning="", final_answer=""), + skillbook=ctx.skillbook, + trace=trace, + injected_skill_ids=ctx.injected_skill_ids, + mode=ctx.mode, + ) + + return ctx.replace(reflections=(reflection,)) diff --git a/ace/steps/rr_step.py b/ace/steps/rr_step.py new file mode 100644 index 0000000000000000000000000000000000000000..90de3595871556d145cc88d58bf81301fa284abb --- /dev/null +++ b/ace/steps/rr_step.py @@ -0,0 +1,511 @@ +"""RRStep — Recursive Reflector pipeline step. + +Subclass of :class:`RecursiveAgent` that satisfies both ``StepProtocol`` +and ``ReflectorLike``. Adds RR-specific trace setup, prompt building, +and timeout handling on top of the generic recursive agent infrastructure. +""" + +from __future__ import annotations + +import json as _json +import logging +from typing import Any, Optional, cast + +from pydantic_ai.models import Model as PydanticModel +from pydantic_ai.output import PromptedOutput +from pydantic_ai.settings import ModelSettings + +from ace.core.context import ACEStepContext +from ace.core.outputs import AgentOutput, ReflectorOutput +from ace.core.recursive_agent import ( + BudgetExhausted, + RecursiveAgent, +) +from ace.core.sandbox import ExecutionResult, ExecutionTimeoutError, TraceSandbox +from ace.implementations.rr.config import RecursiveConfig as RRConfig +from ace.implementations.rr.prompts import ( + COMPACTION_SUMMARY_PROMPT, + REFLECTOR_RECURSIVE_PROMPT, + REFLECTOR_RECURSIVE_SYSTEM, + RR_SKILLBOOK_INSPECTION_SECTION, +) +from ace.implementations.rr.tools import ( + RRDeps, + register_output_validator, + register_read_skill, + register_search_skillbook, + register_think, +) + +logger = logging.getLogger(__name__) + + +def _preview(text: str | None, max_len: int = 150) -> str: + """Return a short preview safe for str.format().""" + if not text: + return "(empty)" + snippet = text if len(text) <= max_len else text[:max_len] + return snippet.replace("{", "{{").replace("}", "}}") + + +class RRStep(RecursiveAgent): + """Recursive Reflector as a pipeline step. + + Satisfies **StepProtocol** (``requires``/``provides``) and + **ReflectorLike** (``reflect`` method). + + Subclass of :class:`RecursiveAgent` — inherits compaction, + recursion, and budget management. + + Args: + model: LiteLLM/PydanticAI model-id string or a pre-built + pydantic-ai ``Model`` instance. Strings go through + ``resolve_model``; instances pass through unchanged (for + callers that need a custom provider — e.g. cross-account + Bedrock with STS-assumed credentials). + config: RR configuration (timeouts, limits, sub-agent settings). + prompt_template: User prompt template with format placeholders. + model_settings: Override PydanticAI model settings. + """ + + requires = frozenset({"trace", "skillbook"}) + provides = frozenset({"reflections"}) + config: RRConfig + + def __init__( + self, + model: str | PydanticModel, + config: Optional[RRConfig] = None, + prompt_template: str = REFLECTOR_RECURSIVE_PROMPT, + model_settings: ModelSettings | None = None, + ) -> None: + self.prompt_template = prompt_template + effective_model_settings: ModelSettings + if model_settings is None: + from pydantic_ai.models.bedrock import BedrockModelSettings + + effective_model_settings = BedrockModelSettings( + temperature=0.0, + bedrock_cache_instructions=True, + bedrock_cache_tool_definitions=True, + bedrock_cache_messages=True, + ) + else: + effective_model_settings = model_settings + + super().__init__( + model, + output_type=cast(Any, PromptedOutput(ReflectorOutput)), + system_prompt=REFLECTOR_RECURSIVE_SYSTEM, + config=config or RRConfig(), + model_settings=effective_model_settings, + tools=[ + register_output_validator, + register_think, + register_read_skill, + register_search_skillbook, + ], + tool_names_to_compact=("execute_code",), + compaction_summary_prompt=COMPACTION_SUMMARY_PROMPT, + compaction_continuation=( + "Your conversation was compacted. " + "All sandbox variables persist — use execute_code to re-inspect data. " + "Do NOT repeat work already completed. Continue your analysis." + ), + microcompact_placeholder=( + "[cleared — data still in sandbox variables, " + "use execute_code to re-inspect]" + ), + on_compaction=RecursiveAgent.on_compaction, + ) + + def _create_agent(self, depth: int = 0) -> Any: + """Create an RR agent and specialize generic tool descriptions.""" + agent = super()._create_agent(depth=depth) + self._specialize_execute_code_tool(agent) + return agent + + @staticmethod + def _specialize_execute_code_tool(agent: Any) -> None: + """Clarify ``execute_code`` semantics for RR without changing core.""" + toolset = getattr(agent, "_function_toolset", None) + tools = getattr(toolset, "tools", {}) if toolset is not None else {} + tool = tools.get("execute_code") + if tool is None: + return + + description = ( + "Execute Python as an evidence workbench over the trace. " + "Use it to inspect runtime data, define sandbox variables, extract " + "slices, store strings/snippets, compute checks, and print compact " + "evidence such as a variable value, short extracted snippet, dict, " + "list, count, boolean, or mismatch. Whenever you would reach for " + '`print("=== HEADING ===")` or a hand-written narrative, route ' + "that prose through the `think` tool instead — that is its job. " + "Final conclusions belong in the structured ReflectorOutput, " + "not in Python prints." + ) + tool.description = description + function_schema = getattr(tool, "function_schema", None) + if function_schema is not None: + function_schema.description = description + code_schema = function_schema.json_schema.get("properties", {}).get("code") + if isinstance(code_schema, dict): + code_schema["description"] = ( + "Python evidence-gathering code. Read from runtime data, " + "assign reusable sandbox variables, compute checks, and " + "print at most compact evidence: a variable value, short " + "snippet, dict/list/check result, count, or mismatch. " + "Send running narration through `think`; send final " + "conclusions through ReflectorOutput." + ) + + # ------------------------------------------------------------------ + # StepProtocol + # ------------------------------------------------------------------ + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + """Run the Recursive Reflector and attach the reflection.""" + trace = ctx.trace or {} + reflection = self._run_reflection( + traces=trace if isinstance(trace, dict) else None, + question=trace.get("question", "") if isinstance(trace, dict) else "", + ground_truth=trace.get("ground_truth") if isinstance(trace, dict) else None, + feedback=trace.get("feedback") if isinstance(trace, dict) else None, + skillbook=ctx.skillbook, + injected_skill_ids=ctx.injected_skill_ids, + trace=trace if not isinstance(trace, dict) else None, + mode=ctx.mode, + ) + return ctx.replace(reflections=(reflection,)) + + # ------------------------------------------------------------------ + # ReflectorLike protocol + # ------------------------------------------------------------------ + + def reflect( + self, + *, + question: str, + agent_output: AgentOutput, + skillbook: Any = None, + ground_truth: Optional[str] = None, + feedback: Optional[str] = None, + **kwargs: Any, + ) -> ReflectorOutput: + """ReflectorLike — delegates to the PydanticAI agent.""" + return self._run_reflection( + question=question, + agent_output=agent_output, + skillbook=skillbook, + ground_truth=ground_truth, + feedback=feedback, + **kwargs, + ) + + # ------------------------------------------------------------------ + # Core reflection logic + # ------------------------------------------------------------------ + + def _run_reflection( + self, + *, + question: str = "", + agent_output: Optional[AgentOutput] = None, + skillbook: Any = None, + ground_truth: Optional[str] = None, + feedback: Optional[str] = None, + injected_skill_ids: tuple[str, ...] = (), + mode: str = "online", + **kwargs: Any, + ) -> ReflectorOutput: + """Run the PydanticAI agent and return analysis.""" + trace_obj = kwargs.pop("trace", None) + if trace_obj is None and agent_output is not None: + trace_obj = getattr(agent_output, "trace_context", None) + + traces = kwargs.pop("traces", None) + if traces is None: + traces = self._build_traces_dict( + question, + agent_output, + ground_truth, + feedback, + trace_obj, + injected_skill_ids, + ) + + sandbox = self._create_sandbox(trace_obj, traces, skillbook) + + skillbook_text = "" + if skillbook is not None: + if hasattr(skillbook, "as_prompt"): + skillbook_text = skillbook.as_prompt() or "(empty skillbook)" + else: + skillbook_text = str(skillbook) + + deps = RRDeps( + sandbox=sandbox, + trace_data=traces, + skillbook_text=skillbook_text or "(empty skillbook)", + skillbook=skillbook, + config=self.config, + depth=0, + max_depth=self.config.max_depth, + ) + + initial_prompt = self._build_initial_prompt(traces, skillbook) + + if ( + mode == "online" + and skillbook_text + and skillbook_text != "(empty skillbook)" + ): + initial_prompt += "\n\n" + RR_SKILLBOOK_INSPECTION_SECTION + + remaining = ( + traces.get("_remaining_tokens") if isinstance(traces, dict) else None + ) + + prompt_payload: Any = initial_prompt + if self.config.cache_prompts: + from pydantic_ai.messages import CachePoint + + prompt_payload = [initial_prompt, CachePoint(ttl=self.config.cache_ttl)] + + try: + output, metadata = self.run( + deps=deps, + prompt=prompt_payload, + remaining_tokens=remaining, + ) + if not isinstance(output, ReflectorOutput): + raise TypeError( + f"RR agent returned {type(output).__name__}, " + "expected ReflectorOutput" + ) + output.raw = { + **output.raw, + "thoughts": list(deps.thoughts), + **metadata, + "rr_trace": { + "total_iterations": deps.iteration, + "subagent_calls": [], + "timed_out": False, + "compactions": metadata.get("compactions", 0), + "depth": 0, + }, + } + except BudgetExhausted as exc: + output = self._build_budget_exhausted_output( + deps, exc.compaction_count, depth=0 + ) + except Exception as e: + logger.error("RR agent failed: %s", e, exc_info=True) + output = ReflectorOutput( + reasoning=f"Recursive analysis failed: {e}", + correct_approach="", + key_insight="", + raw={"error": str(e)}, + ) + + if output.raw.get("timeout") and (ground_truth or agent_output): + output = self._build_timeout_output( + question, agent_output, ground_truth, feedback, deps + ) + + return output + + def _build_budget_exhausted_output( + self, deps: RRDeps, compaction_count: int, depth: int + ) -> ReflectorOutput: + return ReflectorOutput( + reasoning="Analysis reached budget limit.", + error_identification="budget_exhausted", + root_cause_analysis="Analysis incomplete due to token/request budget", + correct_approach="Consider increasing budget or simplifying the analysis", + key_insight="Session reached budget limit before completing", + raw={ + "timeout": True, + "thoughts": list(deps.thoughts), + "rr_trace": { + "total_iterations": deps.iteration, + "subagent_calls": [], + "timed_out": True, + "compactions": compaction_count, + "depth": depth, + }, + }, + ) + + # ------------------------------------------------------------------ + # Setup helpers + # ------------------------------------------------------------------ + + def _build_traces_dict( + self, + question: str, + agent_output: Optional[AgentOutput], + ground_truth: Optional[str], + feedback: Optional[str], + trace_obj: Any, + injected_skill_ids: tuple[str, ...] = (), + ) -> dict[str, Any]: + ao = agent_output + return { + "question": question, + "ground_truth": ground_truth, + "feedback": feedback, + "injected_skill_ids": list(injected_skill_ids), + "steps": [ + { + "role": "agent", + "reasoning": ao.reasoning if ao else "", + "answer": ao.final_answer if ao else "", + } + ], + } + + def _create_sandbox(self, trace_obj: Any, traces: Any, skillbook: Any): + skillbook_text = "" + if skillbook is not None: + if isinstance(skillbook, str): + skillbook_text = skillbook + elif hasattr(skillbook, "as_prompt"): + skillbook_text = skillbook.as_prompt() or "(empty skillbook)" + else: + skillbook_text = str(skillbook) + + return self.create_sandbox( + trace=trace_obj, + variables={ + "traces": traces, + "skillbook": skillbook_text or "(empty skillbook)", + }, + ) + + def _build_data_summary(self, traces: Any) -> str: + if not isinstance(traces, dict): + return ( + f"### Data Summary\n" + f"- **Trace type**: {type(traces).__name__}\n" + f'- **Preview**: "{_preview(str(traces), 200)}"' + ) + + steps = traces.get("steps", []) + question = traces.get("question", "") + feedback = traces.get("feedback", "") + ground_truth = traces.get("ground_truth", "") + + lines = ["### Data Summary"] + trace_size_chars = len(_json.dumps(traces, default=str)) + if feedback: + lines.append(f"- **Feedback**: {_preview(feedback, 200)}") + if ground_truth: + lines.append(f"- **Ground truth**: {_preview(ground_truth, 200)}") + lines.append(f"- **Steps**: {len(steps)}") + if question: + lines.append(f"- **Task**: {_preview(question, 200)}") + + messages = traces.get("messages", []) + if messages: + lines.append(f"- **Messages**: {len(messages)} conversation turns") + tool_calls = sum( + 1 for m in messages if isinstance(m, dict) and m.get("tool_calls") + ) + if tool_calls: + lines.append(f"- **Tool calls**: {tool_calls}") + if len(messages) <= 50 and trace_size_chars <= 50_000: + lines.append( + "- **Expected effort**: small trace — use 2-4 focused " + "execute_code checks, then write the final ReflectorOutput. " + "Do not produce a transcript walkthrough." + ) + elif trace_size_chars <= 50_000: + lines.append( + "- **Expected effort**: small trace — use 2-4 focused " + "execute_code checks, then write the final ReflectorOutput." + ) + + return "\n".join(lines) + + def _build_initial_prompt(self, traces: Any, skillbook: Any) -> str: + trace_size_chars = len(_json.dumps(traces, default=str)) + + skillbook_text = "" + if skillbook is not None: + if isinstance(skillbook, str): + skillbook_text = skillbook + elif hasattr(skillbook, "as_prompt"): + skillbook_text = skillbook.as_prompt() or "" + else: + skillbook_text = str(skillbook) + + if isinstance(traces, dict): + traces_description = f"Dict with keys: {', '.join(sorted(traces.keys()))}" + elif isinstance(traces, list): + traces_description = f"List of {len(traces)} items" + else: + traces_description = f"Object of type {type(traces).__name__}" + + return self.prompt_template.format( + traces_description=traces_description, + trace_size_chars=trace_size_chars, + skillbook_length=len(skillbook_text), + max_iterations=self.config.max_requests, + data_summary=self._build_data_summary(traces), + ) + + # ------------------------------------------------------------------ + # Timeout / error fallback + # ------------------------------------------------------------------ + + def _build_timeout_output( + self, + question: str, + agent_output: Optional[AgentOutput], + ground_truth: Optional[str], + feedback: Optional[str], + deps: RRDeps, + ) -> ReflectorOutput: + is_correct = False + if ground_truth and agent_output: + is_correct = ( + agent_output.final_answer.strip().lower() + == ground_truth.strip().lower() + ) + + return ReflectorOutput( + reasoning=( + f"Recursive analysis reached budget limit. " + f"Basic analysis: Answer was " + f"{'correct' if is_correct else 'incorrect'}." + ), + error_identification="timeout" if not is_correct else "none", + root_cause_analysis="Analysis incomplete due to budget limit", + correct_approach=("Consider increasing budget or simplifying the analysis"), + key_insight=( + "Complex traces may require more budget for thorough analysis" + ), + raw={ + "timeout": True, + "question": question, + "feedback": feedback, + "thoughts": list(deps.thoughts), + "rr_trace": { + "total_iterations": deps.iteration, + "subagent_calls": [], + "timed_out": True, + }, + }, + ) + + +__all__ = [ + "RRConfig", + "RRDeps", + "RRStep", + "ExecutionResult", + "ExecutionTimeoutError", + "TraceSandbox", +] diff --git a/ace/steps/update.py b/ace/steps/update.py new file mode 100644 index 0000000000000000000000000000000000000000..9fe8f45a252bf0a2579156415600db95cb073773 --- /dev/null +++ b/ace/steps/update.py @@ -0,0 +1,80 @@ +"""UpdateStep — runs the SkillManager, which mutates the skillbook in place.""" + +from __future__ import annotations + +from ..core.context import ACEStepContext +from ..core.insight_source import InsightSource, infer_trace_identity +from ..core.skillbook import Skillbook +from ..protocols import SkillManagerLike + + +class UpdateStep: + """Run the agentic SkillManager against the current reflection. + + The SkillManager mutates the real :class:`Skillbook` directly through + its tools (``add_skill`` / ``update_skill`` / ``remove_skill`` / + ``tag_skill``). By the time this step returns the skillbook already + reflects the changes — ``ctx.skill_manager_output`` is a post-hoc + audit log, not a plan to apply. + + ``max_workers = 1`` because the SM reads the current skillbook state + and mutates it; concurrent calls would see stale state and race on + writes. + """ + + requires = frozenset({"reflections", "skillbook"}) + provides = frozenset({"skill_manager_output"}) + + max_workers = 1 + + def __init__(self, skill_manager: SkillManagerLike, skillbook: Skillbook) -> None: + self.skill_manager = skill_manager + self.skillbook = skillbook + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + progress = f"Epoch {ctx.epoch}/{ctx.total_epochs}" + if ctx.total_steps is not None: + progress += f", sample {ctx.step_index}/{ctx.total_steps}" + + sample = getattr(ctx, "sample", None) + sample_question = getattr(sample, "question", "") or "" + sample_context = getattr(sample, "context", "") or "" + + question_context = "" + if isinstance(ctx.trace, dict): + q = str(ctx.trace.get("question", "") or "") + c = str(ctx.trace.get("context", "") or "") + question_context = f"{q}\n{c}".strip() if c else q + elif sample_question: + question_context = ( + f"{sample_question}\n{sample_context}".strip() + if sample_context + else sample_question + ) + + identity = infer_trace_identity( + trace=ctx.trace, + sample=sample, + metadata=ctx.metadata, + ) + reflection = ctx.reflections[0] + source = InsightSource( + trace_uid=identity.trace_uid or "", + source_system=identity.source_system, + trace_id=identity.trace_id, + display_name=identity.display_name, + sample_question=sample_question or None, + epoch=ctx.epoch, + error_identification=reflection.error_identification or None, + learning_text=reflection.key_insight or None, + ) + + output = self.skill_manager.update_skills( + reflections=ctx.reflections, + skillbook=self.skillbook, + question_context=question_context, + progress=progress, + source=source, + injected_skill_ids=ctx.injected_skill_ids, + ) + return ctx.replace(skill_manager_output=output.update) diff --git a/ace/tracing/__init__.py b/ace/tracing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c681d60ebb4666964e97e32d84dc0b5d405406e6 --- /dev/null +++ b/ace/tracing/__init__.py @@ -0,0 +1,39 @@ +"""Kayba tracing — instrument your agents and send traces to Kayba. + +This module re-exports from the standalone ``kayba-tracing`` package. +Both import paths are supported:: + + # Standalone package + from kayba_tracing import configure, trace, start_span + + # Via ace-framework + from ace.tracing import configure, trace, start_span + +Requires the ``tracing`` extra:: + + pip install ace-framework[tracing] +""" + +from kayba_tracing import ( + configure, + disable, + enable, + get_folder, + get_trace, + search_traces, + set_folder, + start_span, + trace, +) + +__all__ = [ + "configure", + "disable", + "enable", + "get_folder", + "get_trace", + "search_traces", + "set_folder", + "start_span", + "trace", +] diff --git a/ace/tracing/_wrapper.py b/ace/tracing/_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..440e3f409082c834f4ff054506b6e60157f97ed3 --- /dev/null +++ b/ace/tracing/_wrapper.py @@ -0,0 +1,228 @@ +"""Thin Kayba-branded wrapper around MLflow tracing. + +All public symbols re-export MLflow functionality so that users never +need to ``import mlflow`` directly. The :func:`configure` helper sets +the MLflow tracking URI and auth to point at the Kayba backend. +""" + +from __future__ import annotations + +import functools +import os +import re +from contextlib import contextmanager +from typing import Any, Callable, Generator, TypeVar, overload + +_TRACING_INSTALL_HINT = ( + "Tracing requires the 'tracing' extra: pip install ace-framework[tracing]" +) + +try: + import mlflow + import mlflow.tracing # noqa: F401 — ensure tracing sub-module is loaded +except ImportError as exc: + raise ImportError(_TRACING_INSTALL_HINT) from exc + +DEFAULT_BASE_URL = "https://use.kayba.ai" + +# Module-level state set by configure() / set_folder(). +_folder: str | None = None + +_MAX_FOLDER_LENGTH = 256 +_SAFE_FOLDER_RE = re.compile(r"[^a-zA-Z0-9 _\-/.]") + + +def _sanitize_folder(name: str) -> str: + """Sanitize a folder name to prevent injection attacks. + + Strips control characters, HTML tags, and characters outside an + allowlist. Truncates to ``_MAX_FOLDER_LENGTH``. + """ + # Strip HTML tags. + clean = re.sub(r"<[^>]*>", "", name) + # Remove anything outside the safe set. + clean = _SAFE_FOLDER_RE.sub("", clean) + return clean.strip()[:_MAX_FOLDER_LENGTH] + + +_P = TypeVar("_P") +_R = TypeVar("_R") + + +def configure( + *, + api_key: str | None = None, + base_url: str | None = None, + experiment: str | None = None, + folder: str | None = None, +) -> None: + """Configure Kayba tracing. + + Sets the MLflow tracking URI and authentication so that all + subsequent ``@trace`` / ``start_span`` calls export to Kayba. + + Args: + api_key: Kayba API key. Falls back to ``KAYBA_API_KEY`` env var. + base_url: Kayba API base URL. Falls back to ``KAYBA_API_URL`` env + var, then to ``https://use.kayba.ai``. + experiment: Alias for ``folder``. If both are provided, ``folder`` + takes precedence. + folder: Optional folder name. Traces will be filed into this + folder in the Kayba dashboard. + """ + global _folder + + resolved_key = api_key or os.environ.get("KAYBA_API_KEY", "") + if not resolved_key: + raise ValueError( + "No API key provided. Pass api_key= or set the KAYBA_API_KEY " + "environment variable." + ) + + resolved_url = base_url or os.environ.get("KAYBA_API_URL") or DEFAULT_BASE_URL + # Strip trailing slash, then append the MLflow-compatible mount path. + tracking_uri = resolved_url.rstrip("/") + "/api/mlflow" + + # Configure MLflow under the hood. + os.environ["MLFLOW_TRACKING_TOKEN"] = resolved_key + mlflow.set_tracking_uri(tracking_uri) + + resolved_folder = folder or experiment + _folder = _sanitize_folder(resolved_folder) or None if resolved_folder else None + + +def set_folder(folder: str | None) -> None: + """Change the target folder for subsequent traces. + + Args: + folder: Folder name, or ``None`` to clear (traces go to Unfiled). + """ + global _folder + _folder = _sanitize_folder(folder) or None if folder else None + + +def get_folder() -> str | None: + """Return the currently configured folder, or ``None``.""" + return _folder + + +# --------------------------------------------------------------------------- +# Wrapped MLflow tracing primitives that inject the folder tag +# --------------------------------------------------------------------------- + + +def _inject_folder_tag() -> None: + """Inject ``kayba.folder`` tag into the active trace if a folder is set.""" + if _folder is not None: + mlflow.update_current_trace(tags={"kayba.folder": _folder}) + + +@overload +def trace(func: Callable[..., _R]) -> Callable[..., _R]: ... + + +@overload +def trace( + func: None = None, + *, + name: str | None = None, + span_type: str = ..., + attributes: dict[str, Any] | None = None, +) -> Callable[[Callable[..., _R]], Callable[..., _R]]: ... + + +def trace( + func: Callable[..., Any] | None = None, + *, + name: str | None = None, + span_type: str = "UNKNOWN", + attributes: dict[str, Any] | None = None, +) -> Any: + """Decorator that creates a trace span for the decorated function. + + Works identically to ``mlflow.trace`` but automatically tags + the trace with the configured Kayba folder. + """ + + def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: + # Wrap the original function so the folder tag is injected + # *inside* the trace context (before MLflow closes it). + @functools.wraps(fn) + def fn_with_tag(*args: Any, **kwargs: Any) -> Any: + result = fn(*args, **kwargs) + _inject_folder_tag() + return result + + # Let MLflow handle the actual tracing. + mlflow_kwargs: dict[str, Any] = {} + if name is not None: + mlflow_kwargs["name"] = name + if span_type != "UNKNOWN": + mlflow_kwargs["span_type"] = span_type + if attributes is not None: + mlflow_kwargs["attributes"] = attributes + + if mlflow_kwargs: + traced = mlflow.trace(**mlflow_kwargs)(fn_with_tag) + else: + traced = mlflow.trace(fn_with_tag) + + @functools.wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> Any: + return traced(*args, **kwargs) + + return wrapper + + if func is not None: + # Called as @trace without parentheses. + return decorator(func) + return decorator + + +@contextmanager +def start_span( + name: str = "span", + span_type: str | None = "UNKNOWN", + attributes: dict[str, Any] | None = None, +) -> Generator[Any, None, None]: + """Context manager that creates a child span. + + Works identically to ``mlflow.start_span`` but automatically tags + the trace with the configured Kayba folder when used as a root span. + """ + with mlflow.start_span( + name=name, span_type=span_type, attributes=attributes + ) as span: + yield span + # Inject folder tag while the trace context is still open. + _inject_folder_tag() + + +# --------------------------------------------------------------------------- +# Utility functions +# --------------------------------------------------------------------------- + + +def enable() -> None: + """Enable Kayba tracing (enabled by default after :func:`configure`).""" + mlflow.tracing.enable() + + +def disable() -> None: + """Disable Kayba tracing without removing the configuration.""" + mlflow.tracing.disable() + + +def get_trace(trace_id: str) -> Any: + """Retrieve a trace by ID.""" + return mlflow.get_trace(trace_id) + + +def search_traces( + experiment_names: list[str] | None = None, + **kwargs: Any, +) -> Any: + """Search for traces, optionally filtered by experiment names.""" + if experiment_names is None: + experiment_names = ["Default"] + return mlflow.search_traces(experiment_names=experiment_names, **kwargs) diff --git a/agent-guides/logfire.md b/agent-guides/logfire.md new file mode 100644 index 0000000000000000000000000000000000000000..2954705bde43de95e93145c8a09b872e8c597362 --- /dev/null +++ b/agent-guides/logfire.md @@ -0,0 +1,404 @@ +# Logfire Trace Querying + +Use this guide when you need to inspect or analyze traces already collected in +the `kayba/ace` Logfire project. + +This is an operational guide for coding agents and scripts. It focuses on how +to authenticate, query the Logfire API, and safely turn the API response into +trace records you can analyze locally. + +## When To Use This Guide + +Read this guide before you: + +- inspect the latest production or benchmark traces in Logfire +- fetch a specific trace by `trace_id` +- summarize failures, exceptions, latency, or model usage from Logfire data +- export collected traces from Logfire into local analysis code + +If you are adding runtime instrumentation, also read the existing Logfire +observability notes in `ace/observability/__init__.py` and the Claude SDK guide. + +## Project And Credentials + +The Logfire project used in this repository is: + +- organization: `kayba` +- project: `ace` + +### Authentication Rules + +The Logfire query API officially requires a **read token**. + +Preferred auth order: + +1. `LOGFIRE_READ_TOKEN` if it is already available +2. a fresh read token created for `kayba/ace` +3. `LOGFIRE_TOKEN` from the repo-local `.env` only as a bootstrap path when you + need to create or recover a read token + +Do not assume `LOGFIRE_TOKEN` can query traces. In this repository it is +usually the project write token used by the SDK for emission, and the query API +will reject it with `401 Invalid token`. + +Do not hardcode token values into source files, docs, tests, or shell history. +Do not print full tokens in logs or user-facing output. + +### Repo-Local `.env` + +This repository commonly stores Logfire credentials in the repo-root `.env`. +If the current shell does not already have `LOGFIRE_TOKEN`, load `.env` first. + +Python: + +```python +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv(Path(".env")) +``` + +Shell: + +```bash +set -a +source .env +set +a +``` + +## Endpoint And Region Selection + +Preferred query endpoint: + +- `https://logfire-api.pydantic.dev/v1/query` + +Regional endpoints also work: + +- US: `https://logfire-us.pydantic.dev/v1/query` +- EU: `https://logfire-eu.pydantic.dev/v1/query` + +Logfire tokens encode the region. If you need to derive the regional URL, the +token format is typically `pylf_v<version>_<region>_<secret>`, where `<region>` +is usually `us` or `eu`. + +## Recommended Access Pattern In This Repo + +Inside this repository, prefer **direct HTTP requests from `.venv/bin/python`** +over the Logfire CLI when running in a sandboxed agent environment. + +Reasons: + +- the Logfire CLI writes logs under `~/.logfire/`, which may be blocked +- `uv run ...` may try to use `~/.cache/uv/`, which may also be blocked +- direct HTTP requests with `requests` are simpler and more predictable + +If you must use `uv` in a restricted sandbox, set: + +```bash +UV_CACHE_DIR=/tmp/uv-cache +``` + +## Query API Basics + +The query API accepts SQL against Logfire tables such as `records`. + +Minimal HTTP shape: + +```http +GET /v1/query?sql=SELECT%20... HTTP/1.1 +Authorization: Bearer <read-token> +Accept: application/json +``` + +Use `GET` for the query API in this repo. `POST /v1/query` currently returns +`405 Method Not Allowed`. + +Useful query parameters: + +- `sql`: required SQL query +- `limit`: response row cap, default `500`, maximum `10000` +- `min_timestamp`: optional lower time bound +- `max_timestamp`: optional upper time bound +- `row_oriented`: may be accepted by the API, but agents in this repo should not + rely on it to change the payload shape + +Official Logfire query API docs: + +- <https://logfire.pydantic.dev/docs/how-to-guides/query-api/> + +## Important Response Shape + +Treat Logfire responses as **column-oriented JSON**, not as a list of row dicts. +Even when `row_oriented` is provided, the safe assumption in this repo is still +that the payload will come back in `columns`. + +Example shape: + +```json +{ + "columns": [ + {"name": "trace_id", "values": ["abc", "def"]}, + {"name": "message", "values": ["agent run", "chat ..."]} + ] +} +``` + +Convert it to rows before analysis: + +```python +def columns_to_rows(payload: dict) -> list[dict]: + columns = payload["columns"] + names = [col["name"] for col in columns] + values = [col["values"] for col in columns] + return [dict(zip(names, row)) for row in zip(*values)] +``` + +Do not assume `response.json()` is already a list. + +## Canonical Python Snippet + +Use this as the default pattern for agents. + +```python +from __future__ import annotations + +import os +from pathlib import Path + +import requests +from dotenv import load_dotenv + +load_dotenv(Path(".env")) + +READ_TOKEN = os.environ.get("LOGFIRE_READ_TOKEN") +if not READ_TOKEN: + raise RuntimeError("LOGFIRE_READ_TOKEN is required for Logfire queries") + +BASE_URL = "https://logfire-api.pydantic.dev" + + +def query_logfire(sql: str, *, limit: int = 1000) -> list[dict]: + resp = requests.get( + f"{BASE_URL}/v1/query", + params={"sql": sql, "limit": limit}, + headers={ + "Authorization": f"Bearer {READ_TOKEN}", + "Accept": "application/json", + }, + timeout=30, + ) + resp.raise_for_status() + payload = resp.json() + columns = payload["columns"] + names = [col["name"] for col in columns] + values = [col["values"] for col in columns] + return [dict(zip(names, row)) for row in zip(*values)] +``` + +## High-Value Queries + +### 1. Latest Root Traces + +Start here when the user asks for the latest traces. + +```sql +SELECT + start_timestamp, + trace_id, + span_id, + service_name, + message, + span_name, + level, + duration +FROM records +WHERE parent_span_id IS NULL +ORDER BY start_timestamp DESC +LIMIT 20 +``` + +Notes: + +- root traces usually have `parent_span_id IS NULL` +- in this project, root messages are often `agent run` for PydanticAI flows +- Tau benchmark runs now emit explicit benchmark spans such as `benchmark run` + and `tau task run` + +### 2. Full Trace By `trace_id` + +Use this after identifying a trace worth inspecting. + +```sql +SELECT + start_timestamp, + span_id, + parent_span_id, + message, + span_name, + level, + duration, + service_name +FROM records +WHERE trace_id = '<TRACE_ID>' +ORDER BY start_timestamp ASC +LIMIT 500 +``` + +### 3. Exceptions Inside One Trace + +Use this to separate top-level failures from recoverable tool retries. + +```sql +SELECT + start_timestamp, + message, + span_name, + level, + exception_type, + exception_message +FROM records +WHERE trace_id = '<TRACE_ID>' AND is_exception = true +ORDER BY start_timestamp ASC +LIMIT 100 +``` + +### 4. Quick Trace Stats + +Use this for a compact summary before drilling deeper. + +```sql +SELECT + COUNT(*) AS record_count, + SUM(CASE WHEN is_exception THEN 1 ELSE 0 END) AS exception_count, + MIN(start_timestamp) AS first_seen, + MAX(start_timestamp) AS last_seen +FROM records +WHERE trace_id = '<TRACE_ID>' +``` + +### 5. Model Usage Within A Trace + +Useful when RR or sub-agent activity is suspected. + +```sql +SELECT + span_name, + COUNT(*) AS call_count, + SUM(duration) AS total_duration +FROM records +WHERE trace_id = '<TRACE_ID>' +GROUP BY span_name +ORDER BY total_duration DESC +LIMIT 50 +``` + +## How To Read Common Patterns + +Typical messages you may see: + +- `agent run`: root span for a PydanticAI run +- `benchmark run`: explicit root span for ace-eval benchmark execution +- `benchmark trial`: one benchmark trial within a benchmark run +- `tau task run`: one TauBench task execution; check attributes such as + `run_phase`, `task_index`, `task_id`, and `skillbook_injected` +- `chat <model>`: one LLM call +- `running tool: execute_code`: sandbox code execution +- `running tool: batch_analyze`: batch semantic analysis + +Interpretation guidance: + +- a trace with `0` exceptions is usually a clean run, but still inspect duration + and child spans if the user asked for performance analysis +- `ToolRetryError` inside `execute_code` often means the RR sandbox made a bad + attempt and retried; this is not automatically a top-level pipeline failure +- `UsageLimitExceeded` indicates an internal request budget or tool budget issue, + not necessarily a transport failure +- long traces with many nested `agent run` spans where `agent_name = sub` often + indicate recursive reflection or batch analysis behavior +- if a user asks about benchmark behavior, start from `benchmark run` or + `tau task run` spans instead of expecting PydanticAI `agent run` traces + +## Safety And Operational Rules + +- Always keep SQL narrow. Add `LIMIT`, and add time filters when possible. +- Prefer starting from root traces, then drill into one `trace_id`. +- Never paste secret tokens into committed docs, code, or logs. +- Never claim the “latest” trace without actually querying Logfire first. +- When reporting times to users, include the full UTC timestamp. +- If the sandbox blocks network access, request escalation instead of guessing. + +## Troubleshooting + +### `401 Unauthorized` + +Likely causes: + +- token is expired +- token is the wrong token type +- token belongs to the wrong project or region +- you are trying to use `LOGFIRE_TOKEN` instead of `LOGFIRE_READ_TOKEN` + +Action: + +- use a valid read token for `kayba/ace` +- prefer the global API endpoint if region selection is unclear + +### `429 Too Many Requests` + +Likely causes: + +- several query requests were fired back-to-back while drilling into the same trace +- one large query pulled too many records or wide JSON attributes + +Action: + +- pause briefly and retry with fewer queries +- prefer one narrow query over several broad exploratory ones +- fetch counts first, then ordered records for a single `trace_id` + +### `200 OK` But No Rows + +Likely causes: + +- wrong project +- query window is too narrow +- data is older than the default filtered range in a helper you are using + +Action: + +- remove accidental timestamp filters +- query root traces first +- widen the time range explicitly + +### CLI Fails In Sandbox + +Likely causes: + +- `logfire` CLI trying to write under `~/.logfire/` +- `uv` trying to write under `~/.cache/uv/` + +Action: + +- prefer `.venv/bin/python` plus `requests` +- if `uv` is required, set `UV_CACHE_DIR=/tmp/uv-cache` + +## Recommended Workflow For Agents + +When asked to inspect the latest traces: + +1. Load `.env` if needed. +2. Confirm you have a valid read token path. +3. Query the latest root traces. +4. Pick the newest interesting `trace_id`. +5. Fetch trace stats. +6. Fetch exception rows. +7. Fetch ordered records for that trace. +8. Summarize: + - exact UTC timestamp + - total duration + - record count + - exception count + - model/tool pattern + - likely failure mode + +This is the default procedure for “look at the latest traces in Logfire”. diff --git a/agent-guides/plan-sm-rewrite-v3.md b/agent-guides/plan-sm-rewrite-v3.md new file mode 100644 index 0000000000000000000000000000000000000000..c653d6a454623640933ebe780ff47896141ea234 --- /dev/null +++ b/agent-guides/plan-sm-rewrite-v3.md @@ -0,0 +1,94 @@ +# ACE SkillManager Rewrite — Plan v3 + +Semi-temporary planning doc. Delete once PR 3 lands and is merged. + +## PR 1 — Substrate + +**`ace/core/context.py:95`** — add field to `ACEStepContext`: +- `injected_skill_ids: tuple[str, ...] = ()` + +**`ace/core/skillbook.py:226`** — add counters to `Skill`: +- `used_count: int = 0` +- `helpful_count: int = 0` +- `harmful_count: int = 0` +- `neutral_count: int = 0` + +**`ace/core/skillbook.py`** — add `Skillbook.tag_skill(skill_id, delta: Literal[+1, -1, 0])` method. Keep the `TAG` branch in `_apply_operation` as its implementation so serialized `UpdateOperation(TAG)` still works. + +**`ace/core/skillbook.py:535`** — `as_prompt()` stays unchanged (no counter rendering). + +**Agent step (`ace/implementations/agent.py:96`)** — after rendering the skillbook into the prompt, write `injected_skill_ids` onto the context and bump `used_count` on each. This is the only upstream counter touched. + +**Remove citation plumbing:** +- `ace/core/outputs.py:52` — delete `SkillTag`. +- `ace/core/outputs.py:79` — delete `ReflectorOutput.skill_tags`. +- `ace/implementations/skill_manager.py:109-112` — remove the `skill_tags` consumer block. + +**Docs:** +- `docs/design/ACE_ARCHITECTURE.md` — update Skill section (counters, injection-based attribution). +- `docs/design/ACE_DECISIONS.md` — add "Injection is ground truth; citation dropped." + +## PR 2 — Reflector & RR prompts + +**ReflectorOutput stays pure analysis** — `reasoning`, `error_identification`, `root_cause_analysis`, `correct_approach`, `key_insight`. No tagging fields, no `harmful_ids`/`helpful_ids`. + +**Reflector prompt (`ace/implementations/prompts.py:419-432`)** — delete `REFLECTOR_SKILL_EVAL_SECTION` entirely. + +**RR prompt (`ace/implementations/rr/prompts.py:115-136`)** — delete the `re.findall` citation recipe. No replacement. RR may still be told "inspect the skillbook (covered / contradicted / gap) and narrate what you find" as analysis guidance — never as a decision. + +**Two read-only RR tools** (so RR can enrich its narrative without citations): +- `search_skillbook(query, top_k)` → wraps `retrieve_top_k`. +- `read_skill(id)` → `Skillbook.get_skill`. Return value includes counters. + +**`ace/steps/rr_step.py`:** +- `_build_traces_dict` (line 267) — drop `skill_ids`; add `injected_skill_ids` from context. +- Line 399 — remove the `skill_tags=[]` remnant in the timeout fallback. + +**RR batch input preserved** at the caller level — contract unchanged. + +## PR 3 — Rewrite SkillManager (same class) + +Rewrite `SkillManager` on top of `RecursiveAgent`. Same class name, same `SkillManagerLike` protocol, same `UpdateStep` wrapper — nothing upstream changes. + +**Mutation tools** (operate on the real `Skillbook`, no staging): +- `add_skill(section, content, justification, evidence)` → `Skillbook.add_skill`. +- `update_skill(skill_id, content?, justification?, evidence?)` → `Skillbook.update_skill`. +- `remove_skill(skill_id, reason)` → `Skillbook.remove_skill`. +- `tag_skill(skill_id, delta: +1 | -1 | 0)` → `Skillbook.tag_skill`. + +**Read-only tools:** +- `search_skills(query, top_k)` → returns skills with counters. +- `read_skill(id)` → returns skill with counters. + +**Sandbox:** +- `sandbox_eval(code)` — reuse `register_execute_code`; opt-in per runner. + +**Termination:** +- `finalize(reasoning)` → terminates the loop; returns `SkillManagerOutput` as an audit log of what was done, not a plan to be applied. + +**Delete `ApplyStep`** (`ace/steps/apply.py:10`) and remove it from the ACE pipeline composition. `UpdateStep` is the sole SM invocation and the skillbook is already mutated when it returns. `UpdateStep.max_workers=1` stays. + +**`SkillManagerOutput` shape** — `reasoning: str` + `operations: list[UpdateOperation]`. Operations become a post-hoc audit trail. Same dataclass, same serialization; semantic shift only. + +**`UpdateBatch` / `apply_update` / `_apply_operation`** remain for offline reconstruction and tests, but the online path no longer flows through them. + +**SM prompt (`ace/implementations/prompts.py:439+`):** +- Drop `<skill_effectiveness>` section (lines 505-513). +- Instruct the SM that it decides helpful/harmful/neutral from `injected_skill_ids` + outcome + reflection. +- Instruct the SM to REMOVE skills whose `harmful_count ≥ N` when it encounters them during investigation. Counters are surfaced via `read_skill` / `search_skills`, not the rendered skillbook. + +**`AgenticConfig(max_requests=…)`** — `max_requests=1` degrades to a single-tool-call pass. + +**Docs:** +- `docs/design/ACE_ARCHITECTURE.md` — SM section (tools, direct mutation, no ApplyStep). +- `docs/design/ACE_REFERENCE.md` — tool surface. +- `docs/design/ACE_DECISIONS.md` — "SM mutates directly; Reflector is analysis-only." + +## Out of scope +- `RetrieveStep` / production top-k injection. +- Sandbox-gated commit gating (Voyager-style verify-before-ADD). +- Counter decay / windowing. +- Global "sweep all skills with harmful_count ≥ N" tool. + +## Ship order +PR 1 → PR 2 → PR 3. diff --git a/agent-guides/tracing-sdk.md b/agent-guides/tracing-sdk.md new file mode 100644 index 0000000000000000000000000000000000000000..fa1a0944499b4767c47b66dc764479dce167376c --- /dev/null +++ b/agent-guides/tracing-sdk.md @@ -0,0 +1,143 @@ +# Kayba Tracing SDK + +Use this guide when you need to instrument agent code with Kayba tracing. + +## When To Use This Guide + +Read this guide before you: + +- add tracing to new or existing agent code +- create examples that send traces to Kayba +- debug why traces are not appearing in the dashboard + +## Module Location + +All tracing code lives in `ace/tracing/`. The public API is re-exported from +`ace/tracing/__init__.py`. The implementation is in `ace/tracing/_wrapper.py`. + +## Installation + +Tracing requires the optional `tracing` extra: + +```bash +pip install ace-framework[tracing] +``` + +This pulls in `mlflow` as the underlying tracing backend. + +## Configuration + +```python +from ace.tracing import configure + +configure( + api_key="...", # or set KAYBA_SDK_KEY / KAYBA_API_KEY env var + base_url="...", # optional, defaults to https://use.kayba.ai + experiment="my-exp", # optional MLflow experiment name + folder="production", # optional dashboard folder +) +``` + +The `configure()` function sets the MLflow tracking URI to +`{base_url}/api/mlflow` and stores the API key in +`MLFLOW_TRACKING_TOKEN`. + +### Environment Variables + +| Variable | Purpose | +|----------|---------| +| `KAYBA_SDK_KEY` or `KAYBA_API_KEY` | API key (alternative to `api_key=`) | +| `KAYBA_API_URL` | Base URL override | + +## Core API + +### `@trace` decorator + +Wraps a function to create a trace span. Supports bare and parameterized forms: + +```python +from ace.tracing import trace + +@trace +def my_agent(query: str) -> str: ... + +@trace(name="custom", span_type="LLM", attributes={"model": "glm-4-plus"}) +def llm_call(messages): ... +``` + +### `start_span()` context manager + +Creates a child span within an active trace: + +```python +from ace.tracing import start_span + +with start_span("retrieval") as span: + span.set_inputs({"query": query}) + results = search(query) + span.set_outputs({"count": len(results)}) +``` + +### Other functions + +- `set_folder(name)` / `get_folder()` — change/read the dashboard folder +- `enable()` / `disable()` — toggle tracing on/off +- `get_trace(trace_id)` — fetch a trace by ID +- `search_traces(experiment_names=[...])` — search traces + +## Using with OpenAI-Compatible Endpoints + +The tracing SDK is LLM-agnostic. Use any OpenAI-compatible client (Zhipu GLM, +vLLM, Ollama, LiteLLM, etc.) and wrap calls with `@trace`: + +```python +from openai import OpenAI +from ace.tracing import configure, trace + +configure(api_key=os.environ["KAYBA_SDK_KEY"]) + +client = OpenAI( + base_url=os.environ["OPENAI_BASE_URL"], + api_key=os.environ["OPENAI_API_KEY"], +) + +@trace(name="llm_call", span_type="LLM") +def llm_call(messages): + return client.chat.completions.create( + model="glm-5.1", + messages=messages, + ).choices[0].message.content +``` + +The `OPENAI_BASE_URL` in `.env` points to `https://api.z.ai/api/coding/paas/v4` +(Zhipu AI). Any model served there (e.g. `glm-4-plus`) works. + +## Span Nesting + +Decorated functions called within other decorated functions produce a nested +trace tree automatically: + +``` +@trace pipeline +├── @trace research_agent +│ ├── start_span("build_prompt") +│ └── @trace llm_call +└── @trace summariser_agent + ├── start_span("build_prompt") + └── @trace llm_call +``` + +## Current Limitations + +- **No async support**: the `@trace` decorator only wraps sync functions. Async + functions will return a coroutine instead of awaiting it. +- **No cross-process context propagation**: each `@trace` root creates an + independent trace. There is no mechanism to link traces across agents running + in separate processes. +- **No agent identity tagging**: spans are not automatically tagged with an + agent name or ID. + +## Example + +See `examples/tracing_glm_example.py` for a full runnable two-agent pipeline +(research + summarise) instrumented with the tracing SDK. diff --git a/assets/kayba-banner.png b/assets/kayba-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..c1ca458856a346dd7719b148eb81e56b36c538b4 --- /dev/null +++ b/assets/kayba-banner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70b7ada3643aa102cc4beeb8a6de6a92b22e53267885b1b99833bead64977c75 +size 113909 diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000000000000000000000000000000000000..013992d2cd270c0a3cee09e831339d5433c50686 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,140 @@ +# ACE Benchmarks + +Evaluate ACE performance with scientific rigor using our comprehensive benchmark suite. + +This evaluation framework tests Agentic Context Engineering (ACE) across multiple datasets with automatic metrics, train/test splits, and overfitting analysis to ensure honest performance measurements. + +## Quick Start + +```bash +# List available benchmarks +uv run python scripts/run_benchmark.py list + +# Run ACE evaluation with train/test split (default) +uv run python scripts/run_benchmark.py finer_ord --limit 100 + +# Run baseline only (no ACE learning) +uv run python scripts/run_benchmark.py simple_qa --limit 50 --skip-adaptation + +# Compare baseline vs ACE side-by-side +uv run python scripts/run_benchmark.py hellaswag --limit 50 --compare +``` + +## Available Benchmarks + +| Benchmark | Description | Domain | Default Limit | +|-----------|-------------|---------|---------------| +| **finer_ord** | Financial Named Entity Recognition | Finance | 100 | +| **simple_qa** | Question Answering (SQuAD) | General | 200 | +| **simple_math** | Math Word Problems (GSM8K) | Mathematics | 100 | +| **mmlu** | Massive Multitask Language Understanding | General Knowledge | 500 | +| **hellaswag** | Commonsense Reasoning | Common Sense | 200 | +| **arc_easy** | AI2 Reasoning Challenge (Easy) | Reasoning | 200 | +| **arc_challenge** | AI2 Reasoning Challenge (Hard) | Reasoning | 200 | + +## Command Options + +```bash +uv run python scripts/run_benchmark.py <benchmark> [options] +``` + +**Key Options:** +- `--limit` - Override sample limit (always overrides config) +- `--model` - Model name (default: gpt-4o-mini) +- `--skip-adaptation` - Skip ACE learning (faster baseline) +- `--compare` - Run both baseline and ACE, then compare results +- `--epochs` - ACE adaptation epochs (default: 1) +- `--split-ratio` - Train/test split ratio (default: 0.8) +- `--online-mode` - Use continuous learning instead of offline +- `--prompt-version` - Use v1 or v2 prompts (default: v1) +- `--save-detailed` - Save per-sample results +- `--quiet` - Suppress progress output + +## Examples + +```bash +# Quick test with 10 samples +uv run python scripts/run_benchmark.py finer_ord --limit 10 --quiet + +# Compare baseline vs ACE +uv run python scripts/run_benchmark.py simple_qa --limit 50 --compare + +# Full ACE evaluation with v2 prompts +uv run python scripts/run_benchmark.py simple_qa --epochs 3 --prompt-version v2 --save-detailed + +# Online learning mode +uv run python scripts/run_benchmark.py hellaswag --limit 100 --online-mode + +# Custom train/test split (90/10) +uv run python scripts/run_benchmark.py mmlu --limit 100 --split-ratio 0.9 + +# Test all benchmarks quickly (baseline only) +for benchmark in finer_ord simple_qa hellaswag arc_easy; do + uv run python scripts/run_benchmark.py $benchmark --limit 5 --skip-adaptation --quiet +done +``` + +## Output + +Results saved to `benchmark_results/` with format: +- **Summary**: `{benchmark}_{model}_{timestamp}_summary.json` +- **Detailed**: `{benchmark}_{model}_{timestamp}_detailed.json` (if `--save-detailed`) + +## Adding Custom Benchmarks + +Create `benchmarks/tasks/my_benchmark.yaml`: + +```yaml +task: my_benchmark +version: "1.0" + +data: + source: huggingface + dataset_path: my/dataset + split: test + limit: 100 + +metrics: + - name: exact_match + weight: 1.0 + +metadata: + description: "My custom benchmark" + domain: "my_domain" +``` + +## Evaluation Modes + +The benchmark script supports three evaluation modes: + +1. **ACE Mode (default)**: Train/test split with learning + ```bash + uv run python scripts/run_benchmark.py simple_qa --limit 100 + ``` + +2. **Baseline Mode**: No learning, direct evaluation + ```bash + uv run python scripts/run_benchmark.py simple_qa --limit 100 --skip-adaptation + ``` + +3. **Comparison Mode**: Runs both baseline and ACE, shows improvement + ```bash + uv run python scripts/run_benchmark.py simple_qa --limit 100 --compare + ``` + +## Key Features + +- **Overfitting Prevention**: Automatic 80/20 train/test splits ensure true generalization metrics +- **Scientific Rigor**: Comprehensive evaluation modes with honest performance analysis +- **Multiple Domains**: Finance, general knowledge, reasoning, math, and common sense benchmarks +- **Flexible Configuration**: Customizable limits, models, and evaluation parameters +- **Performance Tracking**: Detailed results with per-sample analysis options + +## Notes + +- **Default 80/20 train/test split** prevents overfitting and shows true generalization +- The `--limit` parameter always overrides config file limits +- ACE adaptation improves performance through iterative learning +- Use `--compare` to see baseline vs ACE improvement side-by-side +- Overfitting warnings help identify when ACE memorizes vs generalizes +- Opik tracing warnings ("Failed to log adaptation metrics") are harmless \ No newline at end of file diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5502e62b0c0c37a4e2505b3cbc02cd7421cd8240 --- /dev/null +++ b/benchmarks/__init__.py @@ -0,0 +1,19 @@ +""" +Benchmark integration for ACE — currently TAU-bench only. + +Usage: + >>> from benchmarks.loaders.tau2 import Tau2Loader + >>> loader = Tau2Loader() + >>> for task in loader.load(domain="airline"): + ... print(task["task_id"]) +""" + +from .base import DataLoader +from .loaders.tau2 import Tau2Loader + +__all__ = [ + "DataLoader", + "Tau2Loader", +] + +__version__ = "0.1.0" diff --git a/benchmarks/base.py b/benchmarks/base.py new file mode 100644 index 0000000000000000000000000000000000000000..7d07900ab6ba5d19d8916d846c46ec03f7734992 --- /dev/null +++ b/benchmarks/base.py @@ -0,0 +1,22 @@ +""" +Base classes for benchmark data loading. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, Dict, Iterator + + +class DataLoader(ABC): + """Abstract base class for loading benchmark data from different sources.""" + + @abstractmethod + def load(self, **kwargs) -> Iterator[Dict[str, Any]]: + """Load benchmark data and yield individual samples.""" + pass + + @abstractmethod + def supports_source(self, source: str) -> bool: + """Check if this loader supports the given data source.""" + pass diff --git a/benchmarks/loaders/__init__.py b/benchmarks/loaders/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b1ce8ffe181c5d465440ee64fc1ce4f8d7c3a920 --- /dev/null +++ b/benchmarks/loaders/__init__.py @@ -0,0 +1,13 @@ +"""Data loaders for benchmark sources.""" + +from ..base import DataLoader + +__all__ = ["DataLoader"] + +# Tau2 loader is imported conditionally since tau2-bench might not be installed +try: + from .tau2 import Tau2Loader + + __all__.append("Tau2Loader") +except ImportError: + pass diff --git a/benchmarks/loaders/tau2.py b/benchmarks/loaders/tau2.py new file mode 100644 index 0000000000000000000000000000000000000000..6e9bfe0b1a601f2925977630a562b3eebc1d883b --- /dev/null +++ b/benchmarks/loaders/tau2.py @@ -0,0 +1,230 @@ +""" +TAU2-bench data loader for tool-calling agent evaluation. + +This module provides data loading from tau2-bench, a benchmark for evaluating +tool-calling agents in customer service domains (airline, retail, telecom). + +Setup Requirements: + 1. Install tau2: pip install ace-framework[tau-bench] + 2. Set TAU2_DATA_DIR environment variable to point to tau2 data directory + 3. Download data from: https://github.com/sierra-research/tau2-bench +""" + +from __future__ import annotations + +import logging +import os +from typing import Any, Dict, Iterator, List + +from ..base import DataLoader + +logger = logging.getLogger(__name__) + + +class Tau2Loader(DataLoader): + """ + Data loader for TAU2-bench (τ²-bench) tasks. + + TAU2-bench evaluates tool-calling agents in customer service domains: + - airline: Flight bookings, cancellations, seat changes + - retail: Order management, returns, product inquiries + - telecom: Account management, plan changes, billing + + Example: + >>> loader = Tau2Loader() + >>> for task in loader.load(domain="airline", task_split="base", limit=10): + ... print(task["task_id"], task["instruction"]) + + Setup: + 1. Install: pip install ace-framework[tau-bench] + 2. Clone data: git clone https://github.com/sierra-research/tau2-bench + 3. Set environment: export TAU2_DATA_DIR=/path/to/tau2-bench/data + """ + + def supports_source(self, source: str) -> bool: + """Check if this loader supports the given data source.""" + return source == "tau2" + + def load(self, **kwargs) -> Iterator[Dict[str, Any]]: + """ + Load TAU2-bench tasks for a specific domain. + + Args: + domain: Domain to load tasks from (airline, retail, telecom) + task_split: Task split to use (base, human, gpt4o) - for airline/retail + limit: Maximum number of tasks to load + **kwargs: Additional arguments (unused) + + Yields: + Dict containing task data: + - task_id: Unique task identifier + - instruction: Initial user instruction + - tools: List of available tool definitions + - user_llm: LLM model for user simulation + - domain: Domain name + - task_split: Split name + - metadata: Additional task metadata + + Raises: + ImportError: If tau2 is not installed + ValueError: If data directory not configured or tasks cannot be loaded + """ + try: + from tau2.registry import registry + except ImportError: + raise ImportError( + "tau2 is required for TAU2 loader. " + "Install with: pip install ace-framework[tau-bench]" + ) + + # Check if data directory is configured + data_dir = os.environ.get("TAU2_DATA_DIR") + if not data_dir: + raise ValueError( + "TAU2_DATA_DIR environment variable not set. " + "Please set it to point to the tau2-bench data directory. " + "Clone data from: https://github.com/sierra-research/tau2-bench" + ) + + domain = kwargs.get("domain", "airline") + task_split = kwargs.get("task_split", "base") + limit = kwargs.get("limit") + user_llm = kwargs.get("user_llm", "gpt-4o-mini") + + # Get tasks for the domain using the registry + try: + tasks = self._get_tasks_for_domain(registry, domain, task_split) + except FileNotFoundError as e: + raise ValueError( + f"Failed to load tasks for {domain}/{task_split}. " + f"Ensure TAU2_DATA_DIR points to valid tau2 data directory. " + f"Error: {e}" + ) + except Exception as e: + raise ValueError(f"Failed to get tasks for {domain}/{task_split}: {e}") + + if not tasks: + logger.warning(f"No tasks found for {domain}/{task_split}") + return + + # Apply limit if specified + if limit: + tasks = tasks[:limit] + + # Yield each task + for task in tasks: + try: + task_id = getattr(task, "id", str(id(task))) + + # Extract instruction from user_scenario + instruction = self._extract_instruction(task) + + # Get tools from the domain environment + tools = self._get_domain_tools(registry, domain) + + yield { + "task_id": task_id, + "instruction": instruction, + "tools": tools, + "user_llm": user_llm, + "domain": domain, + "task_split": task_split, + "task": task, # Store the full task object for gym + "metadata": { + "task_id": task_id, + "domain": domain, + "task_split": task_split, + "max_steps": 30, + }, + } + except Exception as e: + logger.warning(f"Failed to process task: {e}") + continue + + def _extract_instruction(self, task) -> str: + """Extract instruction text from a tau2 Task object.""" + # Try user_scenario.instructions.reason_for_call first + if hasattr(task, "user_scenario"): + scenario = task.user_scenario + if hasattr(scenario, "instructions"): + instr = scenario.instructions + if hasattr(instr, "reason_for_call") and instr.reason_for_call: + return str(instr.reason_for_call) + + # Fallback to description + if hasattr(task, "description") and task.description: + return str(task.description) + + return "" + + def _get_tasks_for_domain( + self, registry, domain: str, task_split: str + ) -> List[Any]: + """Get tasks for a domain using the appropriate registry method.""" + # Get the task loader function for this domain + tasks_loader = registry.get_tasks_loader(domain) + + # Load tasks with optional split + if task_split and task_split != "base": + # Check if domain supports splits + splits_loader = registry.get_task_splits_loader(domain) + if splits_loader: + splits = splits_loader() + if task_split in splits: + # Filter tasks by split + all_tasks = tasks_loader() + split_ids = set(splits[task_split]) + return [t for t in all_tasks if t.id in split_ids] + + # Default: load all tasks for domain + return tasks_loader() + + def _get_domain_tools(self, registry, domain: str) -> List[Dict[str, Any]]: + """Get available tools for a domain.""" + try: + env_constructor = registry.get_env_constructor(domain) + env = env_constructor() + + # get_tools() returns list of Tool objects + if hasattr(env, "get_tools"): + tools = env.get_tools() + if isinstance(tools, list): + # Convert Tool objects to dicts + return [ + { + "name": getattr(t, "name", str(t)), + "description": getattr(t, "long_desc", ""), + } + for t in tools + ] + + except Exception as e: + logger.debug(f"Could not get tools for {domain}: {e}") + return [] + + def get_domains(self) -> List[str]: + """Get list of available domains.""" + return ["airline", "retail", "telecom"] + + def get_task_splits(self) -> List[str]: + """Get list of available task splits.""" + return ["base", "human", "gpt4o"] + + def get_task_count(self, domain: str, task_split: str = "base") -> int: + """Get number of tasks available for a domain/split combination.""" + try: + from tau2.registry import registry + + tasks = self._get_tasks_for_domain(registry, domain, task_split) + return len(tasks) + except ImportError: + return 0 + except Exception: + return 0 + + def check_data_available(self) -> bool: + """Check if tau2 data is available and configured.""" + data_dir = os.environ.get("TAU2_DATA_DIR") + if not data_dir: + return False + return os.path.isdir(data_dir) diff --git a/benchmarks/tasks/tau_bench/CLAUDE.md b/benchmarks/tasks/tau_bench/CLAUDE.md new file mode 100644 index 0000000000000000000000000000000000000000..09d04af413bf79ba5b4e84b336abaab882d74551 --- /dev/null +++ b/benchmarks/tasks/tau_bench/CLAUDE.md @@ -0,0 +1,116 @@ +# TAU-bench Evaluation Guide + +TAU-bench (tau2) evaluates tool-calling agents in customer service domains using multi-turn conversations and database state assertions. Official leaderboard: https://tau-bench.github.io + +Use `/benchmark <config> [mode] [extra-args]` for quick runs (e.g. `/benchmark haiku`, `/benchmark fast`). + +## Quick Start + +```bash +# Baseline run (no ACE) +uv run python scripts/run_tau_benchmark.py --config sonnet --skip-ace + +# Compare baseline vs ACE +uv run python scripts/run_tau_benchmark.py --config sonnet --compare + +# Quick smoke test (3 tasks, k=1) +uv run python scripts/run_tau_benchmark.py --config fast --skip-ace +``` + +## Config Profiles + +All configs live in `benchmarks/tasks/tau_bench/` and inherit from `default.yaml`. + +| Profile | Model | Use Case | +|---------|-------|----------| +| `default` | gpt-4.1-mini | Official leaderboard defaults | +| `sonnet` | claude-sonnet-4-5 | Claude Sonnet evaluation | +| `haiku` | claude-haiku-4-5 | Claude Haiku evaluation | +| `gpt4.1-mini` | gpt-4.1-mini | Explicit GPT-4.1-mini | +| `gpt4.1` | gpt-4.1 | GPT-4.1 (stronger) | +| `fast` | gpt-4.1-mini | Quick iteration (3 tasks, k=1) | + +CLI args override config values: `--config sonnet --domain retail --k 2` + +## Best Practices + +- **User simulator must be `gpt-4.1-2025-04-14`** — wrong model gives ~50% lower scores. +- **max_steps=200** — lower values cause tasks to fail early. +- **Temperature 0**, **seed 300**, **k=4** for reproducibility matching the leaderboard. +- Task splits: train=30, test=20 for airline. Use `test` split to match leaderboard. +- Always run `--skip-ace` first to establish a baseline before testing ACE. +- Use `--compare` for side-by-side baseline vs ACE in one run. +- Use `--save-detailed` for per-task trial-level data (debugging). +- ACE trains on `train` split, evaluates on `test` split (automatic with `--compare`). +- Use `--skillbook path/to/skillbook.json` to evaluate a pre-trained skillbook without re-training. +- `--batch-reflect` defers learning until all training tasks complete. +- `--capture-reflector-inputs DIR` runs train tasks with an empty skillbook and saves the exact reflector inputs per task as JSON to DIR (no reflection/learning happens). Useful for offline analysis or replaying with different reflector versions. +- `--replay-reflector-inputs DIR` replays captured inputs through all prompt versions (base, v2–v5) to train one skillbook per version. No agent re-execution needed. + +## Capturing Reflector Inputs + +Save the inputs that *would* be passed to `reflector.reflect()` without actually calling it: + +```bash +# Capture inputs for all train tasks +uv run python scripts/run_tau_benchmark.py --config fast \ + --capture-reflector-inputs tau_benchmark_results/reflector_inputs + +# Capture specific tasks only +uv run python scripts/run_tau_benchmark.py --config fast \ + --capture-reflector-inputs tau_benchmark_results/reflector_inputs \ + --task-ids 0,1,2 +``` + +Each task produces a `task_{id}.json` that is exactly what `reflector.reflect()` receives: +```json +{ + "question": "customer service task", + "ground_truth": null, + "feedback": "Task SUCCEEDED. Reward: 1.00, Steps: 12", + "agent_output": { "final_answer": "...", "reasoning": "...", "skill_ids": [] }, + "skillbook": "(empty skillbook)" +} +``` + +All existing flags (`--feedback-level`, `--config`, `--domain`, `--limit`, `--task-ids`, `--max-steps`, `--seed`) work naturally with `--capture-reflector-inputs`. Uses the train split by default (override with `--task-split`). + +## Replaying Reflector Inputs + +Replay captured inputs through all prompt versions (base, v2, v3, v4, v5) to train one skillbook per version — no agent re-execution needed: + +```bash +# Replay all captured inputs through all prompt versions +uv run python scripts/run_tau_benchmark.py \ + --replay-reflector-inputs tau_benchmark_results/reflector_inputs_airline_train \ + --model "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0" +``` + +For each version, creates a fresh Skillbook + Reflector (recursive mode) + SkillManager, iterates all task files sequentially (reflect → update_skills → apply_update), and saves the trained skillbook: + +``` +{input_dir}/training_recursive_sequential/base/skillbook.json +{input_dir}/training_recursive_sequential/v2/skillbook.json +{input_dir}/training_recursive_sequential/v3/skillbook.json +{input_dir}/training_recursive_sequential/v4/skillbook.json +{input_dir}/training_recursive_sequential/v5/skillbook.json +``` + +Use `--reflector-model` to override the model used for reflection (defaults to `--model`). + +## Presenting Results + +Always include these fields: exact model ID (e.g. `claude-sonnet-4-5-20250929`), user LLM, domain, split + task count, skillbook status, training info (or "none"), and all pass^k metrics. Only include max steps / seed if non-default (200 / 300). + +Use a settings table + pass^k table. For comparisons, add Baseline / ACE / Delta columns. + +## Output Files + +Results saved to `tau_benchmark_results/`: +``` +tau_{domain}_{config}_{phase}_{timestamp}_summary.json +tau_{domain}_{config}_{phase}_{timestamp}_detailed.json (with --save-detailed) +tau_{domain}_{config}_{phase}_{timestamp}_skillbook.json (if skills learned) +``` + +The summary JSON contains all configuration and metrics needed to reproduce the result. diff --git a/benchmarks/tasks/tau_bench/Tau2Benchmark Result Haiku4.5.png b/benchmarks/tasks/tau_bench/Tau2Benchmark Result Haiku4.5.png new file mode 100644 index 0000000000000000000000000000000000000000..5d7d4af2cad940987bdd26fc4aa443ef17c5f0d1 --- /dev/null +++ b/benchmarks/tasks/tau_bench/Tau2Benchmark Result Haiku4.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0e91cf337646fa789fc15900879e74c2962992c1c645e1906e68a793bba0756 +size 146906 diff --git a/benchmarks/tasks/tau_bench/default.yaml b/benchmarks/tasks/tau_bench/default.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cf53cf7ff40aae91afa02f0030ee68d05102bd81 --- /dev/null +++ b/benchmarks/tasks/tau_bench/default.yaml @@ -0,0 +1,21 @@ +task: tau_bench +version: 2.0 + +# Official leaderboard defaults +model: gpt-4.1-mini-2025-04-14 +user_llm: gpt-4.1-2025-04-14 +temperature: 0.0 +max_tokens: 2048 +max_steps: 200 +max_errors: 10 +seed: 300 +k: 4 +task_split: test +domain: airline + +# ACE settings +ace: + epochs: 1 + max_refinement_rounds: 3 + batch_reflect: false + trace_limit: 500 diff --git a/benchmarks/tasks/tau_bench/fast.yaml b/benchmarks/tasks/tau_bench/fast.yaml new file mode 100644 index 0000000000000000000000000000000000000000..590a2326af8839c86553bdf64428b0e379c10d89 --- /dev/null +++ b/benchmarks/tasks/tau_bench/fast.yaml @@ -0,0 +1,4 @@ +inherits: default +limit: 3 +k: 1 +max_steps: 50 diff --git a/benchmarks/tasks/tau_bench/gpt4.1-mini.yaml b/benchmarks/tasks/tau_bench/gpt4.1-mini.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b2ffd3a3205566ad26cf0b836f45ac5cba1045f2 --- /dev/null +++ b/benchmarks/tasks/tau_bench/gpt4.1-mini.yaml @@ -0,0 +1,2 @@ +inherits: default +model: gpt-4.1-mini-2025-04-14 diff --git a/benchmarks/tasks/tau_bench/gpt4.1.yaml b/benchmarks/tasks/tau_bench/gpt4.1.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c21b951e5438871fc0e96e0d6024769efbc09c31 --- /dev/null +++ b/benchmarks/tasks/tau_bench/gpt4.1.yaml @@ -0,0 +1,2 @@ +inherits: default +model: gpt-4.1-2025-04-14 diff --git a/benchmarks/tasks/tau_bench/haiku.yaml b/benchmarks/tasks/tau_bench/haiku.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3edd99721524017345d0438d44dfcb25af62114c --- /dev/null +++ b/benchmarks/tasks/tau_bench/haiku.yaml @@ -0,0 +1,2 @@ +inherits: default +model: claude-haiku-4-5-20251001 diff --git a/benchmarks/tasks/tau_bench/sonnet.yaml b/benchmarks/tasks/tau_bench/sonnet.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e98592680bbaeb809529fa33bf4053ea4faadbed --- /dev/null +++ b/benchmarks/tasks/tau_bench/sonnet.yaml @@ -0,0 +1,2 @@ +inherits: default +model: claude-sonnet-4-5-20250929 diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000000000000000000000000000000000000..3e5cc60a6c5ffe45cf28da7024c3942238aa778f --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,439 @@ +# API Reference + +Quick reference for the most-used classes and functions in `ace`. + +## Runners + +### ACELiteLLM + +Simple self-improving conversational agent. + +```python +from ace import ACELiteLLM + +agent = ACELiteLLM.from_model("gpt-4o-mini") +``` + +| Method | Description | +|--------|-------------| +| `ask(question, context="")` | Generate an answer using the current skillbook | +| `learn(samples, environment, epochs=1, *, wait=True)` | Run the full ACE learning pipeline | +| `learn_from_feedback(feedback, ground_truth=None)` | Learn from the last `ask()` interaction | +| `learn_from_traces(traces, epochs=1, *, wait=True)` | Learn from pre-recorded execution traces | +| `save(path)` | Save skillbook to JSON | +| `load(path)` | Load skillbook from JSON | +| `enable_learning()` / `disable_learning()` | Toggle learning on/off | +| `wait_for_background(timeout=None)` | Wait for async learning to finish | +| `learning_stats` | Dict with background learning progress | +| `get_strategies()` | Formatted string of current strategies | + +See [LiteLLM Integration](../integrations/litellm.md) for full details. + +### ACE + +Full adaptive pipeline (Agent + Reflector + SkillManager + Environment). + +```python +from ace import ACE, Agent, Reflector, SkillManager, Skillbook, SimpleEnvironment + +runner = ACE.from_roles( + agent=Agent("gpt-4o-mini"), + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), + environment=SimpleEnvironment(), + skillbook=Skillbook(), +) + +results = runner.run(samples, epochs=3) +``` + +| Method | Description | +|--------|-------------| +| `run(samples, epochs=1, wait=True)` | Run adaptation loop, return `list[SampleResult]` | +| `save(path)` | Save skillbook | +| `wait_for_background(timeout=None)` | Wait for async learning | +| `learning_stats` | Background learning progress | + +See [Full Pipeline Guide](../guides/full-pipeline.md). + +### BrowserUse + +Browser automation with learning. + +```python +from ace import BrowserUse + +runner = BrowserUse.from_model(browser_llm=my_llm, ace_model="gpt-4o-mini") +results = runner.run("Find the top post on Hacker News") +``` + +See [Browser-Use Integration](../integrations/browser-use.md). + +### LangChain + +Wrap LangChain Runnables with learning. + +```python +from ace import LangChain + +runner = LangChain.from_model(my_chain, ace_model="gpt-4o-mini") +results = runner.run([{"input": "Summarize this document"}]) +``` + +See [LangChain Integration](../integrations/langchain.md). + +### ClaudeCode + +Claude Code CLI with learning. + +```python +from ace import ClaudeCode + +runner = ClaudeCode.from_model(working_dir="./project", ace_model="gpt-4o-mini") +results = runner.run("Add unit tests for utils.py") +``` + +See [Claude Code Integration](../integrations/claude-code.md). + +### ClaudeSDKExecuteStep / ClaudeSDKToTrace + +Direct Anthropic Messages API steps for custom pipelines. + +```python +from ace import Pipeline, Reflector, SkillManager, Skillbook, learning_tail +from ace.integrations import ClaudeSDKExecuteStep, ClaudeSDKToTrace + +skillbook = Skillbook() +pipe = Pipeline([ + ClaudeSDKExecuteStep(model="claude-sonnet-4-20250514"), + ClaudeSDKToTrace(), + *learning_tail(Reflector("gpt-4o-mini"), SkillManager("gpt-4o-mini"), skillbook), +]) +``` + +`ClaudeSDKResult` and `ToolCall` are Pydantic models, so token counts, latency, +tool calls, and serialization are validated before the learning tail consumes +the trace. + +See [Claude SDK Integration](../integrations/claude-sdk.md). + +--- + +## Roles + +### Agent + +Produces answers using the current skillbook. + +```python +from ace import Agent + +agent = Agent("gpt-4o-mini") +output = agent.generate( + question="What is 2+2?", + context="", + skillbook=skillbook, + reflection=None, # optional +) +``` + +**AgentOutput fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `final_answer` | `str` | The generated answer | +| `reasoning` | `str` | Step-by-step reasoning | +| `skill_ids` | `list[str]` | Skillbook strategies cited | +| `raw` | `dict` | Raw LLM response | + +### Reflector + +Analyzes what worked and what failed. + +```python +from ace import Reflector + +reflector = Reflector("gpt-4o-mini") +reflection = reflector.reflect( + question="What is 2+2?", + agent_output=output, + skillbook=skillbook, + ground_truth="4", + feedback="Correct!", +) +``` + +**ReflectorOutput fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `reasoning` | `str` | Analysis of the outcome | +| `error_identification` | `str` | What went wrong | +| `root_cause_analysis` | `str` | Why it went wrong | +| `correct_approach` | `str` | What should have been done | +| `key_insight` | `str` | Main lesson learned | +| `skill_tags` | `list[SkillTag]` | `(skill_id, tag)` pairs — populated in online mode when agent cited skills | +| `raw` | `dict` | Raw LLM response | + +### SkillManager + +Transforms reflections into skillbook updates. + +```python +from ace import SkillManager + +skill_manager = SkillManager("gpt-4o-mini") +sm_output = skill_manager.update_skills( + reflections=(reflection,), + skillbook=skillbook, + question_context="Math problems", + progress="3/5 correct", + source=source, +) +# skillbook has already been mutated in place +``` + +Returns a `SkillManagerOutput` with an `.update` field (`UpdateBatch`) and `.raw` field. + +See [Roles](../concepts/roles.md) for full details. + +--- + +## Skillbook + +```python +from ace import Skillbook + +skillbook = Skillbook() +``` + +| Method / Property | Description | +|-------------------|-------------| +| `add_skill(section, issue=None, keywords=None, insight=None, content=None)` | Add a skill | +| `apply_update(update_batch)` | Apply update operations | +| `as_prompt()` | Markdown format for LLM consumption | +| `save_to_file(path)` | Save JSON plus embeddings sidecar | +| `Skillbook.load_from_file(path)` | Load JSON plus embeddings sidecar if present | +| `stats()` | Section count, skill count, active skill totals | +| `skills()` | List of all skills | + +See [The Skillbook](../concepts/skillbook.md). + +--- + +## Data Types + +### Sample + +```python +from ace import Sample + +sample = Sample( + question="What is 2+2?", + context="Show your work", + ground_truth="4", +) +``` + +### EnvironmentResult + +```python +from ace import EnvironmentResult + +result = EnvironmentResult( + feedback="Correct!", + ground_truth="4", + metrics={"accuracy": 1.0}, +) +``` + +### UpdateOperation + +```python +from ace import UpdateOperation + +op = UpdateOperation( + type="ADD", + section="context", + keywords=["math", "decomposition"], + issue="Complex arithmetic questions are easier to solve when the work is decomposed into smaller verified steps.", + insight="Break problems into smaller steps before computing.", + reflection_index=0, + reflection_indices=[0, 1], + skill_id="math-00001", +) +``` + +Operations: `ADD`, `UPDATE`, `TAG`, `REMOVE`. See [Update Operations](../concepts/updates.md). + +### DeduplicationConfig + +**Requires:** `uv add ace-framework[deduplication]` + +```python +from ace import DeduplicationConfig + +config = DeduplicationConfig( + enabled=True, + embedding_model="text-embedding-3-small", + similarity_threshold=0.85, +) +``` + +--- + +## Environments + +Extend `TaskEnvironment` to provide evaluation feedback: + +```python +from ace import TaskEnvironment, EnvironmentResult + +class MyEnvironment(TaskEnvironment): + def evaluate(self, sample, agent_output): + correct = sample.ground_truth.lower() in agent_output.final_answer.lower() + return EnvironmentResult( + feedback="Correct!" if correct else "Incorrect", + ground_truth=sample.ground_truth, + ) +``` + +A built-in `SimpleEnvironment` uses substring matching and is included for quick testing. + +--- + +## Providers + +### resolve_model + +Resolve a model string to a PydanticAI model instance: + +```python +from ace.providers import resolve_model + +model = resolve_model("gpt-4o-mini") +``` + +Supports any [LiteLLM model](https://docs.litellm.ai/) or PydanticAI-native identifier. + +### ACEModelConfig + +Configuration for model selection per role: + +```python +from ace.providers import ACEModelConfig + +config = ACEModelConfig.from_toml("ace.toml") +agent_model = config.for_role("agent") +``` + +--- + +## Observability + +### OpikStep + +Append to any pipeline for automatic tracing and cost tracking: + +```python +from ace import OpikStep + +OpikStep(project_name="my-experiment", tags=["training"]) +``` + +### register_opik_litellm_callback + +Standalone LLM cost tracking without pipeline traces: + +```python +from ace import register_opik_litellm_callback + +register_opik_litellm_callback(project_name="my-experiment") +``` + +See [Opik Observability](../integrations/opik.md). + +--- + +## Recursive Reflector (RR) + +PydanticAI agent-based trace analyser with tools for code execution and sub-agent analysis. + +### RRStep + +Drop-in replacement for `Reflector` — satisfies both `StepProtocol` and `ReflectorLike`. + +```python +from ace.rr import RRStep, RRConfig + +rr = RRStep( + "gpt-4o-mini", # Model string + config=RRConfig(max_requests=20), # Configuration +) + +# As drop-in reflector +ace = ACELiteLLM.from_model("gpt-4o-mini", reflector=rr) + +# As pipeline step +pipe = Pipeline([..., rr, ...]) +``` + +### RRConfig + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `timeout` | `30.0` | Per-execution timeout in seconds (Unix only) | +| `max_tokens` | `500_000` | Total token budget (input + output) per agent run | +| `max_requests` | `50` | Safety cap on LLM requests per agent run | +| `context_window` | `128_000` | Model context window; compaction triggers at 85% | +| `max_output_chars` | `20_000` | Per-execution output truncation limit | +| `max_depth` | `2` | Maximum recursion depth (0=root, max_depth=leaf) | +| `child_budget_fraction` | `0.5` | Fraction of remaining token budget for child sessions | +| `max_compactions` | `3` | Safety cap on full summarization rounds | +| `microcompact_keep_recent` | `3` | Recent tool results to preserve during microcompaction | + +### Sandbox Functions + +Available inside `execute_code` tool calls: + +| Function | Description | +|----------|-------------| +| `FINAL(value)` | Submit final result dict (terminates the loop) | +| `FINAL_VAR(name)` | Submit a named variable as the result | +| `SHOW_VARS()` | Print available variables (debugging) | +| `register_helper(name, source, desc)` | Register a reusable helper function | +| `list_helpers()` | List registered helper names/descriptions | +| `run_helper(name, *args, **kwargs)` | Invoke a registered helper | +| `get_item_messages(item)` | Return message list for a batch item | +| `get_item_question(item)` | Return question string for a batch item | +| `get_message_text(msg)` | Safely render message content as text | + +### TraceContext + +Structured trace wrapper with factory methods: + +| Factory | Input | +|---------|-------| +| `TraceContext.from_agent_output(output)` | `AgentOutput` | +| `TraceContext.from_conversation_history(msgs)` | `list[dict]` | +| `TraceContext.from_tau_simulation(msgs, system_prompt)` | TAU-bench messages | +| `TraceContext.from_browser_use(history)` | browser-use `AgentHistory` | +| `TraceContext.from_langchain(steps)` | LangChain intermediate steps | +| `TraceContext.from_reasoning_string(text)` | Raw reasoning string | +| `TraceContext.combine(traces)` | Merge multiple traces | + +See [RR_DESIGN.md](../RR_DESIGN.md) for the full architecture reference. + +--- + +## Prompts + +The default prompts are v2.1 (built into `ace`). Pass a custom template via `prompt_template`: + +```python +agent = Agent("gpt-4o-mini", prompt_template="Custom prompt with {skillbook}, {question}, {context}") +reflector = Reflector("gpt-4o-mini", prompt_template="Custom reflector prompt ...") +skill_manager = SkillManager("gpt-4o-mini", prompt_template="Custom skill manager prompt ...") +``` + +See [Prompt Engineering](../guides/prompts.md). diff --git a/docs/concepts/insight-levels.md b/docs/concepts/insight-levels.md new file mode 100644 index 0000000000000000000000000000000000000000..f34293bcc18b1b884b9eaf83b7f23833992058d8 --- /dev/null +++ b/docs/concepts/insight-levels.md @@ -0,0 +1,81 @@ +# Insight Levels + +The ACE framework operates at three insight levels depending on what scope the Reflector analyzes. + +## Overview + +| Level | Reflector Scope | Feedback Source | Implementation | +|-------|-----------------|----------------|----------------| +| **Micro** | Single interaction | Environment (ground truth) | `ACE` runner with `TaskEnvironment` | +| **Meso** | Full agent run | Execution trace (no ground truth) | Integration runners (`BrowserUse`, `LangChain`, `ClaudeCode`) | +| **Macro** | Cross-run analysis | Pattern comparison across runs | Future enhancement | + +## Micro-Level + +The Reflector receives the agent's output **and** environment feedback (ground truth, correctness). This is the most precise learning signal. + +```mermaid +graph LR + Q[Question] --> R[Reflector] + A[Agent Answer] --> R + GT[Ground Truth] --> R + F[Feedback] --> R +``` + +Use when you have labeled data or a reliable evaluation function. + +```python +from ace import ACE, Sample, SimpleEnvironment + +runner = ACE.from_roles( + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + environment=SimpleEnvironment(), +) + +samples = [ + Sample(question="What is 2+2?", context="", ground_truth="4"), +] +runner.run(samples, epochs=3) +``` + +## Meso-Level + +The Reflector receives the full **execution trace** — the agent's reasoning steps, tool calls, actions, and outcomes — but no external ground truth. It learns from execution patterns rather than correctness evaluation. + +```mermaid +graph LR + T[Task] --> R[Reflector] + ET["Execution Trace (thoughts, actions, results)"] --> R +``` + +Use when wrapping external agents where you don't have labeled answers. + +```python +from ace import BrowserUse + +# The browser-use agent produces a rich trace of actions +runner = BrowserUse.from_model( + browser_llm=ChatOpenAI(model="gpt-4o"), + ace_model="gpt-4o-mini", +) +runner.run("Find the top post on Hacker News") +``` + +The extracted trace includes: + +- Agent reasoning at each step +- Browser actions (click, type, navigate) +- Page observations +- Success/failure of each action + +## Macro-Level + +Cross-run pattern analysis — comparing strategies across multiple execution histories. Not yet implemented. + +## What to Read Next + +- [Three Roles](roles.md) — the roles involved at each level +- [Integration Pattern](../guides/integration.md) — meso-level integrations in practice +- [Full Pipeline Guide](../guides/full-pipeline.md) — micro-level pipelines in practice diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md new file mode 100644 index 0000000000000000000000000000000000000000..574ecf3dcb3dbe19e0fc55c794101afdd2994f30 --- /dev/null +++ b/docs/concepts/overview.md @@ -0,0 +1,119 @@ +# How ACE Works + +**Agentic Context Engineering (ACE)** enables AI agents to learn from their own execution feedback. Instead of updating model weights (expensive, slow, opaque), ACE evolves a **skillbook** of strategies based on what actually works. + +!!! info "Research" + ACE was introduced in [*Agentic Context Engineering: Evolving Contexts for Self-Improving Language Models*](https://arxiv.org/abs/2510.04618) by researchers at Stanford University and SambaNova Systems. + +## The Learning Loop + +Three collaborative roles share the same base LLM: + +```mermaid +graph LR + S[Sample] --> A[Agent] + A --> E[Environment] + E -->|feedback| R[Reflector] + R -->|analyzes| SM[SkillManager] + SM -->|updates| SK[Skillbook] + SK -.->|context| A +``` + +1. The **Agent** executes a task using strategies from the skillbook +2. The **Environment** evaluates the result (correct/incorrect, feedback) +3. The **Reflector** analyzes what worked and what failed +4. The **SkillManager** updates the skillbook with new strategies + +The **Skillbook** accumulates strategies across runs, making every subsequent agent call smarter. + +## Three Roles + +| Role | Responsibility | Key Class | +|------|---------------|-----------| +| **Agent** | Executes tasks using skillbook strategies | `Agent` | +| **Reflector** | Analyzes execution results (what worked, what failed) | `Reflector` | +| **SkillManager** | Transforms reflections into skillbook updates | `SkillManager` | + +All three roles use the same LLM — the intelligence comes from the specialized prompts each role receives. + +See [Three Roles](roles.md) for details on each role's inputs and outputs. + +## Two Architecture Patterns + +### Full ACE Pipeline + +Use when building a new agent from scratch. + +```mermaid +graph LR + S[Sample] --> A[Agent] + A --> E[Environment] + E --> R[Reflector] + R --> SM[SkillManager] + SM --> SK[Skillbook] +``` + +All three roles participate. The Agent produces answers, the Environment evaluates them, and the learning pipeline updates the skillbook. + +```python +from ace import ACE, Agent, Reflector, SkillManager, SimpleEnvironment + +runner = ACE.from_roles( + agent=Agent("gpt-4o-mini"), + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), + environment=SimpleEnvironment(), +) +results = runner.run(samples, epochs=3) +``` + +### Integration Pattern + +Use when wrapping an existing agent (browser-use, LangChain, Claude Code). + +```mermaid +graph LR + EA[External Agent] -->|executes| R[Reflector] + R -->|analyzes trace| SM[SkillManager] + SM -->|updates| SK[Skillbook] +``` + +No ACE Agent — the external framework handles execution. ACE only learns from the results. + +Three steps: **INJECT** skillbook context, **EXECUTE** with external agent, **LEARN** from results. + +```python +from ace import BrowserUse + +runner = BrowserUse.from_model( + browser_llm=ChatOpenAI(model="gpt-4o"), + ace_model="gpt-4o-mini", +) +results = runner.run("Find the top post on Hacker News") +``` + +See [Integration Pattern](../guides/integration.md) for building custom integrations. + +## How It Compares + +| Approach | Updates | Speed | Interpretability | +|----------|---------|-------|-----------------| +| **Fine-tuning** | Model weights | Slow (hours) | Low (opaque) | +| **RAG** | External documents | Medium | Medium | +| **ACE** | Skillbook context | Fast (real-time) | High (readable strategies) | + +ACE strategies are human-readable, auditable, and transferable between models. + +## Performance + +| Benchmark | Improvement | Notes | +|-----------|-------------|-------| +| AppWorld Agent | **+17.1 pp** | Complex multi-step tasks with tool use | +| FiNER (Finance) | **+8.6 pp** | Financial reasoning tasks | +| Adaptation Latency | **-86.9%** | vs. existing context-adaptation methods | + +## What to Read Next + +- [The Skillbook](skillbook.md) — how strategies are stored and evolve +- [Three Roles](roles.md) — Agent, Reflector, and SkillManager in detail +- [Quick Start](../getting-started/quick-start.md) — run your first agent diff --git a/docs/concepts/roles.md b/docs/concepts/roles.md new file mode 100644 index 0000000000000000000000000000000000000000..1b71c0e73c7e2b487b3edd40a3362e9f6ed8c4c2 --- /dev/null +++ b/docs/concepts/roles.md @@ -0,0 +1,132 @@ +# Three Roles + +ACE uses three collaborative roles that share the same base LLM. Each role has a specialized prompt that focuses it on a specific part of the learning loop. + +```mermaid +graph LR + A[Agent] -->|execute| E[Environment] + E -->|evaluate| R[Reflector] + R -->|analyze| SM[SkillManager] + SM -->|update| SK[Skillbook] +``` + +## Agent + +**Produces answers** using the current skillbook. + +The Agent receives a question, context, and the skillbook's strategies, then generates a reasoned answer citing which skills it used. + +```python +from ace import Agent + +agent = Agent("gpt-4o-mini") + +output = agent.generate( + question="What is 2+2?", + context="Show your work", + skillbook=skillbook, + reflection=None, # Optional: reflection from a previous attempt +) +``` + +### AgentOutput + +| Field | Type | Description | +|-------|------|-------------| +| `final_answer` | `str` | The generated answer | +| `reasoning` | `str` | Step-by-step reasoning | +| `skill_ids` | `List[str]` | Skillbook strategies cited | +| `raw` | `Dict` | Raw LLM response | + +## Reflector + +**Analyzes execution outcomes** — what worked, what failed, and why. + +The Reflector receives the agent's output, the environment's feedback, and the skillbook. It produces an analysis of the outcome and tags each cited skill as helpful, harmful, or neutral. + +```python +from ace import Reflector + +reflector = Reflector(llm) + +reflection = reflector.reflect( + question="What is 2+2?", + agent_output=output, + skillbook=skillbook, + ground_truth="4", + feedback="Correct!", +) +``` + +### ReflectorOutput + +| Field | Type | Description | +|-------|------|-------------| +| `reasoning` | `str` | Analysis of the outcome | +| `error_identification` | `str` | What went wrong (if anything) | +| `root_cause_analysis` | `str` | Why it went wrong | +| `correct_approach` | `str` | What should have been done | +| `key_insight` | `str` | Main lesson learned | +| `skill_tags` | `List[SkillTag]` | `(skill_id, tag)` pairs | + +### Reflector Modes + +| Mode | Description | +|------|-------------| +| `SIMPLE` | Single-pass analysis (default) | +| `RECURSIVE` | Multi-pass with code execution in a REPL loop | + +## SkillManager + +**Transforms reflections into skillbook updates.** + +The SkillManager takes the Reflector's analysis and decides which operations to apply to the skillbook — adding new strategies, updating existing ones, or removing harmful ones. + +```python +from ace import SkillManager + +skill_manager = SkillManager(llm) + +sm_output = skill_manager.update_skills( + reflections=(reflection,), + skillbook=skillbook, + question_context="Math problems", + progress="3/5 correct", +) + +# Apply the updates +skillbook.apply_update(sm_output.update) +``` + +### SkillManagerOutput + +| Field | Type | Description | +|-------|------|-------------| +| `update` | `UpdateBatch` | Batch of update operations to apply | +| `consolidation_ops` | `List` | Deduplication operations (if enabled) | + +## Shared LLM + +All three roles use the same model string. The intelligence comes from the specialized prompts, not from using different models: + +```python +from ace import Agent, Reflector, SkillManager + +agent = Agent("gpt-4o-mini") +reflector = Reflector("gpt-4o-mini") +skill_manager = SkillManager("gpt-4o-mini") +``` + +You can optionally use a cheaper model for the learning roles (Reflector + SkillManager) while keeping a stronger model for the Agent: + +```python +agent = Agent("gpt-4o") +reflector = Reflector("gpt-4o-mini") +skill_manager = SkillManager("gpt-4o-mini") +``` + +## What to Read Next + +- [Insight Levels](insight-levels.md) — micro, meso, and macro analysis scopes +- [Update Operations](updates.md) — the operations the SkillManager emits +- [Full Pipeline Guide](../guides/full-pipeline.md) — wire the roles together diff --git a/docs/concepts/skillbook.md b/docs/concepts/skillbook.md new file mode 100644 index 0000000000000000000000000000000000000000..eb987b29e3c0dd6b6ca6268558fb89ac66e9d4a7 --- /dev/null +++ b/docs/concepts/skillbook.md @@ -0,0 +1,183 @@ +# The Skillbook + +The **Skillbook** is ACE's knowledge store — a structured collection of learned issues and insights. Each entry is called a **skill**. + +## What Is a Skill? + +A skill is a single learned entry with: + +| Field | Description | +|-------|-------------| +| `id` | Unique identifier (e.g., `context-00001`) | +| `section` | Pipeline-facing split: `context` or `harness` | +| `keywords` | Structured topic labels (domain, subsystem, API, behavior) | +| `issue` | The problem this skill captures | +| `insight` | The recommended action; required for `context`, optional for `harness` | +| `active` | Whether the skill participates in normal active views | +| `helpful_count` / `harmful_count` / `neutral_count` | Effectiveness counters | +| `occurrences` | Provenance records linking the skill back to supporting traces | + +Example skill: + +```json +{ + "id": "context-00001", + "section": "context", + "keywords": ["math", "decomposition"], + "issue": "Complex arithmetic questions are easier to solve when the work is decomposed into smaller verified steps.", + "insight": "Break the problem into smaller steps before computing.", + "active": true, + "helpful_count": 5, + "harmful_count": 0, + "neutral_count": 1 +} +``` + +## Skill Lifecycle + +Skills go through four stages: + +1. **Created** — the SkillManager adds a new skill after a reflection +2. **Tagged** — each time the Agent cites a skill, the Reflector tags it as helpful, harmful, or neutral +3. **Updated** — the SkillManager may refine a skill's issue, insight, or keywords based on new learnings +4. **Removed** — skills are soft-removed by setting `active=False` + +These correspond to four [update operations](updates.md): `ADD`, `TAG`, `UPDATE`, `REMOVE`. + +## Prompt Format + +When the skillbook is rendered as text, it is grouped by section and shows keywords plus issue/insight: + +```python +skillbook.as_prompt() # Markdown format for LLM consumption +``` + +``` +## context +- [context-00001] + Keywords: math, decomposition + Issue: Complex arithmetic questions are easier to solve when the work is decomposed into smaller verified steps. + Insight: Break the problem into smaller steps before computing. + +## harness +- [harness-00001] + Keywords: retries, rate_limit + Issue: The runtime can stall for long periods when provider 429 retries fan out. +``` + +## Sections + +Skills are organized into exactly two sections: + +- `context`: learnings the agent should apply while solving the task +- `harness`: environment or runtime learnings that affect the pipeline itself + +Fine-grained categorization lives in `keywords`: + +```python +from ace import Skillbook + +skillbook = Skillbook() + +# Add a context skill with explicit keywords and an action insight +skillbook.add_skill( + section="context", + issue="Complex arithmetic questions are easier to solve when the work is decomposed into smaller verified steps.", + keywords=["math", "decomposition"], + insight="Break complex problems into smaller steps before computing.", +) +``` + +## Persistence + +```python +# Save +skillbook.save_to_file("strategies.json") +# Writes `strategies.json` plus `strategies.embeddings.npz` + +# Load +skillbook = Skillbook.load_from_file("strategies.json") +``` + +## Statistics + +```python +stats = skillbook.stats() +# {"sections": 2, "skills": 15, "active_skills": 14, "by_section": {"context": 11, "harness": 3}} +``` + +## Deduplication + +As the skillbook grows, similar skills can accumulate. The `DeduplicationManager` detects and consolidates them using embedding similarity: + +```python +from ace import DeduplicationConfig, DeduplicationManager + +config = DeduplicationConfig( + enabled=True, + embedding_model="text-embedding-3-small", + similarity_threshold=0.85, + within_section_only=True, +) +dedup = DeduplicationManager(config) +``` + +When used with a runner, deduplication runs automatically at a configurable interval: + +```python +runner = ACE.from_roles( + ..., + dedup_manager=dedup, + dedup_interval=10, # Every 10 samples +) +``` + +## Insight Source Tracing + +Each skill tracks where it came from using structured provenance records. +Each source stores a stable trace identity (`trace_uid`, `source_system`, +`trace_id`, `display_name`) plus optional learning metadata such as +`epoch`, `step`, `learning_text`, and `trace_refs`. + +One skill can carry multiple source records. This is especially useful for +skills synthesized from several traces, where ACE can attach one primary +source plus additional supporting sources instead of collapsing everything +onto a single trace. + +Trace identity is exact. In-trace anchors are best-effort: when the trace is +structured enough, `trace_refs` can include `json_path`, `step_indices`, +`message_indices`, and `span_ids`; otherwise ACE falls back to excerpt-only +references. + +Typical source record: + +```json +{ + "trace_uid": "kayba-hosted:conv-123", + "source_system": "kayba-hosted", + "trace_id": "conv-123", + "display_name": "checkout-failure.md", + "epoch": 1, + "step": 3, + "learning_text": "Check for a next-page token before stopping", + "trace_refs": [ + { + "text_excerpt": "The API response included next_page_token.", + "excerpt_location": "operation.evidence" + } + ] +} +``` + +Query provenance with: + +```python +sources = skillbook.source_map() # skill_id -> source info +summary = skillbook.source_summary() # Aggregated statistics +``` + +## What to Read Next + +- [Update Operations](updates.md) — how ADD, UPDATE, TAG, REMOVE work +- [Three Roles](roles.md) — which role creates, tags, and updates skills +- [Full Pipeline Guide](../guides/full-pipeline.md) — see the skillbook in action diff --git a/docs/concepts/updates.md b/docs/concepts/updates.md new file mode 100644 index 0000000000000000000000000000000000000000..1b9fc5185bae656044fe380c5a38e6dcc92bba8c --- /dev/null +++ b/docs/concepts/updates.md @@ -0,0 +1,123 @@ +# Update Operations + +The SkillManager communicates changes to the skillbook through **update operations**. Each operation is a structured instruction to modify the skillbook in a specific way. + +## Operation Types + +| Type | Description | Required Fields | +|------|-------------|----------------| +| `ADD` | Create a new skill | `section`, `issue`, `keywords` (`insight` required for `context`) | +| `UPDATE` | Modify an existing skill | `skill_id`, `issue` | +| `TAG` | Record whether a skill helped, harmed, or was neutral | `skill_id`, `metadata.delta` | +| `REMOVE` | Soft-remove a skill from the active skillbook | `skill_id` | + +## Examples + +### ADD + +Adds a new strategy learned from experience: + +```json +{ + "type": "ADD", + "section": "context", + "keywords": ["math", "decomposition"], + "issue": "Complex arithmetic questions are easier to solve when the work is decomposed into smaller verified steps.", + "insight": "Break complex problems into smaller steps before computing." +} +``` + +### UPDATE + +Refines an existing strategy: + +```json +{ + "type": "UPDATE", + "skill_id": "context-00001", + "section": "context", + "keywords": ["math", "verification"], + "issue": "Complex arithmetic questions are easier to solve when the work is decomposed into smaller verified steps.", + "insight": "Break complex problems into smaller steps and verify each step before proceeding." +} +``` + +### TAG + +Records whether a skill helped, harmed, or had no clear effect: + +```json +{ + "type": "TAG", + "section": "context", + "skill_id": "context-00001", + "metadata": {"delta": 1} +} +``` + +### REMOVE + +Prunes a strategy that is consistently harmful: + +```json +{ + "type": "REMOVE", + "section": "context", + "skill_id": "context-00003", + "reason": "The guidance is stale and now causes wrong tool choices." +} +``` + +## Update Batches + +The SkillManager emits operations as an `UpdateBatch` — one or more operations applied atomically: + +```python +from ace import UpdateOperation, UpdateBatch + +batch = UpdateBatch(operations=[ + UpdateOperation( + type="ADD", + section="context", + keywords=["debugging", "logging"], + issue="Input shape mismatches are hard to diagnose without request-level visibility.", + insight="Log the incoming payload before the failing transformation.", + ), + UpdateOperation(type="REMOVE", section="context", skill_id="context-00003"), +]) + +skillbook.apply_update(batch) +``` + +In batch reflection mode, `ADD` and `UPDATE` operations may also include +`reflection_index` to indicate which reflection in the input tuple primarily +produced the operation. + +When an operation is synthesized from multiple reflections, it may instead use +`reflection_indices` to list all contributing reflections. This lets downstream +provenance attach multiple trace sources to one learned skill. + +## Skill Tagging + +Skill effectiveness is recorded through `TAG` operations. The SkillManager decides +whether each injected skill helped, harmed, or had no material effect, and encodes +that as `metadata.delta`: + +- `1` → increment `helpful_count` +- `-1` → increment `harmful_count` +- `0` → increment `neutral_count` + +## How Updates Flow + +``` +Agent cites or injects skill_ids --> Reflector analyzes outcome --> SkillManager emits ADD/UPDATE/TAG/REMOVE +``` + +1. The **Agent** cites skill IDs it used in its reasoning +2. The **Reflector** produces the analysis that the SkillManager learns from +3. The **SkillManager** uses that analysis to ADD, UPDATE, TAG, or REMOVE skills + +## What to Read Next + +- [The Skillbook](skillbook.md) — where operations are applied +- [Three Roles](roles.md) — which role emits which operations diff --git a/docs/design/ACE_ARCHITECTURE.md b/docs/design/ACE_ARCHITECTURE.md new file mode 100644 index 0000000000000000000000000000000000000000..4d10a3b7163cc02b9195300f6b1b519797bbc909 --- /dev/null +++ b/docs/design/ACE_ARCHITECTURE.md @@ -0,0 +1,574 @@ +# ACE Architecture + +> Architecture for ACE, a pipeline-based framework for building self-improving AI agents. Roles are backed by PydanticAI agents; the pipeline engine handles composition and concurrency. + +For full code examples and API reference, see [ACE_REFERENCE.md](ACE_REFERENCE.md). +For design decisions and rejected alternatives, see [ACE_DECISIONS.md](ACE_DECISIONS.md). +For the pipeline engine, see [PIPELINE_DESIGN.md](PIPELINE_DESIGN.md). + +--- + +## Overview + +ACE (Agentic Context Engine) builds AI agents that learn from their own executions. It combines: + +- A **pipeline engine** (`pipeline/`) with typed step contracts, concurrent execution, and structured error handling +- **Roles** (Agent, Reflector, SkillManager) backed by PydanticAI agents for structured LLM interactions +- A **Skillbook** — an evolving knowledge base of strategies that agents read from and learning loops write to +- **Integration steps** for external frameworks (browser-use, LangChain, Claude Code, Anthropic SDK) +- **Observability** via Logfire auto-instrumentation of all PydanticAI agent calls + +The LLM interaction layer uses PydanticAI exclusively. Three legacy hand-rolled LLM clients (LiteLLM, Instructor, ClaudeCode) were replaced — PydanticAI handles structured output, retries with error feedback, and multi-provider support as maintained infrastructure. The pipeline engine and skillbook/learning loop are untouched. + +| Kept (core IP) | Replaced (commodity plumbing) | +|---|---| +| Pipeline engine (`requires`/`provides`, `async_boundary`, `max_workers`) | LLM client abstraction (3 implementations → PydanticAI agents) | +| Skillbook & learning loop (Reflect → Update → Apply) | Structured output parsing + retries (→ PydanticAI native validation) | +| Step composition (`learning_tail`, pipeline nesting) | RR iteration loop, code extraction, budget tracking (~2,500 lines → PydanticAI agent + tools) | +| Domain-specific prompts | Sub-agent call management (CallBudget → `UsageLimits`) | + +--- + +## Naming + +| Legacy | Current | What it does | +|---|---|---| +| `OfflineACE` | `TraceAnalyser` | Analyse pre-recorded traces → evolve a skillbook | +| `OnlineACE` | `ACE` | Live execution → feedback → learning loop | +| `ACEBase` | `ACERunner` | Shared runner infrastructure (composition, not inheritance from Pipeline) | +| `ACEStepResult` | Removed — use `SampleResult` from the pipeline engine | Unified result type | + +--- + +## Architecture Layers + +The framework separates concerns into four layers: + +| Layer | Location | Responsibility | Example | +|-------|----------|----------------|---------| +| **Protocols** | `ace/protocols/` | Interface contracts | `ReflectorLike.reflect()` | +| **Roles** | `ace/implementations/` | Business logic (LLM calls) | `Reflector`, `RRStep` | +| **Steps** | `ace/steps/` | Context plumbing (extract → call role → put back) | `ReflectStep` | +| **Runners** | `ace/runners/` | Orchestration (sample loop, epoch management) | `ACELiteLLM` | + +**Protocols** define what a role must look like. **Roles** implement the logic. **Steps** adapt between the pipeline's context-based data flow and the role's parameter-based API. **Runners** compose steps into pipelines and iterate over inputs. + +Roles are interchangeable anywhere their protocol is expected — both `Reflector` (simple single-pass) and `RRStep` (recursive multi-iteration) satisfy `ReflectorLike`. The runner and pipeline don't know or care which one is in use. + +--- + +## Core Concepts + +### Sample + +The input unit for ACE. A question with optional context and ground truth: + +```python +@dataclass +class Sample: + question: str + context: str = "" + ground_truth: str | None = None + metadata: dict = field(default_factory=dict) + id: str | None = None +``` + +### ACESample — protocol for step access + +Steps access `ctx.sample.question` uniformly. A `Protocol` makes this duck typing explicit and type-safe. `Sample` satisfies it structurally — no inheritance required. + +### SkillbookView — read-only projection + +The `Skillbook` is mutable — steps add, update, and remove skills. Placing it directly on a `frozen=True` context would allow mutation through the reference, breaking the immutability guarantee. + +`SkillbookView` wraps a `Skillbook` and exposes only read methods (`as_prompt()`, `get_skill()`, `skills()`, `stats()`). Write methods don't exist on the class — calling them raises `AttributeError` at runtime and a type error at check time. + +**Enforcement:** +- **Type checker** — mypy/pyright flags `ctx.skillbook.add_skill(...)` because `SkillbookView` has no such method. +- **Runtime** — `AttributeError` if someone calls a write method anyway. +- **Convention** — the underlying `_sb` is underscore-prefixed. Accessing it is a deliberate violation. + +Steps that only **read** the skillbook (ReflectStep) access `ctx.skillbook` — the view. Steps that **write** the skillbook (AgentStep, UpdateStep, DeduplicateStep, CheckpointStep) receive the real `Skillbook` via constructor injection and use `self.skillbook`. `AgentStep` bumps `used_count`; `UpdateStep` invokes the agentic SkillManager whose tools apply ADD / UPDATE / REMOVE / TAG directly. + +### ACEStepContext — immutable step-to-step data + +Subclass of the pipeline engine's `StepContext`. Carries all step-to-step data for the ACE pipeline. The pipeline engine only knows about `sample` and `metadata`; all ACE-specific fields live here. + +Key fields: + +| Field | Type | Source | +|---|---|---| +| `mode` | `Literal["online", "offline"]` | `"online"` (default) — reserved for downstream steps | +| `sample` | `ACESample \| None` | Set by runner's `_build_context()` | +| `skillbook` | `SkillbookView \| None` | Read-only projection of the real Skillbook | +| `trace` | `object \| None` | Raw execution record — any type, no enforced schema | +| `agent_output` | `AgentOutput \| None` | Produced by `AgentStep` | +| `reflections` | `tuple[ReflectorOutput, ...]` | Produced by `ReflectStep` / `RRStep` | +| `skill_manager_output` | `UpdateBatch \| None` | Produced by `UpdateStep` (audit log of mutations the SM already applied) | +| `injected_skill_ids` | `tuple[str, ...]` | Produced by `AgentStep` — skill IDs rendered into the agent prompt; downstream attribution scope | +| `epoch`, `total_epochs` | `int` | Runner bookkeeping | +| `step_index`, `total_steps` | `int` | Runner bookkeeping | +| `global_sample_index` | `int` | Runner bookkeeping (used by interval steps) | + +The `trace` field holds the raw execution record from any external system — a browser-use `AgentHistoryList`, a LangChain result dict, a Claude Code transcript, or any arbitrary Python object. The Reflector receives the raw trace and is responsible for making sense of it. + +The `reflections` field is a tuple. In single-trace mode, it's a 1-tuple. In batch mode, it holds one `ReflectorOutput` per trace. Downstream steps iterate uniformly — no special-casing. + +### Context vs constructor injection + +| | On the context | Injected via constructor | +|---|---|---| +| **Nature** | Step-to-step data + read-only dependencies | Mutable shared state | +| **Lifetime** | Per-sample (born in `_build_context`, dies after pipeline) | Per-runner (created once, shared across samples) | +| **Immutable?** | Yes — frozen fields, read-only views | No — mutable by design | +| **Examples** | `agent_output`, `reflections`, `skillbook` (view) | `skillbook` (real), `environment`, `dedup_manager` | +| **Validated by engine?** | Yes — `requires`/`provides` | No — runtime error if missing | + +--- + +## Protocols + +Steps depend on protocols, not concrete classes. Each protocol defines the minimal interface a step needs. Concrete implementations satisfy them structurally — no inheritance required. + +| Protocol | Method | Used by | Satisfied by | +|---|---|---|---| +| `AgentLike` | `generate(question, context, skillbook, reflection, **kwargs) → AgentOutput` | `AgentStep` | `Agent` | +| `ReflectorLike` | `reflect(question, agent_output, skillbook, ground_truth, feedback, **kwargs) → ReflectorOutput` | `ReflectStep` | `Reflector`, `RRStep` | +| `SkillManagerLike` | `update_skills(reflections, skillbook, question_context, progress, **kwargs) → SkillManagerOutput` | `UpdateStep` | `SkillManager` | +| `DeduplicationManagerLike` | `get_similarity_report(skillbook) → str \| None` | `DeduplicateStep` | `DeduplicationManager` | + +Roles take a model string directly (e.g. `Agent("gpt-4o-mini")`). Internally each role creates a PydanticAI agent that handles structured output natively — no separate LLM client protocol is needed. + +**Why protocols, not ABC:** Protocols use structural typing (duck typing checked by mypy). A class satisfies a protocol if it has the right methods — no `class Agent(AgentLike)` inheritance needed. Users can pass any object with a matching method, mocks satisfy protocols without ceremony, and steps are decoupled from implementations at the type level. + +--- + +## Roles (Implementations) + +Concrete LLM-based implementations of the role protocols. Live in `ace/implementations/` — fully self-contained. + +| Class | Protocol | Method | What it does | +|---|---|---|---| +| `Agent` | `AgentLike` | `generate()` | Produces answers using the current skillbook of strategies | +| `Reflector` | `ReflectorLike` | `reflect()` | Single-pass analysis of agent outputs to extract lessons | +| `RRStep` | `ReflectorLike` + `StepProtocol` | `reflect()` / `__call__()` | Recursive multi-iteration reflection via PydanticAI agent with tools | +| `SkillManager` | `SkillManagerLike` | `update_skills()` | Transforms reflections into actionable skillbook updates | + +All three share the same constructor pattern: `__init__(self, model: str, *, prompt_template=..., max_retries=3)`. The `model` parameter is resolved via `resolve_model()` to a PydanticAI agent. + +`RRStep` is both a `StepProtocol[ACEStepContext]` (composable in any pipeline) and `ReflectorLike` (usable as a drop-in reflector). It is a subclass of `RecursiveAgent` with `execute_code` and `recurse` tools, plus two-tier compaction and depth-based recursion. See [RR_DESIGN.md](RR_DESIGN.md) for the full Recursive Reflector architecture. + +--- + +## Steps + +Reusable step implementations in `ace/steps/`. Each satisfies `StepProtocol[ACEStepContext]`. Each step does exactly one thing. + +**Design principle: steps are stateless.** A step's `__call__` is a pure function of its constructor arguments and the incoming `ACEStepContext`. No internal counters, no accumulated state between invocations. Run-scoped information (like a global sample index for interval logic) comes from the context. + +### Step summary + +| Step | Requires | Provides | Side effects | `max_workers` | +|---|---|---|---|---| +| **AgentStep** | `sample`, `skillbook` | `agent_output` | None | 1 | +| **EvaluateStep** | `sample`, `agent_output` | `trace` | None | 1 | +| **ReflectStep** | `trace`, `skillbook` | `reflections` | None | 3; `async_boundary = True` | +| **UpdateStep** | `reflections`, `skillbook` | `skill_manager_output` | Agentic SkillManager mutates skillbook directly via ADD / UPDATE / REMOVE / TAG tools; output is an audit log | 1 | +| **DeduplicateStep** | `global_sample_index` | — | Consolidates similar skills | 1 | +| **CheckpointStep** | `global_sample_index` | — | Saves skillbook to disk | 1 | +| **LoadTracesStep** | `sample` | `trace` | None | 1 | +| **PersistStep** | `skillbook` | — | Writes skillbook to external file | 1 | +| **ExportSkillbookMarkdownStep** | `skillbook` | — | Exports skillbook as markdown | 1 | + +**Requires vs Injected:** `Requires` lists context fields (validated by the pipeline engine at construction time). The `skillbook` on the context is a `SkillbookView` (read-only). Steps that **write** to the skillbook receive the real `Skillbook` via constructor injection. + +**`trace` as the universal learning input:** The learning tail's entry point (ReflectStep) requires only `trace` and `skillbook`. In the standard ACE pipeline, `EvaluateStep` bundles structured fields into a `trace` dict. In TraceAnalyser, `_build_context` places the raw trace directly. In integrations, the execute step provides `trace` from its framework's native output. The learning tail is agnostic to trace format. + +Steps with empty `provides` are pure side-effect steps — they mutate shared state (skillbook) or write to external systems (disk) but add no new fields to the context. + +--- + +## Runners + +### Class hierarchy + +``` +ACERunner (shared infrastructure: epoch loop, delegates to Pipeline.run()) +├── TraceAnalyser — [Reflect → Update → Apply] +├── ACE — [Agent → Evaluate → Reflect → Update → Apply] +├── BrowserUse — [BrowserExecute → BrowserToTrace → learning_tail] +├── LangChain — [LangChainExecute → LangChainToTrace → learning_tail] +├── ClaudeCode — [ClaudeCodeExecute → ClaudeCodeToTrace → learning_tail] +└── OpenClaw (script) — [LoadTraces → OpenClawToTrace → learning_tail] + +ACELiteLLM (standalone convenience wrapper — not an ACERunner subclass) +├── ask() — direct Agent call, no pipeline +├── learn() — delegates to lazy-init ACE runner +├── learn_from_traces() — delegates to lazy-init TraceAnalyser +└── learn_from_feedback()— runs learning_tail from last ask() + +RRStep (RecursiveAgent subclass — composable iterative step) +├── __call__() — StepProtocol entry; usable in any runner's pipeline +├── reflect() — ReflectorLike entry; drop-in reflector for runners +└── _run_reflection() — PydanticAI agent with execute_code and recurse tools +``` + +All runners compose a `Pipeline` rather than extending it. + +### ACERunner — shared base + +Encapsulates everything runners have in common: the epoch loop and Iterable validation. Per-sample iteration, error handling, background execution, and checkpoints are all delegated to `Pipeline.run()`. + +Subclasses only override `run()` (public signature) and `_build_context()` (input mapping). + +**Responsibilities:** + +| Concern | Owner | +|---|---| +| Epoch loop + Iterable validation | `ACERunner._run()` | +| Per-sample iteration + error isolation | `Pipeline.run()` | +| Foreground/background split | `Pipeline.run()` (via `async_boundary`) | +| Concurrent workers | `Pipeline.run(workers=N)` | +| Checkpoints | `CheckpointStep` (in the pipeline) | +| Background drain | `ACERunner.wait_for_background()` → `Pipeline.wait_for_background()` | +| Skillbook I/O | `save(path)` on the runner | + +Each sample is independent — no state persists across samples. The skillbook is the only cross-sample coupling. + +**Eventual consistency:** `SkillbookView` is a thin delegation wrapper, not a snapshot — it reads from the live `Skillbook` at call time. When background learning is active, concurrent samples may observe partially-updated skillbook state. This is by design: steps see a best-effort view rather than a point-in-time snapshot. The trade-off is acceptable because (1) the skillbook is LLM prompt context where a few missing or extra skills have negligible impact, (2) serialising reads would eliminate the concurrency benefit, and (3) write steps already run with `max_workers = 1`. + +### TraceAnalyser + +Analyses pre-recorded traces without executing an agent. Runs the learning tail only. Accepts raw trace objects of any type. + +**When to use:** You have execution logs from an external system and want to build or refine a skillbook from historical data. Multi-epoch mode re-processes all traces with the evolving skillbook. + +**Pipeline:** + +``` +[ReflectStep] → [UpdateStep] (SkillManager mutates the skillbook directly) +``` + +No AgentStep, no EvaluateStep. The trace already contains the agent's output and the evaluation feedback. + +**Multi-epoch semantics:** Each epoch re-processes all traces with the current skillbook. Early epochs extract obvious patterns; later epochs refine and consolidate. + +### ACE + +The full live adaptive pipeline. An agent executes, the reflector analyses, the skill manager updates. Optionally evaluates against a `TaskEnvironment` for feedback-driven learning. + +**When to use:** Building a new agent, or running closed-loop learning where the agent improves in real time. + +**Pipeline:** + +``` +[AgentStep] → [EvaluateStep] → [ReflectStep] → [UpdateStep] (SkillManager mutates the skillbook directly) +``` + +A single class handles both single-pass (`epochs=1`) and multi-epoch batch training (`epochs > 1`). The `environment` is optional — when provided, `EvaluateStep` generates feedback. When omitted, the Reflector learns from ground-truth comparison or the agent's reasoning alone. + +### ACELiteLLM — standalone convenience wrapper + +`ACELiteLLM` is not an `ACERunner` subclass. It wraps two different runners (`ACE` and `TraceAnalyser`) and exposes a fundamentally different API: + +| Method | What it does | +|---|---| +| `ask(question, context)` | Direct Agent call — no pipeline. Stores interaction for `learn_from_feedback()` | +| `learn(samples, environment, epochs)` | Delegates to lazy-init ACE runner | +| `learn_from_traces(traces, epochs)` | Delegates to lazy-init TraceAnalyser | +| `learn_from_feedback(feedback, ground_truth)` | Manual single-shot learning from last `ask()` call | + +Runners are cached and invalidated on `load()` (new skillbook object means stale references). + +### Factory methods + +All runners provide a `from_roles` factory that takes pre-built role instances. Integration runners also provide `from_model()` that auto-builds PydanticAI-backed roles from a model string. + +**Common parameters on `from_roles`:** + +| Parameter | Default | Description | +|---|---|---| +| `skillbook` | `Skillbook()` | Starting skillbook | +| `dedup_manager` | `None` | Appends a `DeduplicateStep` | +| `dedup_interval` | `10` | Deduplication frequency | +| `checkpoint_dir` | `None` | Appends a `CheckpointStep` | +| `checkpoint_interval` | `10` | Checkpoint frequency | +| `extra_steps` | `None` | Additional steps appended after the learning tail | + +### `learning_tail()` — reusable learning steps + +Every integration assembles the same `[Reflect → Update → Apply]` suffix. `learning_tail()` returns this standard step list, with optional dedup and checkpoint steps. If the provided reflector already exposes `provides = {'reflections'}` (e.g. `RRStep`), it's inserted directly instead of being wrapped in `ReflectStep`. + +--- + +## Integration Pattern + +External frameworks integrate via composable pipeline steps in `ace/integrations/`. Each integration provides: + +1. **Result type** — an integration-specific dataclass (e.g. `BrowserResult`, `ClaudeCodeResult`) +2. **Execute step** — INJECT skillbook context + EXECUTE the framework, writes to `ctx.trace` +3. **ToTrace step** — converts the integration-specific result into the standardised trace dict + +### Execute → Convert → Learn + +``` +Standard ACE: [Agent → Evaluate] → [Reflect → Update → Apply] + ╰── execute (built-in) ──╯ ╰──────── learn (shared) ──────╯ + provides: trace (dict) ─────────────────► requires: trace + +Browser-use: [BrowserExecute] → [BrowserToTrace] → [Reflect → Update → Apply] + ╰── execute ────╯ ╰── convert ──╯ ╰──────── learn (shared) ──────╯ + provides: trace rewrites trace requires: trace + (BrowserResult) (BrowserResult → dict) + +TraceAnalyser: [_build_context] → [Reflect → Update → Apply] + ╰── sets ctx.trace (raw object) ───────╯ ╰──────── learn (shared) ──────╯ +``` + +The standardised trace dict keys match what `ReflectStep` expects: `question`, `reasoning`, `answer`, `skill_ids`, `feedback`, `ground_truth`. + +### Result types + +| Integration | Result type | Key fields | +|---|---|---| +| Browser-use | `BrowserResult` | `task`, `success`, `output`, `error`, `steps_count`, `duration_seconds`, `cited_skill_ids`, `chronological_steps`, `raw_history` | +| Claude Code | `ClaudeCodeResult` | `task`, `success`, `output`, `execution_trace`, `returncode`, `error` | +| Claude SDK | `ClaudeSDKResult` | `task`, `success`, `output`, `error`, `model`, `stop_reason`, `input_tokens`, `output_tokens`, `tool_calls`, `cited_skill_ids` | +| LangChain | `LangChainResult` | `task`, `output`, `result_type`, `success`, `error`, `intermediate_steps`, `messages`, `raw_result` | + +### Why two steps instead of one + +Splitting execute from trace conversion gives independent testability, reusability (execute step usable standalone), and separation of concerns (framework interaction vs trace formatting). + +### Live vs offline + +| | Integration Runner | TraceAnalyser | +|---|---|---| +| When | Live execution | Post-hoc analysis | +| Agent | Framework runs it | Already ran | +| Feedback | Generated live | Baked into trace | +| Use case | Production deployment | Historical batch learning, debugging | + +Both update the same skillbook. A common workflow: TraceAnalyser builds an initial skillbook from historical data, then an integration runner refines it during live deployment. + +> **MCP Server** is a different pattern. It does not add pipeline steps — it's a thin async layer over `ACELiteLLM` that exposes ACE as an MCP tool provider. See [MCP Server docs](../integrations/mcp.md). + +--- + +## Configuration & Providers + +### Principles + +1. **API keys never appear in ACE APIs** — no `api_key` parameter anywhere. Keys are resolved from the environment by LiteLLM at call time. +2. **Per-role model selection** — Agent, Reflector, and SkillManager can each use different models. +3. **Validate before running** — `ace setup` and `validate_connection()` make a tiny LLM call to verify auth before writing code. + +### Config types + +- **`ModelConfig`** — which model to use for a role (model string, temperature, max_tokens). No secrets. +- **`ACEModelConfig`** — model selection per ACE role. Serialises to/from `ace.toml` (committable, no secrets). + +### Construction paths + +| Constructor | Input | Use case | +|---|---|---| +| `ACELiteLLM.from_setup()` | `ace.toml` + `.env` | Teams, CI, guided setup | +| `ACELiteLLM.from_config(config)` | `ACEModelConfig` object | Per-role model selection in code | +| `ACELiteLLM.from_model("gpt-4o")` | Model string | Quick start, single model | +| `ACELiteLLM("gpt-4o-mini", ...)` | Model string + overrides | Full control | + +### CLI + +| Command | What it does | +|---|---| +| `ace setup` | Interactive wizard: model name, API key, validate, assign per-role models. Saves `.env` + `ace.toml`. | +| `ace models [query]` | Search LiteLLM's model registry (2,600+ models). Filter by `--provider`. | +| `ace providers` | List providers with env var names and key status. | +| `ace validate <model>` | Test a model connection with a tiny LLM call. | + +### File layout + +| File | Secrets? | Committable? | Purpose | +|---|---|---|---| +| `.env` | Yes | No (gitignored) | API keys only | +| `ace.toml` | No | Yes | Model names + parameters per role | + +### Key resolution flow + +``` +API Key: .env → os.environ → LiteLLM reads OPENAI_API_KEY / ANTHROPIC_API_KEY / etc. +Model: ace.toml → ACEModelConfig.for_role("agent") → resolve_model(model) → PydanticAI agent +``` + +### Provider resolution + +ACE model strings follow LiteLLM convention (`provider/model`). The resolver in `ace/providers/pydantic_ai.py` routes them through three paths: + +1. **PydanticAI-native prefix** — strings like `openai:gpt-4o` pass through unchanged +2. **LiteLLM prefix → native provider** — when the first path segment matches a PydanticAI native provider, `/` is rewritten to `:` (e.g. `bedrock/model` → `bedrock:model`) +3. **Fallback** — everything else is prefixed with `litellm:` for the proxy provider + +``` +LiteLLM string → PydanticAI string +───────────────────────────────────────────────── ────────────────────────────────────────── +gpt-4o-mini → litellm:gpt-4o-mini +bedrock/eu.anthropic.claude-haiku-4-5-v1:0 → bedrock:eu.anthropic.claude-haiku-4-5-v1:0 +groq/llama-3.1-70b-versatile → groq:llama-3.1-70b-versatile +openrouter/anthropic/claude-3.5-sonnet → openrouter:anthropic/claude-3.5-sonnet +anthropic/claude-3-5-sonnet-20241022 → anthropic:claude-3-5-sonnet-20241022 +ollama/llama3 → litellm:ollama/llama3 +together_ai/meta-llama/Llama-3-70b → litellm:together_ai/meta-llama/Llama-3-70b +``` + +Mapped LiteLLM prefixes: `anthropic`, `azure`, `azure_ai`, `bedrock`, `cohere`, `deepseek`, `groq`, `mistral`, `openrouter`, `vertex_ai`. All others fall through to `litellm:`. + +Native providers are faster (no proxy hop) and use the provider's own API key env vars directly. Install with extras: `uv add "pydantic-ai-slim[anthropic,openai,bedrock]"`. + +--- + +## Deduplication + +Skill deduplication subsystem in `ace/deduplication/` — fully self-contained. + +| Class | Role | +|---|---| +| `SimilarityDetector` | Computes embeddings, detects similar pairs via cosine similarity | +| `DeduplicationManager` | Coordinates detection and consolidation | + +**Embedding providers:** LiteLLM (remote) or sentence-transformers (local, lazy-loaded). + +**Consolidation operations:** + +| Operation | Effect | +|---|---| +| `MergeOp` | Combine skills — accumulate counters, soft-delete others | +| `DeleteOp` | Soft-delete a redundant skill | +| `KeepOp` | Store a similarity decision so the pair is not flagged again | +| `UpdateOp` | Refine content to differentiate, clear embedding | + +**Pipeline integration:** Deduplication runs as a separate `DeduplicateStep` at a configurable interval, not inside the SkillManager. Appended by factory methods when a `DeduplicationManagerLike` is provided. + +--- + +## Observability + +PydanticAI has first-class Logfire integration. One call auto-instruments everything: + +```python +logfire.configure() +logfire.instrument_pydantic_ai() +``` + +This automatically captures agent runs, tool calls, model requests, structured output validation, and sub-agent delegation — the entire RR execution appears as a structured trace. No custom span-building code. + +**Pipeline integration:** Logfire is OpenTelemetry-based, so pipeline-level spans coexist. Steps that don't use PydanticAI can use `logfire.span()` / `logfire.info()` directly. + +**Setup:** `ace/observability/configure_logfire()` auto-instruments all PydanticAI agents. Opt-in via `ACELiteLLM(logfire=True)`. Config is purely env-based (`LOGFIRE_TOKEN`), no changes to `ace.toml`. + +--- + +## Concurrency + +Both TraceAnalyser and ACE inherit async capabilities from the pipeline engine. No custom async machinery is needed. + +### ReflectStep as async boundary + +`ReflectStep.async_boundary = True` means everything before it (Agent, Evaluate) runs in the foreground, and everything from ReflectStep onwards runs in a background thread pool: + +``` +sample 1: [AgentStep] [EvaluateStep] ──fire──► [ReflectStep] [UpdateStep] +sample 2: [AgentStep] [EvaluateStep] ──fire──► ... + ↑ + async_boundary +``` + +### Concurrency knobs + +| Knob | Where | Effect | +|---|---|---| +| `ReflectStep.max_workers = 3` | Step class attribute | Up to 3 reflections in parallel | +| `UpdateStep.max_workers = 1` | Step class attribute | Serialises skill manager LLM calls AND skillbook writes (SM tools mutate in place) | +| `wait_for_background(timeout)` | Runner method | Blocks until background threads drain | + +### Cancellation + +ACE inherits cancellation from the pipeline engine. Pass a `CancellationToken` to `run()`. The pipeline checks it before each foreground step. Within a step, PydanticAI's async runtime handles cancellation natively. The token flows via `contextvars.ContextVar` — no parameter changes needed across layers. See [PIPELINE_DESIGN.md § Cancellation](PIPELINE_DESIGN.md#cancellation). + +--- + +## Error Handling + +Follows the pipeline engine's error model without additions. + +- **Per-sample isolation:** A failing sample does not abort the run. The exception is recorded in `SampleResult.error` and `SampleResult.failed_at`. +- **Background failures:** Captured and attached to `SampleResult` by the pipeline engine. +- **No retry logic in the runner.** Retries are the responsibility of individual steps (e.g., PydanticAI's built-in retry with error feedback). + +--- + +## Directory Structure + +``` +ace/ + __init__.py ← Public API re-exports + core/ + context.py ← ACEStepContext, SkillbookView, ACESample + insight_source.py ← TraceIdentity, TraceReference, InsightSource + outputs.py ← AgentOutput, ReflectorOutput, SkillManagerOutput + skillbook.py ← Skill, Skillbook, SimilarityDecision + environments.py ← Sample, TaskEnvironment, SimpleEnvironment + protocols/ ← Role protocols (one file per protocol) + agent.py, reflector.py, skill_manager.py, deduplication.py + implementations/ ← PydanticAI-backed role implementations + agent.py, reflector.py, skill_manager.py, helpers.py, prompts.py + steps/ ← Pipeline steps (one file per class) + __init__.py ← learning_tail() helper + agent.py, evaluate.py, reflect.py, update.py, + apply.py, deduplicate.py, checkpoint.py, + load_traces.py, persist.py, export_markdown.py, observability.py + runners/ ← Runner classes + base.py ← ACERunner + trace_analyser.py, ace.py, browser_use.py, langchain.py, + claude_code.py, litellm.py + integrations/ ← Integration steps (execute + result + converter) + browser_use.py, langchain.py, claude_code.py, claude_sdk.py + openclaw/ ← OpenClaw trace converter + mcp/ ← Optional MCP server + providers/ ← PydanticAI model resolution + pydantic_ai.py, config.py, registry.py + deduplication/ ← Skill deduplication subsystem + detector.py, manager.py, operations.py, prompts.py + rr/ ← Recursive Reflector (PydanticAI agent) + observability/ ← Logfire configuration +``` + +### Key modules + +| Module | Contents | +|---|---| +| `ace/core/` | `ACEStepContext`, `SkillbookView`, `Skillbook`, `AgentOutput`, `ReflectorOutput`, `InsightSource` | +| `ace/protocols/` | `AgentLike`, `ReflectorLike`, `SkillManagerLike` protocols | +| `ace/implementations/` | PydanticAI-backed `Agent`, `Reflector`, `SkillManager` | +| `ace/steps/` | All pipeline steps + `learning_tail()` | +| `ace/runners/` | `ACERunner`, `TraceAnalyser`, `ACE`, `BrowserUse`, `LangChain`, `ClaudeCode`, `ACELiteLLM` | +| `ace/providers/` | `resolve_model`, `ACEModelConfig`, `validate_connection` | +| `ace/steps/rr_step.py` | `RRStep` (RecursiveAgent subclass), `RRConfig`, `TraceSandbox` | +| `ace/core/recursive_agent.py` | `RecursiveAgent`, `AgenticConfig`, `AgenticDeps`, compaction, recursion, `usage_callback` hook | +| `ace/core/metered_model.py` | `MeteredModel` — pydantic-ai `WrapperModel` that fires the `usage_callback` once per request | +| `ace/integrations/` | Execute steps, result types, ToTrace converters; MCP server | +| `ace/deduplication/` | Dedup subsystem (detector, manager, operations) | +| `ace/observability/` | Logfire configuration (`configure_logfire()`) | + +--- + +## Future Directions + +Issues acknowledged but deferred. + +**Streaming / lazy iteration:** `_run()` eagerly materializes the full iterable before passing to `Pipeline.run()`. True streaming would require the pipeline to accept an iterator. Deliberate simplification — revisit if memory pressure from large single-pass runs becomes real. + +**Builder API for custom pipelines:** The current API offers two extremes: factory methods that hide the pipeline, and manual construction that requires understanding step contracts. A builder could bridge this gap, but `learning_tail()` covers the most common customisation (custom execute step + standard learning). Worth pursuing when users hit friction with manual wiring. + +**Skillbook rollback and versioning:** Currently the skillbook is mutated in place with no undo. A lightweight versioning mechanism (snapshotting at epoch boundaries, `rollback(to_version)`) would enable automatic revert when metrics degrade. Deferred because checkpoints cover the common recovery scenario. + +**LiteLLM proxy base URL support:** Users running a LiteLLM proxy may need `api_base` configuration. Deferred because all current users connect directly to providers. diff --git a/docs/design/ACE_DECISIONS.md b/docs/design/ACE_DECISIONS.md new file mode 100644 index 0000000000000000000000000000000000000000..3290c2d887a3865b0972c2f73083a4e3744e9e54 --- /dev/null +++ b/docs/design/ACE_DECISIONS.md @@ -0,0 +1,106 @@ +# Design Decisions + +> What was considered and rejected for ACE and the PydanticAI migration — and why. + +For architecture and concepts, see [ACE_ARCHITECTURE.md](ACE_ARCHITECTURE.md). +For code reference and examples, see [ACE_REFERENCE.md](ACE_REFERENCE.md). + +--- + +## PydanticAI Migration + +### What we replaced and why + +ACE had three hand-rolled LLM client implementations (LiteLLMClient, InstructorClient, ClaudeCodeLLMClient) with inconsistent retry/validation behavior, ~3,500 lines of custom agent-loop plumbing in the Recursive Reflector, and manual code extraction via regex. PydanticAI handles all of this as maintained infrastructure. + +| Before | After | +|---|---| +| 3 LLM client implementations | PydanticAI agents inside roles | +| Manual JSON extraction + Pydantic parse | PydanticAI validates via tool-call schema, retries with error feedback | +| 3 blind retries or Instructor | PydanticAI native (configurable, with error context) | +| LiteLLM wrapper for provider support | PydanticAI (native support for 15+ providers, wraps LiteLLM internally) | +| Custom SubRunner loop (~400 lines) | PydanticAI's agent loop — LLM calls tools until it produces structured output | +| 200 lines of regex code extraction | Tool args are pre-parsed (code arrives as `execute_code` parameter) | +| Inner pipeline steps (~500 lines) | ~50 lines of tool definitions | +| CallBudget + SubAgentLLM (~200 lines) | `ctx.usage` shared budget + delegate agent | +| Custom `RRIterationContext` | PydanticAI manages message state internally | +| Manual Opik span building (~356 lines) | `logfire.instrument_pydantic_ai()` auto-instruments everything | + +**Net result for RR:** ~3,500 lines → ~1,000 lines (sandbox + prompts + trimming + agent definition). ~2,500 lines of loop/extraction/budget/context plumbing deleted. + +### What we kept + +- **Pipeline engine** (`pipeline/`) — `requires`/`provides` contracts, `async_boundary`, per-step `max_workers`, `SampleResult` error isolation. No framework offers this combination. +- **Skillbook & learning loop** — Reflect → Update → Apply → Deduplicate. This is core IP. +- **Step composition** — `learning_tail()`, pipeline-as-step nesting, `SkillbookView` read/write split. +- **Domain-specific prompts** — tightly coupled to skillbook format and ACE's reflection strategy. +- **All pipeline steps** — they depend on protocols, not implementations. Completely unchanged. + +### Provider resolution design + +The resolver routes LiteLLM model strings to PydanticAI through three paths: + +1. **PydanticAI-native prefix** — pass through unchanged +2. **LiteLLM prefix → native provider** — rewrite `/` to `:` when the prefix matches a native provider. This is necessary because PydanticAI's `litellm` provider uses an OpenAI-compatible HTTP client under the hood, which doesn't work for providers with non-OpenAI APIs (Bedrock via SigV4, Anthropic's native API, etc.). +3. **Fallback** — prefix with `litellm:` for the proxy provider + +User-facing API is unchanged — same LiteLLM model strings as before. + +--- + +## ACE Architecture Decisions + +**Runner extends Pipeline:** +Making TraceAnalyser and ACE subclasses of `Pipeline` was considered. Rejected — the runner is not a pipeline. It owns the epoch loop. Composition (`self.pipeline`) keeps responsibilities separate. + +**Cross-sample state (reflection window):** +A rolling window of recent reflections that persists across samples was considered, with variants: on the runner, on `StepContext`, on step instances, via a shared mediator object. All rejected — each sample should be independent. The only cross-sample coupling is the skillbook itself. Adding a reflection window complicates the model (reset between epochs, eventual consistency with background steps, ordering issues with concurrent workers) for marginal benefit. + +**Separate Online and Offline classes:** +Keeping two runner classes for single-pass and multi-epoch was considered. Rejected — the only difference is `epochs=1` vs `epochs > 1`, which is a parameter, not a class distinction. ACE handles both. TraceAnalyser is a separate class because its input type is fundamentally different (raw traces vs `Sample + Environment`). + +**Structured Trace dataclass:** +A `@dataclass Trace` with typed fields (`task`, `output`, `feedback`, `reasoning`, etc.) was considered. Rejected — it imposes a schema on trace data that doesn't match reality. External frameworks produce wildly different trace shapes (browser-use `AgentHistoryList`, LangChain result dicts, Claude Code transcripts). Forcing them through a common dataclass means either losing information or adding catch-all `metadata` buckets that defeat the purpose of typing. Instead, `ctx.trace` is `object | None` and the Reflector makes sense of whatever it receives. + +**Steps that accept both traces and samples:** +Making ReflectStep and UpdateStep polymorphic over input type was considered. Rejected — steps always receive `StepContext` with the same named fields. The runner (`_build_context`) is responsible for building the context correctly. + +**Observability in the runner:** +Keeping observability logic in `ACERunner._track_observability_data()` was considered. Rejected — it mixes concerns. Observability is handled by Logfire auto-instrumentation. + +**Custom AsyncLearningPipeline:** +The legacy `ace/async_learning.py` implements a manual thread pool with reflector and skill manager queues. Rejected — the pipeline engine's `async_boundary` and `max_workers` provide the same functionality with less code and consistent semantics. + +**Per-integration pipeline classes:** +Having each integration define its own pipeline class was considered. Rejected — every integration pipeline has the same learning tail; only the execute step differs. Instead, integrations provide execute steps that compose into an `ACERunner` subclass, reusing the shared `_run()` loop. + +**Checkpoints in the runner:** +Having the runner own checkpoint logic (via `run()` parameters) was considered. Rejected — a `CheckpointStep` at the end of the pipeline tail keeps checkpointing within the pipeline formalism. Configuration belongs at construction time (factory methods), not at call time (`run()`). + +**Mutable Skillbook directly on the context:** +Storing the real `Skillbook` as a field on `ACEStepContext` was the initial design. Rejected — `StepContext` is frozen, but `Skillbook` is mutable. Placing it on the context creates the illusion of immutability while allowing any step to mutate shared state through the reference. Instead, the context carries a `SkillbookView` (read-only projection). Write steps receive the real `Skillbook` via constructor injection. + +**Injection is ground truth; citation dropped.** +Earlier the Agent "cited" skills by writing `[skill-id]` markers in its reasoning, the Reflector scanned the text to produce `skill_tags`, and the SkillManager consumed those tags. Rejected — citation scanning does not scale: it is fragile (regex over free-form text), biased (agents forget to cite skills they used), and asks the Reflector to dictate downstream state mutation. Replaced with injection-based attribution: the `AgentStep` records `ctx.injected_skill_ids` (the set of active skills rendered into the agent prompt) and bumps `used_count` on each skill. The SkillManager — not the Reflector — now decides helpful/harmful/neutral per injected skill, using atomic `tag_skill` / `remove_skill` tool calls against the real Skillbook. The Reflector produces pure analysis; `skill_tags` and the `SkillTag` output type were removed. + +**SkillManager mutates directly; Reflector is analysis-only.** +The old SkillManager was a one-shot PydanticAI agent that emitted an `UpdateBatch` of planned operations, and a separate `ApplyStep` applied them to the skillbook. Rejected — two-phase (plan → apply) forced the LLM to commit to decisions without inspecting the skillbook, and serialisation of operations meant the agent could not dedupe-before-ADD, inspect counters, or sandbox-verify candidate strategies. Replaced with an agentic SkillManager built on `RecursiveAgent` with atomic mutation tools (`add_skill`, `update_skill`, `remove_skill`, `tag_skill`) and read-only inspection tools (`search_skills`, `read_skill`). Tools operate on the real `Skillbook` immediately — there is no staging. `ApplyStep` was deleted; `UpdateStep` is the sole SM invocation and the skillbook is already mutated when it returns. `SkillManagerOutput` now carries a post-hoc audit trail of the operations the tools executed, not a plan to be applied. `UpdateStep.max_workers=1` is preserved and now guards the mutation critical section in addition to serialising LLM calls. `AgenticConfig.max_requests` controls loop budget; `max_requests=1` approximates the old one-shot behaviour. `UpdateBatch` / `apply_update` / `_apply_operation` are retained for offline reconstruction and tests but the online path no longer flows through them. + +**Instructor auto-wrapping in implementations:** +The old `ace/roles.py` auto-wrapped LLM clients with Instructor if `complete_structured` was missing. Rejected — PydanticAI handles structured output natively via its `result_type` parameter. + +**Recursive Reflector (initial rejection, now implemented):** +The old `ace/reflector/` subsystem supports recursive mode. Initially rejected for `ace` due to complexity. Now implemented as `RRStep` — a PydanticAI agent-based step that runs an iterative REPL loop. Satisfies both `StepProtocol[ACEStepContext]` and `ReflectorLike`. + +**Observability decorator on implementations:** +The old `ace/roles.py` uses `@maybe_track()` decorators for Opik tracing on every role method. Rejected — Logfire auto-instrumentation handles observability. Per-method decorators would double-count and create coupling. + +**Deduplication inside SkillManager:** +The old `ace/roles.py` SkillManager integrates with `DeduplicationManager` directly. Rejected — deduplication is now a separate `DeduplicateStep` in the pipeline. Cleaner separation: the SkillManager only produces output, deduplication runs at a configurable interval. + +**Shared `ace/features.py` module:** +A centralized feature detection module was considered. Rejected — the only code that needs it is `deduplication/detector.py`, which uses a local `_has(module)` helper. A shared module would add a file for a single 4-line function. + +**Separate wrapper classes for integration runners:** +Separate convenience classes (`ACEAgent`, `ACELangChain`, `ACEClaudeCode`) wrapping the runners were the initial design. Rejected — the wrappers only added `from_model()` and a few lifecycle helpers, which fit naturally on the runner class itself. Two classes for one concept forces users to choose between them. Exception: `ACELiteLLM`, which wraps two runners and exposes a fundamentally different API. + diff --git a/docs/design/ACE_REFERENCE.md b/docs/design/ACE_REFERENCE.md new file mode 100644 index 0000000000000000000000000000000000000000..11d12bf1715bda4ed36b9f5f40d5eba62ba0b9c1 --- /dev/null +++ b/docs/design/ACE_REFERENCE.md @@ -0,0 +1,1119 @@ +# ACE Code Reference + +> Full code examples, API signatures, step implementations, and usage patterns. + +For architecture and concepts, see [ACE_ARCHITECTURE.md](ACE_ARCHITECTURE.md). +For design decisions and rejected alternatives, see [ACE_DECISIONS.md](ACE_DECISIONS.md). + +--- + +## Public API + +All pipeline primitives, ACE steps, and context types are importable from `ace`: + +```python +# Pipeline engine +from ace import Pipeline, Branch, MergeStrategy, StepProtocol, SampleResult + +# ACE context +from ace import ACEStepContext, SkillbookView + +# Runner base class (for custom runners) +from ace import ACERunner + +# Core steps +from ace import ( + AgentStep, EvaluateStep, ReflectStep, UpdateStep, + DeduplicateStep, CheckpointStep, LoadTracesStep, ExportSkillbookMarkdownStep, + ObservabilityStep, PersistStep, learning_tail, +) +``` + +Integration steps live in `ace.integrations` (they have framework-specific dependencies): + +```python +from ace.integrations.browser_use import BrowserExecuteStep, BrowserToTrace +from ace.integrations.langchain import LangChainExecuteStep, LangChainToTrace +from ace.integrations.claude_code import ClaudeCodeExecuteStep, ClaudeCodeToTrace +from ace.integrations.claude_sdk import ClaudeSDKExecuteStep, ClaudeSDKToTrace +from ace.integrations.openclaw import OpenClawToTraceStep +``` + +Every runner also exposes a `build_steps()` classmethod that returns the step list it would compose internally. + +--- + +## Core Type Definitions + +### Sample + +```python +@dataclass +class Sample: + question: str + context: str = "" + ground_truth: str | None = None + metadata: dict = field(default_factory=dict) + id: str | None = None +``` + +### ACESample protocol + +```python +class ACESample(Protocol): + """Minimal interface that Sample satisfies.""" + + @property + def question(self) -> str: ... + + @property + def context(self) -> str: ... + + @property + def ground_truth(self) -> str | None: ... + + @property + def metadata(self) -> dict: ... +``` + +### SkillbookView + +```python +class SkillbookView: + """Read-only projection of a Skillbook. Safe on a frozen context.""" + + __slots__ = ("_sb",) + + def __init__(self, skillbook: Skillbook) -> None: + self._sb = skillbook + + def as_prompt(self) -> str: + return self._sb.as_prompt() + + def get_skill(self, skill_id: str) -> Skill | None: + return self._sb.get_skill(skill_id) + + def skills(self, include_invalid: bool = False) -> list[Skill]: + return self._sb.skills(include_invalid=include_invalid) + + def stats(self) -> dict[str, object]: + return self._sb.stats() + + def __len__(self) -> int: + return len(self._sb.skills()) + + def __iter__(self): + return iter(self._sb.skills()) + + def __repr__(self) -> str: + return f"SkillbookView({len(self)} skills)" +``` + +### ACEStepContext + +```python +@dataclass(frozen=True) +class ACEStepContext(StepContext): + """Immutable context for the ACE pipeline. + + The skillbook field is a SkillbookView (read-only). Steps that need to + write to the skillbook receive the real Skillbook via constructor injection. + """ + + sample: ACESample | None = None + skillbook: SkillbookView | None = None + trace: object | None = None + agent_output: AgentOutput | None = None + reflections: tuple[ReflectorOutput, ...] = () + skill_manager_output: UpdateBatch | None = None + epoch: int = 1 + total_epochs: int = 1 + step_index: int = 0 + total_steps: int | None = None + global_sample_index: int = 0 +``` + +--- + +## Protocol Definitions + +All protocols live in `ace/protocols/` (one file per protocol, re-exported from `__init__.py`). + +```python +class AgentLike(Protocol): + def generate(self, question: str, context: str, skillbook: SkillbookView, + reflection: str | None = None, **kwargs) -> AgentOutput: ... + +class ReflectorLike(Protocol): + def reflect(self, question: str, agent_output: AgentOutput, skillbook: SkillbookView, + ground_truth: str | None = None, feedback: str | None = None, + **kwargs) -> ReflectorOutput: ... + +class SkillManagerLike(Protocol): + def update_skills(self, reflections: tuple[ReflectorOutput, ...], + skillbook: SkillbookView, question_context: str, + progress: str, **kwargs) -> SkillManagerOutput: ... + +class DeduplicationManagerLike(Protocol): + def get_similarity_report(self, skillbook: Skillbook) -> str | None: ... +``` + +--- + +## Step Implementations + +### AgentStep + +```python +class AgentStep: + requires = frozenset({"sample", "skillbook"}) + provides = frozenset({"agent_output", "injected_skill_ids"}) + + def __init__(self, agent: AgentLike, skillbook: Skillbook) -> None: + self.agent = agent + self.skillbook = skillbook + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + injected_ids = tuple(s.id for s in self.skillbook.skills()) + agent_output = self.agent.generate( + question=ctx.sample.question, + context=ctx.sample.context, + skillbook=ctx.skillbook, # SkillbookView (read-only) + sample=ctx.sample, + ) + self.skillbook.mark_used(injected_ids) + return ctx.replace( + agent_output=agent_output, + injected_skill_ids=injected_ids, + ) +``` + +### EvaluateStep + +Bridges the execute head (typed ACE objects) to the learning tail (raw traces). Optionally evaluates against a `TaskEnvironment`. + +```python +class EvaluateStep: + requires = frozenset({"sample", "agent_output"}) + provides = frozenset({"trace"}) + + def __init__(self, environment: TaskEnvironment | None = None) -> None: + self.environment = environment + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + trace = { + "question": ctx.sample.question, + "context": ctx.sample.context, + "ground_truth": ctx.sample.ground_truth, + "reasoning": ctx.agent_output.reasoning, + "answer": ctx.agent_output.final_answer, + "skill_ids": ctx.agent_output.skill_ids, + } + if self.environment: + result = self.environment.evaluate( + sample=ctx.sample, agent_output=ctx.agent_output, + ) + trace["feedback"] = result.feedback + return ctx.replace(trace=trace) +``` + +### ReflectStep + +Handles two trace formats: (1) dict from EvaluateStep — extracts known fields; (2) any other object from TraceAnalyser or integrations — passes raw trace via `**kwargs`. + +```python +class ReflectStep: + requires = frozenset({"trace", "skillbook"}) + provides = frozenset({"reflections"}) + + async_boundary = True + max_workers = 3 + + def __init__(self, reflector: ReflectorLike) -> None: + self.reflector = reflector + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + trace = ctx.trace + + if isinstance(trace, dict): + agent_output = AgentOutput( + reasoning=trace.get("reasoning", ""), + final_answer=trace.get("answer", ""), + skill_ids=trace.get("skill_ids", []), + ) + reflection = self.reflector.reflect( + question=trace.get("question", ""), + agent_output=agent_output, + skillbook=ctx.skillbook, + ground_truth=trace.get("ground_truth"), + feedback=trace.get("feedback"), + ) + else: + reflection = self.reflector.reflect( + question="", + agent_output=AgentOutput(reasoning="", final_answer=""), + skillbook=ctx.skillbook, + trace=trace, + ) + + return ctx.replace(reflections=(reflection,)) +``` + +### UpdateStep + +Runs the agentic `SkillManager`. The SM's tools mutate the real `Skillbook` +directly; the returned ``skill_manager_output`` on the context is the +post-hoc audit log. There is **no** separate ``ApplyStep`` — the skillbook +already reflects the changes when ``UpdateStep`` returns. + +```python +class UpdateStep: + requires = frozenset({"reflections", "skillbook"}) + provides = frozenset({"skill_manager_output"}) + + max_workers = 1 + + def __init__( + self, skill_manager: SkillManagerLike, skillbook: Skillbook + ) -> None: + self.skill_manager = skill_manager + self.skillbook = skillbook + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + output = self.skill_manager.update_skills( + reflections=ctx.reflections, + skillbook=self.skillbook, # real Skillbook — SM tools mutate it + question_context=..., + progress=..., + injected_skill_ids=ctx.injected_skill_ids, + ) + return ctx.replace(skill_manager_output=output.update) +``` + +### DeduplicateStep + +Optional — consolidates similar skills at a configurable interval. + +```python +class DeduplicateStep: + requires = frozenset({"global_sample_index"}) + provides = frozenset() + + max_workers = 1 + + def __init__(self, manager: DeduplicationManagerLike, skillbook: Skillbook, *, interval: int = 10) -> None: + self.manager = manager + self.skillbook = skillbook + self.interval = interval + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + if ctx.global_sample_index % self.interval != 0: + return ctx + report = self.manager.get_similarity_report(self.skillbook) + if report: + logger.info("DeduplicateStep: similarity report at sample %d:\n%s", + ctx.global_sample_index, report) + return ctx +``` + +### CheckpointStep + +Optional — periodically saves the skillbook to disk. + +```python +class CheckpointStep: + requires = frozenset({"global_sample_index"}) + provides = frozenset() + + def __init__(self, directory: str | Path, skillbook: Skillbook, *, interval: int = 10) -> None: + self.directory = Path(directory) + self.skillbook = skillbook + self.interval = interval + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + if ctx.global_sample_index % self.interval != 0: + return ctx + self.directory.mkdir(parents=True, exist_ok=True) + self.skillbook.save_to_file(str(self.directory / f"checkpoint_{ctx.global_sample_index}.json")) + self.skillbook.save_to_file(str(self.directory / "latest.json")) + return ctx +``` + +### LoadTracesStep + +Generic JSONL file loader — reads a file path from `ctx.sample`, parses each line as JSON. + +```python +class LoadTracesStep: + requires = frozenset({"sample"}) + provides = frozenset({"trace"}) + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + path = Path(ctx.sample) + events: list[dict] = [] + for line in path.read_text().splitlines(): + line = line.strip() + if not line: + continue + try: + events.append(json.loads(line)) + except json.JSONDecodeError: + continue + return ctx.replace(trace=events) +``` + +### PersistStep + +Writes the current skillbook to an external file (e.g. `CLAUDE.md` for Claude Code). + +```python +class PersistStep: + requires = frozenset({"skillbook"}) + provides = frozenset() + + def __init__(self, target_path: str | Path, skillbook: Skillbook) -> None: + self.target_path = Path(target_path) + self.skillbook = skillbook + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + self.skillbook.save_to_file(str(self.target_path)) + return ctx +``` + +### ExportSkillbookMarkdownStep + +Exports the skillbook as a human-readable markdown file, grouped by section. + +```python +class ExportSkillbookMarkdownStep: + requires = frozenset({"skillbook"}) + provides = frozenset() + + def __init__(self, path: str | Path, skillbook: Skillbook) -> None: + self.path = Path(path) + self.skillbook = skillbook + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + # Rewrites the markdown file from the current skillbook state + ... + return ctx +``` + +--- + +## Factory Methods + +### `learning_tail()` — reusable learning steps + +```python +# ace/steps/__init__.py + +def learning_tail( + reflector: ReflectorLike, + skill_manager: SkillManagerLike, + skillbook: Skillbook, + *, + dedup_manager: DeduplicationManagerLike | None = None, + dedup_interval: int = 10, + checkpoint_dir: str | Path | None = None, + checkpoint_interval: int = 10, +) -> list[StepProtocol[ACEStepContext]]: + """Return the standard ACE learning steps.""" + steps: list[StepProtocol[ACEStepContext]] = [ + ReflectStep(reflector), + UpdateStep(skill_manager, skillbook), + ] + if dedup_manager: + steps.append(DeduplicateStep(dedup_manager, skillbook, interval=dedup_interval)) + if checkpoint_dir: + steps.append(CheckpointStep(checkpoint_dir, skillbook, interval=checkpoint_interval)) + return steps +``` + +### TraceAnalyser `from_roles` + +```python +@classmethod +def from_roles(cls, *, reflector, skill_manager, skillbook=None, + dedup_manager=None, dedup_interval=10, + checkpoint_dir=None, checkpoint_interval=10, + extra_steps=None): + skillbook = skillbook or Skillbook() + steps = learning_tail( + reflector, skill_manager, skillbook, + dedup_manager=dedup_manager, dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, checkpoint_interval=checkpoint_interval, + ) + if extra_steps: + steps.extend(extra_steps) + return cls(pipeline=Pipeline(steps), skillbook=skillbook) +``` + +### ACE `from_roles` + +```python +@classmethod +def from_roles(cls, *, agent, reflector, skill_manager, environment=None, + skillbook=None, dedup_manager=None, dedup_interval=10, + checkpoint_dir=None, checkpoint_interval=10, + extra_steps=None): + skillbook = skillbook or Skillbook() + steps = [ + AgentStep(agent, skillbook), + EvaluateStep(environment), + *learning_tail( + reflector, skill_manager, skillbook, + dedup_manager=dedup_manager, dedup_interval=dedup_interval, + checkpoint_dir=checkpoint_dir, checkpoint_interval=checkpoint_interval, + ), + ] + if extra_steps: + steps.extend(extra_steps) + return cls(pipeline=Pipeline(steps), skillbook=skillbook) +``` + +--- + +## Runner Implementations + +### ACERunner base + +```python +class ACERunner: + """Shared runner infrastructure for all ACE runners.""" + + def __init__(self, pipeline: Pipeline, skillbook: Skillbook) -> None: + self.pipeline = pipeline + self.skillbook = skillbook + + def save(self, path: str) -> None: + self.skillbook.save_to_file(path) + + def wait_for_background(self, timeout: float | None = None) -> None: + self.pipeline.wait_for_background(timeout) + + @property + def learning_stats(self) -> dict: + return self.pipeline.background_stats() +``` + +### Generic run loop (`_run`) + +```python +def _run(self, items, *, epochs, wait=True, **kwargs) -> list[SampleResult]: + if epochs > 1 and not isinstance(items, Sequence): + raise ValueError("Multi-epoch requires a Sequence, not a consumed Iterable.") + + results: list[SampleResult] = [] + n = len(items) if isinstance(items, Sequence) else None + + for epoch in range(1, epochs + 1): + contexts = [ + self._build_context(item, epoch=epoch, total_epochs=epochs, + index=idx, total=n, + global_sample_index=(epoch - 1) * n + idx if n is not None else idx, + **kwargs) + for idx, item in enumerate(items, start=1) + ] + epoch_results = self.pipeline.run(contexts) + results.extend(epoch_results) + + if wait: + self.pipeline.wait_for_background() + return results +``` + +### TraceAnalyser + +```python +class TraceAnalyser(ACERunner): + """Analyse pre-recorded traces to build a skillbook.""" + + @classmethod + def from_roles(cls, *, reflector, skill_manager, skillbook=None, **kwargs) -> "TraceAnalyser": ... + + def run(self, traces: Sequence[Any], epochs: int = 1, *, wait: bool = True) -> list[SampleResult]: + return self._run(traces, epochs=epochs, wait=wait) + + def _build_context(self, raw_trace, *, epoch, total_epochs, index, total, + global_sample_index) -> ACEStepContext: + return ACEStepContext( + skillbook=SkillbookView(self.skillbook), + trace=raw_trace, + metadata={...}, # inferred trace identity for provenance + epoch=epoch, total_epochs=total_epochs, + step_index=index, total_steps=total, + global_sample_index=global_sample_index, + ) +``` + +### ACE + +```python +class ACE(ACERunner): + """Live adaptive pipeline: Agent → Evaluate → Reflect → Update → Apply.""" + + @classmethod + def from_roles(cls, *, agent, reflector, skill_manager, + environment=None, skillbook=None, **kwargs) -> "ACE": ... + + def run(self, samples, epochs=1, *, wait=True) -> list[SampleResult]: + return self._run(samples, epochs=epochs, wait=wait) + + def _build_context(self, sample, *, epoch, total_epochs, index, total, + global_sample_index, **_) -> ACEStepContext: + return ACEStepContext( + sample=sample, + skillbook=SkillbookView(self.skillbook), + metadata={...}, + epoch=epoch, total_epochs=total_epochs, + step_index=index, total_steps=total, + global_sample_index=global_sample_index, + ) +``` + +### Integration runner pattern + +```python +class BrowserUse(ACERunner): + """Browser-use agent with ACE learning pipeline.""" + + @classmethod + def from_roles(cls, *, browser_llm, reflector, skill_manager, + skillbook=None, **kwargs): + skillbook = skillbook or Skillbook() + steps = [ + BrowserExecuteStep(browser_llm), + BrowserToTrace(), + *learning_tail(reflector, skill_manager, skillbook, **kwargs), + ] + return cls(pipeline=Pipeline(steps), skillbook=skillbook) + + @classmethod + def from_model(cls, browser_llm, *, ace_model="gpt-4o-mini", + ace_max_tokens=2048, ace_temperature=0.0, **kwargs) -> BrowserUse: + return cls.from_roles( + browser_llm=browser_llm, + reflector=Reflector(ace_model), + skill_manager=SkillManager(ace_model), + **kwargs, + ) + + def run(self, tasks, epochs=1, *, wait=True): + return self._run(tasks, epochs=epochs, wait=wait) + + def _build_context(self, task, *, epoch, total_epochs, index, total, + global_sample_index, **_): + return ACEStepContext( + sample=task, # raw string — not wrapped in Sample + skillbook=SkillbookView(self.skillbook), + epoch=epoch, total_epochs=total_epochs, + step_index=index, total_steps=total, + global_sample_index=global_sample_index, + ) +``` + +### ACELiteLLM + +```python +class ACELiteLLM: + def __init__(self, model="gpt-4o-mini", *, skillbook=None, environment=None, + reflector=None, skill_manager=None, ...): + self.agent = Agent(model) + self.reflector = reflector or Reflector(model) + self.skill_manager = skill_manager or SkillManager(model) + self._skillbook = skillbook or Skillbook() + self.environment = environment + self._ace: ACE | None = None + self._analyser: TraceAnalyser | None = None + + @classmethod + def from_model(cls, model="gpt-4o-mini", *, max_tokens=2048, + temperature=0.0, **kwargs) -> ACELiteLLM: + return cls(model, **kwargs) + + def ask(self, question, context="") -> str: + """Direct Agent call — no pipeline. Stores interaction for learn_from_feedback().""" + ... + + def learn(self, samples, environment=None, epochs=1, *, wait=True): + """Delegate to lazy-init ACE runner.""" + return self._get_ace(environment).run(samples, epochs=epochs, wait=wait) + + def learn_from_traces(self, traces, epochs=1, *, wait=True): + """Delegate to lazy-init TraceAnalyser.""" + return self._get_analyser().run(traces, epochs=epochs, wait=wait) + + def learn_from_feedback(self, feedback, ground_truth=None) -> bool: + """Manual single-shot learning from last ask() call.""" + ... + + def load(self, path): + """Load skillbook — invalidates cached runners (stale refs).""" + self._skillbook = Skillbook.load_from_file(path) + self._ace = None + self._analyser = None +``` + +--- + +## Role Implementations + +### Agent + +Produces answers using the current skillbook. Formats the prompt, calls PydanticAI with `AgentOutput` as the structured result type, extracts cited skill IDs via `extract_cited_skill_ids()`. + +```python +agent = Agent("gpt-4o-mini") +output = agent.generate( + question="What is the capital of France?", + context="Answer concisely", + skillbook=skillbook, +) +# output.final_answer == "Paris" +# output.skill_ids == ["geography-00001"] +``` + +### Reflector + +Single-pass analysis. Builds a skillbook excerpt from cited IDs, formats the prompt, calls PydanticAI with `ReflectorOutput`. + +```python +reflector = Reflector("gpt-4o-mini") +reflection = reflector.reflect( + question="What is 2+2?", + agent_output=agent_output, + skillbook=skillbook, + ground_truth="4", + feedback="Correct!", +) +# reflection.key_insight +``` + +### SkillManager (agentic) + +A `RecursiveAgent` subclass with atomic mutation tools. Tools operate on the real +`Skillbook` directly; there is no staging and no downstream `ApplyStep`. + +```python +from ace import SkillManager +from ace.core.recursive_agent import AgenticConfig + +sm = SkillManager("gpt-4o-mini", config=AgenticConfig(max_requests=20)) +output = sm.update_skills( + reflections=(reflection_output,), + skillbook=skillbook, # real Skillbook — mutated in place + question_context="Math problem solving", + progress="5/10 correct", + source=source, + injected_skill_ids=ctx.injected_skill_ids, +) +# skillbook has already been updated; `output` is the post-hoc audit log +``` + +**Tool surface** (`implementations/sm_tools.py`): + +| Tool | Kind | Purpose | +|---|---|---| +| `add_skill(section, issue, keywords, insight?)` | mutate | ADD a new skill | +| `update_skill(skill_id, issue, keywords?, insight?)` | mutate | UPDATE an existing skill | +| `remove_skill(skill_id, reason)` | mutate | REMOVE a skill (duplicate, vague, or `harmful_count ≥ 3`) | +| `tag_skill(skill_id, delta)` | mutate | Bump `helpful_count` / `harmful_count` / `neutral_count` (+1 / -1 / 0) | +| `search_skills(query, top_k, section?, keywords?)` | read | Hybrid retrieval lookup (check before ADD) | +| `read_skill(skill_id)` | read | Fetch full skill payload including counters | +| `execute_code(code)` | read | Inherited sandbox tool for verification | + +Each mutation tool appends an `UpdateOperation` to `SMDeps.operations`; `update_skills()` +splices that list into the returned `SkillManagerOutput`. + +### Shared helpers (`implementations/helpers.py`) + +| Function | Purpose | +|---|---| +| `format_optional(value)` | Returns `"(none)"` for falsy values | +| `make_skillbook_excerpt(skillbook, skill_ids)` | Builds issue / insight excerpts for listed skills | + +### Prompt templates (`implementations/prompts.py`) + +| Constant | Role | +|---|---| +| `AGENT_PROMPT` | Agent prompt with strategic problem-solving protocol | +| `REFLECTOR_PROMPT` | Reflector prompt with pure-analysis protocol (no tagging) | +| `SKILL_MANAGER_SYSTEM` | SkillManager system prompt (tool rules + rejection criteria) | +| `SKILL_MANAGER_PROMPT` | SkillManager user prompt (reflections, stats, workflow) | +| `SKILLBOOK_USAGE_INSTRUCTIONS` | Shared text for skillbook usage guidance | + +Also exports `wrap_skillbook_for_external_agent(skillbook)` — the canonical function for injecting skillbook context into external agentic systems. + +--- + +## Integration Step Examples + +### Execute step pattern + +```python +class BrowserExecuteStep: + requires = frozenset({"sample", "skillbook"}) + provides = frozenset({"trace"}) + + def __init__(self, browser_llm, browser=None, **agent_kwargs) -> None: + self.browser_llm = browser_llm + self.browser = browser + self.agent_kwargs = agent_kwargs + + async def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + task: str = ctx.sample + + # INJECT — prepend skillbook context + enhanced_task = self._inject(task, ctx.skillbook) + + # EXECUTE — run browser-use agent + agent = Agent(task=enhanced_task, llm=self.browser_llm, **self.agent_kwargs) + history = await agent.run() + + result = BrowserResult( + task=task, success=True, output=history.final_result(), + steps_count=history.number_of_steps(), + chronological_steps=..., raw_history=history, + ) + return ctx.replace(trace=result) +``` + +### ToTrace step pattern + +```python +class SomeToTrace: + requires = frozenset({"trace"}) + provides = frozenset({"trace"}) + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + r: SomeResult = ctx.trace + trace = { + "question": r.task, + "reasoning": r.execution_trace, + "answer": r.output, + "skill_ids": r.cited_skill_ids, + "feedback": f"Task {'succeeded' if r.success else 'failed'}", + "ground_truth": None, + } + return ctx.replace(trace=trace) +``` + +### Trace file pipeline composition + +```python +steps = [ + LoadTracesStep(), + OpenClawToTraceStep(), + *learning_tail(reflector, skill_manager, skillbook), +] +``` + +### Custom pipeline with `learning_tail` + +```python +from ace.steps import learning_tail + +skillbook = Skillbook.load_from_file("expert.json") +steps = [ + MyCustomExecuteStep(my_agent), + MyValidationStep(), + *learning_tail(reflector, skill_manager, skillbook, dedup_manager=dedup), +] +runner = ACERunner(Pipeline(steps), skillbook) +``` + +--- + +## Provider Resolution + +```python +# ace/providers/pydantic_ai.py — resolve_model() +# Routes LiteLLM model strings to PydanticAI: + +# 1. PydanticAI-native prefix → pass through +# "openai:gpt-4o" → "openai:gpt-4o" + +# 2. LiteLLM prefix matching native provider → rewrite +# "bedrock/model" → "bedrock:model" + +# 3. Fallback → litellm: prefix +# "ollama/llama3" → "litellm:ollama/llama3" +``` + +Mapped prefixes: `anthropic`, `azure`, `azure_ai`, `bedrock`, `cohere`, `deepseek`, `groq`, `mistral`, `openrouter`, `vertex_ai`. + +Install native provider extras for faster calls: + +```bash +uv add "pydantic-ai-slim[anthropic]" # uses ANTHROPIC_API_KEY +uv add "pydantic-ai-slim[openai]" # uses OPENAI_API_KEY +uv add "pydantic-ai-slim[bedrock]" # uses AWS credentials +uv add "pydantic-ai-slim[anthropic,openai,bedrock]" # multiple +``` + +--- + +## Config types + +```python +@dataclass +class ModelConfig: + """Which model to use for a role. No secrets.""" + model: str + temperature: float = 0.0 + max_tokens: int = 2048 + extra_params: dict[str, Any] | None = None + +@dataclass +class ACEModelConfig: + """Model selection per ACE role.""" + default: ModelConfig + agent: ModelConfig | None = None + reflector: ModelConfig | None = None + skill_manager: ModelConfig | None = None + + def for_role(self, role: str) -> ModelConfig: ... +``` + +### `ace.toml` example + +```toml +[default] +model = "gpt-4o-mini" + +[agent] +model = "claude-sonnet-4-20250514" +max_tokens = 4096 + +[reflector] +model = "gpt-4o-mini" +``` + +### Registry (`ace/providers/registry.py`) + +- `validate_connection(model, api_key?)` — 3-token LLM call to verify auth +- `get_required_key(model)` — returns `(provider, env_var)` +- `search_models(query?, provider?)` — searches LiteLLM's model cost database +- `suggest_models(typo)` — fuzzy match for typos +- `available_providers()` — lists providers with key status + +--- + +## Usage Examples + +### TraceAnalyser — learn from browser-use history + +```python +from ace import TraceAnalyser, Reflector, SkillManager + +traces = [ + { + "task": "Find the cheapest flight to Tokyo", + "output": "$450 on ANA, departing March 15", + "feedback": "Correct price found in 8 steps", + "reasoning": "Step 1: Navigate to Google Flights...", + }, + { + "task": "Book a hotel in Shibuya", + "output": "Failed: could not find checkout button", + "feedback": "Task failed after 15 steps — checkout button was behind a cookie modal", + "reasoning": "Step 1: Navigate to Booking.com...", + }, +] + +analyser = TraceAnalyser.from_roles(reflector=Reflector("gpt-4o-mini"), skill_manager=SkillManager("gpt-4o-mini")) +results = analyser.run(traces, epochs=2) +analyser.save("travel_agent.json") +``` + +### ACE — live Q&A training + +```python +from ace import ACE, Sample, SimpleEnvironment, Agent, Reflector, SkillManager + +samples = [ + Sample(question="Capital of France?", ground_truth="Paris"), + Sample(question="Largest ocean?", ground_truth="Pacific"), +] + +ace = ACE.from_roles( + agent=Agent("gpt-4o-mini"), + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), + environment=SimpleEnvironment(), +) +results = ace.run(samples, epochs=3) +ace.save("geography.json") +``` + +### ACE — without environment + +```python +ace = ACE.from_roles( + agent=Agent("gpt-4o-mini"), + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), +) +results = ace.run(samples, epochs=3) +``` + +### ACE — with checkpoints and deduplication + +```python +from ace import ACE, Agent, Reflector, SkillManager, SimpleEnvironment +from ace.deduplication import DeduplicationManager +from ace.protocols.deduplication import DeduplicationConfig + +ace = ACE.from_roles( + agent=Agent("gpt-4o-mini"), + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), + environment=SimpleEnvironment(), + dedup_manager=DeduplicationManager(DeduplicationConfig(similarity_threshold=0.85)), + checkpoint_dir="./checkpoints", + checkpoint_interval=10, +) +# Pipeline: Agent → Evaluate → Reflect → Update → Apply → Deduplicate → Checkpoint +results = ace.run(samples, epochs=3) +``` + +### Integration — browser-use runner + +```python +from ace import BrowserUse, Reflector, SkillManager +from langchain_openai import ChatOpenAI + +browser_llm = ChatOpenAI(model="gpt-4o") + +# Explicit construction +runner = BrowserUse.from_roles( + browser_llm=browser_llm, + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), +) + +# Or convenience construction +runner = BrowserUse.from_model(browser_llm, ace_model="gpt-4o-mini") + +results = runner.run(["Find top HN post", "Check weather in Tokyo"]) +runner.save("browser_expert.json") +``` + +### Integration — LangChain runner + +```python +from ace import LangChain +from langchain_openai import ChatOpenAI +from langchain_core.prompts import ChatPromptTemplate + +chain = ChatPromptTemplate.from_template("Answer: {input}") | ChatOpenAI(model="gpt-4o") + +runner = LangChain.from_model(chain, ace_model="gpt-4o-mini") +results = runner.run([{"input": "What is ACE?"}, {"input": "Explain skillbooks"}]) +runner.save("chain_expert.json") +``` + +### Integration — Claude Code runner + +```python +from ace import ClaudeCode + +runner = ClaudeCode.from_model(working_dir="./my_project", ace_model="gpt-4o-mini") +results = runner.run(["Add unit tests for utils.py", "Refactor the auth module"]) +runner.save("code_expert.json") +``` + +### ACELiteLLM — conversational agent with learning + +```python +from ace import ACELiteLLM, SimpleEnvironment, Sample + +ace = ACELiteLLM.from_model("gpt-4o-mini") + +# Direct Q&A (no pipeline) +answer = ace.ask("What is the capital of France?") + +# Batch learning +samples = [ + Sample(question="Capital of France?", ground_truth="Paris"), + Sample(question="Largest ocean?", ground_truth="Pacific"), +] +ace.learn(samples, environment=SimpleEnvironment(), epochs=3) + +# Manual feedback learning from last ask() +ace.ask("What is 2+2?") +ace.learn_from_feedback("The answer should be 4", ground_truth="4") + +ace.save("learned.json") + +# With Recursive Reflector +from ace import RRStep, RRConfig +rr = RRStep("gpt-4o-mini", config=RRConfig(max_requests=20)) +ace = ACELiteLLM("gpt-4o-mini", reflector=rr) +``` + +### Fire-and-forget — results while learning continues + +```python +ace = ACE.from_roles( + agent=Agent("gpt-4o-mini"), + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), +) + +# wait=False: returns after foreground steps (Agent + Evaluate) +results = ace.run(samples, epochs=1, wait=False) + +# Use agent outputs immediately +for r in results: + print(r.output.agent_output.final_answer) + +# Check learning progress +print(ace.learning_stats) +# {"active": 3, "completed": 12} + +# Block when you need the skillbook finalised +ace.wait_for_background(timeout=60.0) +ace.save("learned.json") +``` + +### Mixed workflow — batch then live + +```python +from ace import TraceAnalyser, ACE, Skillbook +from ace.implementations import Agent, Reflector, SkillManager + +reflector = Reflector("gpt-4o-mini") +skill_manager = SkillManager("gpt-4o-mini") + +# Phase 1: build skillbook from historical traces +skillbook = Skillbook() +analyser = TraceAnalyser.from_roles( + reflector=reflector, skill_manager=skill_manager, skillbook=skillbook, +) +analyser.run(historical_traces, epochs=3) + +# Phase 2: deploy with live learning (reuse the evolved skillbook) +ace = ACE.from_roles( + agent=Agent("gpt-4o-mini"), + reflector=reflector, skill_manager=skill_manager, skillbook=skillbook, +) +ace.run(live_samples, epochs=1) +ace.save("production.json") +``` + +### Offline learning from integration traces + +```python +# Record browser executions +histories = [await agent.run(task) for task in tasks] + +# Feed raw histories directly — Reflector analyses them as-is +analyser = TraceAnalyser.from_roles( + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), +) +analyser.run(histories, epochs=2) +analyser.save("browser_expert.json") +``` diff --git a/docs/design/CLI_DESIGN.md b/docs/design/CLI_DESIGN.md new file mode 100644 index 0000000000000000000000000000000000000000..087115f2b66242be72abfa223f956a7d458e2709 --- /dev/null +++ b/docs/design/CLI_DESIGN.md @@ -0,0 +1,484 @@ +# CLI & Provider Configuration Design + +Design document for the `ace` CLI, model configuration system, and provider registry. + +--- + +## Implementation Status + +| Component | Status | Location | +|---|---|---| +| `ModelConfig` / `ACEModelConfig` dataclasses | Done | `ace/providers/config.py` | +| TOML persistence (`ace.toml`) | Done | `ace/providers/config.py` | +| `.env` persistence (secrets) | Done | `ace/providers/config.py` | +| Provider registry (LiteLLM delegation) | Done | `ace/providers/registry.py` | +| Model search / discovery | Done | `ace/providers/registry.py` | +| Connection validation | Done | `ace/providers/registry.py` | +| `ace setup` interactive wizard | Done | `ace/cli/setup.py` | +| `ace models` search command | Done | `ace/cli/setup.py` | +| `ace validate` connection test | Done | `ace/cli/setup.py` | +| Lazy imports (fast CLI startup) | Done | `ace/__init__.py`, `ace/providers/__init__.py`, `ace/providers/registry.py` | + +--- + +## Goals + +1. **Zero-friction onboarding** — `ace setup` walks the user from nothing to a working config in under a minute. +2. **Secrets / config separation** — `ace.toml` (committable, no secrets) + `.env` (gitignored API keys). Teams share model choices without leaking credentials. +3. **Per-role model selection** — different models for Agent, Reflector, and Skill Manager to optimise cost vs quality. +4. **Provider agnosticism** — any model string LiteLLM supports works. No provider-specific code in the config layer. +5. **Fast CLI startup** — heavy dependencies (LiteLLM, instructor, openai) are lazily imported. The `ace` command starts in ~50ms, not ~2s. + +--- + +## Architecture Overview + +``` +pyproject.toml + [project.scripts] + ace = "ace.cli.setup:main" # entry point + +ace/cli/ + __init__.py + setup.py # CLI commands + interactive wizard + +ace/providers/ + __init__.py # lazy re-exports + config.py # ModelConfig, ACEModelConfig, TOML + .env I/O + registry.py # provider detection, model search, connection validation + pydantic_ai.py # resolve_model, settings_from_config (PydanticAI model resolution) +``` + +The CLI layer (`ace/cli/`) depends on: +- `ace/providers/config.py` — always (lightweight, no heavy deps) +- `ace/providers/registry.py` — only when validating or searching (imports LiteLLM lazily) + +The config layer has **zero heavy dependencies** — it uses only `tomllib` (stdlib), `pathlib`, and `dataclasses`. + +--- + +## Configuration Model + +### Two-file split + +| File | Contains | Git | Written by | +|------|----------|-----|------------| +| `ace.toml` | Model names, temperature, max_tokens, extra_params | Commit | `save_config()` | +| `.env` | `OPENAI_API_KEY=sk-...`, `ANTHROPIC_API_KEY=sk-ant-...` | Gitignore | `save_env_var()` | + +### `ace.toml` format + +```toml +[default] +model = "gpt-4o-mini" + +[agent] +model = "claude-sonnet-4-20250514" +max_tokens = 4096 + +[reflector] +model = "gpt-4o-mini" +temperature = 0.2 +``` + +Roles without an explicit section inherit from `[default]`. Only non-default values are written (e.g. `temperature` is omitted when it equals `0.0`). + +### Config discovery + +`find_config(start)` walks up from `start` to the filesystem root looking for `ace.toml`. This supports monorepos where the config lives at the project root but commands run from subdirectories. + +### Loading in code + +```python +from ace import ACELiteLLM + +# Option 1: Load from ace.toml + .env (created by `ace setup`) +ace = ACELiteLLM.from_setup() + +# Option 2: Explicit model, keys from environment +ace = ACELiteLLM.from_model("gpt-4o-mini") + +# Option 3: Full config object +from ace import ACEModelConfig, ModelConfig +ace = ACELiteLLM.from_config(ACEModelConfig( + default=ModelConfig(model="gpt-4o-mini"), + agent=ModelConfig(model="claude-sonnet-4-20250514"), +)) +``` + +--- + +## Data Types + +### ModelConfig + +```python +@dataclass +class ModelConfig: + model: str # LiteLLM model string + temperature: float = 0.0 + max_tokens: int = 2048 + extra_params: dict[str, Any] | None = None +``` + +Serialises to/from a TOML section. `to_dict()` omits default values to keep the file clean. + +### ACEModelConfig + +```python +@dataclass +class ACEModelConfig: + default: ModelConfig # required — used as fallback + agent: ModelConfig | None = None # overrides default for Agent role + reflector: ModelConfig | None = None # overrides default for Reflector role + skill_manager: ModelConfig | None = None # overrides default for Skill Manager role +``` + +`for_role(role)` returns the role-specific config or falls back to `default`. + +### ValidationResult + +```python +@dataclass +class ValidationResult: + success: bool + model: str = "" + provider: str = "" + latency_ms: int = 0 + error: str = "" +``` + +Returned by `validate_connection()`. On success, includes the provider name and round-trip latency. On failure, includes a human-readable error string. + +### ModelInfo + +```python +@dataclass +class ModelInfo: + model: str + provider: str + max_input_tokens: int | None = None + max_output_tokens: int | None = None + input_cost_per_m: float | None = None # cost per million tokens + output_cost_per_m: float | None = None + key_found: bool = False # are the required env vars set? +``` + +Returned by `search_models()`. Pricing is per million tokens (converted from LiteLLM's per-token values). + +--- + +## Provider Registry + +All provider logic is delegated to LiteLLM. The registry module (`ace/providers/registry.py`) wraps LiteLLM with a stable API: + +| Function | What it does | Makes API calls? | +|----------|-------------|-----------------| +| `get_provider(model)` | Detect provider from model string | No | +| `get_missing_keys(model)` | List required env vars that are unset | No | +| `keys_are_set(model)` | Check if all required env vars exist | No | +| `validate_connection(model, api_key?)` | Send a 3-token test call | Yes (tiny) | +| `search_models(query, provider?, limit?)` | Search LiteLLM's static model registry | No | +| `suggest_models(typo, limit?)` | Fuzzy-match model names for typo correction | No | + +### LiteLLM lazy import + +LiteLLM takes ~1.5s to import. The registry defers import until first use: + +```python +def _litellm(): + global _litellm_mod + try: + return _litellm_mod + except NameError: + pass + import litellm as _mod + _litellm_mod = _mod + return _mod +``` + +All registry functions call `_litellm()` instead of using a top-level import. + +### Provider key mapping + +`PROVIDER_KEY_ENV` maps provider names to the environment variables they require: + +| Provider | Required env vars | +|----------|------------------| +| `openai` | `OPENAI_API_KEY` | +| `anthropic` | `ANTHROPIC_API_KEY` | +| `azure` | `AZURE_API_KEY` | +| `gemini` | `GEMINI_API_KEY` | +| `deepseek` | `DEEPSEEK_API_KEY` | +| `groq` | `GROQ_API_KEY` | +| `bedrock` | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` + `AWS_REGION_NAME` | +| `bedrock_converse` | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` + `AWS_REGION_NAME` | +| `vertex_ai` | `GOOGLE_APPLICATION_CREDENTIALS` | +| `cohere` | `COHERE_API_KEY` | +| `mistral` | `MISTRAL_API_KEY` | +| `openrouter` | `OPENROUTER_API_KEY` | +| `together_ai` | `TOGETHERAI_API_KEY` | +| `fireworks_ai` | `FIREWORKS_AI_API_KEY` | +| `replicate` | `REPLICATE_API_KEY` | +| `huggingface` | `HUGGINGFACE_API_KEY` | +| `perplexity` | `PERPLEXITYAI_API_KEY` | +| `anyscale` | `ANYSCALE_API_KEY` | + +For multi-key providers (bedrock), **all** listed vars must be set for `_quick_key_check` to report `True`. + +This mapping is preferred over LiteLLM's `validate_environment()` because: +- LiteLLM's response can be inaccurate for certain providers (e.g. bedrock_converse) +- Our mapping is used for both the `Key` column in `ace models` and the key prompting in `ace setup` + +--- + +## CLI Commands + +### Entry point + +Defined in `pyproject.toml`: + +```toml +[project.scripts] +ace = "ace.cli.setup:main" +``` + +`main()` uses `argparse` with subcommands: + +``` +ace setup [--dir DIR] Interactive configuration wizard +ace models [QUERY] [--provider P] [--limit N] Search model catalog +ace validate MODEL Test a model connection +ace config Show current configuration +``` + +### `ace setup` + +Interactive wizard flow: + +``` +1. Load existing .env (if present) +2. Check for existing ace.toml + - If found: show current config, ask "Reconfigure?" + - If declined: return existing config +3. Step 1: Choose your model + - Prompt for model name + - Detect provider via get_provider() + - Validate connection + - If auth fails: prompt for missing keys, retry + - If model not found: suggest alternatives, re-prompt + - If success: continue +4. Step 2: Role assignment + - Ask "Use this model for all roles?" (default: yes) + - If no: prompt for each role (Agent, Reflector, Skill Manager) + - Enter = keep default (skip validation) + - Different model = validate it +5. Save ace.toml and .env +6. Print configuration summary +``` + +Key behaviours: +- **Validation-first**: the wizard tries the connection immediately. If credentials exist in the environment (env vars, `.env`, AWS profiles), no prompting needed. +- **Error recovery**: on failed validation, env vars set during prompting are rolled back. +- **Secret handling**: API keys are prompted via `getpass` (hidden input). Non-secret values like `AWS_REGION_NAME` and `GOOGLE_APPLICATION_CREDENTIALS` use regular `input()` so users can see what they type. +- **Typo correction**: when a model is not found, `suggest_models()` offers alternatives. + +### `ace models` + +Searches LiteLLM's static `model_cost` registry (no API calls): + +``` +$ ace models claude haiku + +Model Provider Input $/M Output $/M Key +------------------------------------------------------------------------------------------ +claude-haiku-4-5-20251001 anthropic $1.00 $5.00 ✓ +us.anthropic.claude-haiku-4-5-20251001-v1:0 bedrock_converse $1.10 $5.50 ✗ +``` + +- Multiple query terms are AND-matched (all must appear in the model name) +- `--provider` filters by LiteLLM provider name +- `--limit` caps results (default 20), shows total count +- `Key` column uses `_quick_key_check()` — checks env vars only, no API calls + +### `ace validate` + +Sends a minimal LLM call (3 tokens: "Say 'ok'") to verify: +- API key authentication +- Model availability at the provider +- Network connectivity + +``` +$ ace validate gpt-4o-mini +✓ Connected! (gpt-4o-mini via openai, 203ms) +``` + +On failure, suggests similar model names if the model wasn't found. + +All subcommands (`models`, `validate`, `config`) use `_load_project_dotenv()` which finds `.env` relative to `ace.toml` (via `find_config()`), not just CWD. + +### `ace config` + +Displays the current configuration from `ace.toml`: + +``` +$ ace config +Configuration (/path/to/ace.toml) + + Role Model + ---------------- --------------------------------------------- + default gpt-4o-mini + agent claude-sonnet-4-20250514 + reflector (default) + skill_manager (default) +``` + +Uses `find_config()` to locate `ace.toml` from the current directory upward. + +### `ace models` (no query) + +When called without arguments, shows usage examples instead of dumping arbitrary results: + +``` +$ ace models +Usage: ace models <query> + +Examples: + ace models claude All Claude models + ace models gpt 4o GPT-4o variants + ace models haiku us US-region Haiku models + ace models --provider openai All OpenAI models +``` + +--- + +## Kayba CLI + +The `kayba` entry point (`ace.cli:main`) provides the hosted API client. It wraps trace management, pipeline execution, insight triage, prompt generation, and integration management. See [Hosted API docs](../integrations/hosted-api.md) for the full reference. + +```toml +[project.scripts] +ace = "ace.cli.setup:main" +kayba = "ace.cli:main" +ace-mcp = "ace.integrations.mcp.server:main" +``` + +Key command groups: `traces`, `run`, `insights`, `prompts`, `integrations`, `status`, `materialize`, `batch`, `setup`. + +--- + +## Lazy Import Strategy + +### Problem + +`import ace` eagerly imported all submodules, including `litellm` (~1.5s). This made the CLI unusable (~2s startup for a simple `ace --help`). + +### Solution + +Three-layer lazy import: + +1. **`ace/__init__.py`** — `__getattr__`-based lazy loading. `TYPE_CHECKING` block for IDE support, `_LAZY_IMPORTS` dict for runtime. + +2. **`ace/providers/__init__.py`** — same pattern. Config imports are eager (lightweight), everything else is lazy. + +3. **`ace/providers/registry.py`** — `_litellm()` helper defers `import litellm` until first function call. + +Result: `ace --help` runs in ~50ms. LiteLLM is only imported when the user actually searches, validates, or sets up. + +### Pattern + +```python +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .heavy_module import HeavyClass # IDE autocomplete + +_LAZY_IMPORTS = { + "HeavyClass": ("package.heavy_module", "HeavyClass"), +} + +def __getattr__(name: str) -> object: + if name in _LAZY_IMPORTS: + module_path, attr = _LAZY_IMPORTS[name] + import importlib + module = importlib.import_module(module_path) + value = getattr(module, attr) + globals()[name] = value # cache for subsequent access + return value + raise AttributeError(...) +``` + +--- + +## File Map + +``` +ace/ + __init__.py # lazy re-exports for all ace symbols + cli/ + setup.py # main(), run_setup(), _cmd_models(), _cmd_validate(), _cmd_config() + providers/ + __init__.py # lazy re-exports (config eager, rest lazy) + config.py # ModelConfig, ACEModelConfig, TOML/env I/O + registry.py # provider detection, model search, validation + pydantic_ai.py # resolve_model, settings_from_config (PydanticAI model resolution) +``` + +Generated files: + +``` +project-root/ + ace.toml # model config (commit this) + .env # API keys (gitignore this) +``` + +--- + +## Known Issues + +### Resolved + +| # | Issue | Fix | +|---|-------|-----| +| 1 | `_prompt_secret` used `getpass` for non-secrets like `AWS_REGION_NAME` | Non-secret vars (`AWS_REGION_NAME`, `GOOGLE_APPLICATION_CREDENTIALS`) now use visible `_prompt()` | +| 2 | Error classification used fragile substring matching | Simplified: only "not found" is non-recoverable; everything else offers key prompting | +| 3 | `save_env_var` didn't quote values | Values now written as `KEY="value"` | +| 4 | `_PROVIDER_KEY_ENV` was private but imported externally | Renamed to `PROVIDER_KEY_ENV` (public) | +| 5 | `v` / `x` status symbols | Replaced with `✓` / `✗` | +| 7 | `ace models` with no query dumped arbitrary results | Now shows usage examples instead | +| 8 | No way to inspect current config | Added `ace config` command | +| 10 | `ace validate` / `ace models` only loaded `.env` from CWD | Added `_load_project_dotenv()` — finds `.env` relative to `ace.toml` via `find_config()` | +| 11 | Unused imports `asdict`, `field` in `config.py` | Removed | +| 12 | Dead function `_litellm_available()` in `registry.py` | Removed | +| 13 | Unused `PROVIDER_MODEL_EXAMPLES` import in `setup.py` | Removed | +| 14 | Duplicate `search_models` import in `setup.py` | Consolidated to single top-level import | + +### Open + +| # | Issue | Impact | +|---|-------|--------| +| 6 | No `--non-interactive` mode for CI/Docker | Blocks CI automation — requires a future `ace setup --model MODEL --skip-validation` flag | +| 9 | Per-role model selection skips validation when keeping default | Low — the default was already validated in Step 1 | +| 15 | No multi-provider key setup in one pass | When reconfiguring (`ace setup` on an existing config), users who want different providers per role (e.g. OpenAI default + Anthropic agent + Bedrock reflector) must go through each role sequentially — keys are only prompted when a model fails validation. A future improvement should let users configure all providers and their keys upfront in a single credentials step, before role assignment begins. | + +--- + +## Design Decisions + +### Why hand-rolled TOML serialisation? + +Python's `tomllib` (stdlib since 3.11) only reads TOML. Writing requires `tomli-w` (third-party) or a manual serialiser. To avoid adding a dependency for a simple four-section config file, we hand-roll `_to_toml()`. The format is simple enough that this is reliable — the only complex case is `extra_params` (inline table). + +### Why our own key mapping instead of LiteLLM's? + +LiteLLM's `validate_environment()` sometimes returns incorrect keys (e.g. suggesting `AWS_BEARER_TOKEN_BEDROCK` for bedrock_converse when the standard auth is `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` + `AWS_REGION_NAME`). Our `PROVIDER_KEY_ENV` mapping is simpler, auditable, and covers the common providers. For unknown providers, we fall back to LiteLLM's response or guess `{PROVIDER}_API_KEY`. + +### Why validation-first in setup? + +Many users already have credentials in their environment (exported vars, AWS profiles, `.env` from another project). Prompting for keys before trying would waste their time. By attempting the connection first, the happy path is: type model name → instant success → done. + +### Why `ace.toml` instead of `pyproject.toml [tool.ace]`? + +- Keeps ACE config decoupled from the Python project (ACE might be used in non-Python contexts) +- `find_config()` can walk up the directory tree independently +- Easier to reason about — one file, one purpose diff --git a/docs/design/PIPELINE_DESIGN.md b/docs/design/PIPELINE_DESIGN.md new file mode 100644 index 0000000000000000000000000000000000000000..d1607d36fb54b7d48f4bfd7e13307dd4c8b3c29b --- /dev/null +++ b/docs/design/PIPELINE_DESIGN.md @@ -0,0 +1,756 @@ +# Pipeline Architecture Design + +Design decisions for the generalized pipeline system. Trying to keep is as generic as possible. +--- + +## Core Primitives + +Everything in the framework composes from three primitives: + +``` +Sequential: A → B → C +Branch: A → (B ∥ C) → D (fork + implicit join) +Pipeline: a step that is itself a pipeline (nesting / reuse) +``` + +--- + +## Step + +A `Step` is the smallest unit of work. It receives a `StepContext`, does one focused thing, and returns the context. + +```python +class MyStep: + requires = {"agent_output"} # fields it reads + provides = {"reflections"} # fields it writes + + def __call__(self, ctx: StepContext) -> StepContext: + ... + return ctx +``` + +Rules: +- Always synchronous within its own execution +- Must declare `requires` and `provides` — the pipeline validates ordering at construction time +- Steps declare their own parallelism constraints (see below) + +### Step protocol + +For static type checking, the framework exposes a generic `typing.Protocol`: + +```python +from typing import Protocol, TypeVar, runtime_checkable + +Ctx = TypeVar("Ctx", bound=StepContext) + +@runtime_checkable +class StepProtocol(Protocol[Ctx]): + requires: frozenset[str] + provides: frozenset[str] + + def __call__(self, ctx: Ctx) -> Ctx: ... +``` + +`StepProtocol` is generic over the context type. The base `StepProtocol` (or `StepProtocol[StepContext]`) is satisfied by `Pipeline` and `Branch`, so they can be nested wherever a step is expected. Domain-specific steps use the parameterized form — e.g. `StepProtocol[ACEStepContext]` — so that mypy validates the `__call__` signature against the concrete context subclass without needing `# type: ignore` comments. + +`@runtime_checkable` lets the pipeline validator use `isinstance(step, StepProtocol)` at construction time to give a clear error if a step is missing required attributes, rather than failing at call time. The type parameter is erased at runtime, so `isinstance` checks work the same as with a non-generic protocol. + +### StepContext — immutability contract + +`StepContext` is a frozen dataclass. Steps never mutate the incoming context — they return a new one via `.replace()`. + +The pipeline engine defines a minimal base with only two fields: + +```python +from types import MappingProxyType + +@dataclass(frozen=True) +class StepContext: + sample: Any + metadata: MappingProxyType = field(default_factory=lambda: MappingProxyType({})) + + def __post_init__(self): + # Ensures mutation is a hard runtime error even if caller passes a plain dict + if not isinstance(self.metadata, MappingProxyType): + object.__setattr__(self, "metadata", MappingProxyType(self.metadata)) + + def replace(self, **changes) -> "StepContext": + return dataclasses.replace(self, **changes) +``` + +The engine never reads anything beyond `sample` and `metadata`. All domain-specific fields are added by subclassing. + +#### Subclassing for domain fields + +Consuming applications subclass `StepContext` to add named fields for concepts shared across their pipelines: + +```python +@dataclass(frozen=True) +class ACEContext(StepContext): + # Shared across all ACE pipelines + skillbook: Skillbook | None = None + environment: TaskEnvironment | None = None + + # Produced by steps (None until the providing step runs) + agent_output: AgentOutput | None = None + environment_result: EnvironmentResult | None = None + reflections: tuple[ReflectorOutput, ...] = () + skill_manager_output: UpdateBatch | None = None + + # Runner bookkeeping + epoch: int = 1 + total_epochs: int = 1 + step_index: int = 0 + total_steps: int = 0 +``` + +The `requires`/`provides` validation works on attribute names (strings) — it checks that the field exists on the context object at runtime, so it is subclass-agnostic. A step that declares `requires = {"skillbook"}` works whether the context is `ACEContext` or any other subclass that has a `skillbook` attribute. + +Data that is specific to a single integration or step goes in `metadata` to prevent field accumulation on the subclass. For example, `metadata["browser_history"]` for browser-use or `metadata["transcript_path"]` for Claude Code. + +#### Immutable update patterns + +Updating metadata follows the same immutable pattern as any other field: + +```python +return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "key": value})) +``` + +Steps follow this pattern: + +```python +def __call__(self, ctx: StepContext) -> StepContext: + result = do_work(ctx.sample) + return ctx.replace(result=result) +``` + +`frozen=True` makes mutation a hard error at runtime rather than a subtle bug. It also makes `Branch` safe by default — since `StepContext` is immutable, all branches can receive the same object without risk; no deep copy is needed. + +--- + +## Pipeline + +A `Pipeline` is an ordered list of steps that runs sequentially for a single input. It also satisfies the `Step` protocol, so it can be embedded inside another pipeline. + +```python +pipe = Pipeline([ + AgentStep(), + EvaluateStep(), + ReflectStep(), + UpdateStep(), +]) +``` + +**Fluent builder API (preferred):** + +```python +pipe = ( + Pipeline() + .then(AgentStep()) + .then(EvaluateStep()) + .then(ReflectStep()) + .then(UpdateStep()) +) +``` + +**Fan-out across contexts:** + +```python +pipe.run(contexts, workers=4) # same pipeline, N contexts in parallel +``` + +### Inner pipeline as a fan-out step + +A `Pipeline`-as-`Step` receives one context and must return one context — but nothing prevents it from internally expanding to multiple sub-inputs. This is the **map-reduce step** pattern: + +```python +class MultiSearchStep: + """Generates N queries from one context, runs them in parallel, merges.""" + def __call__(self, ctx: StepContext) -> StepContext: + queries = generate_queries(ctx.sample) # 1 → N + sub_ctxs = [StepContext(sample=q) for q in queries] + sub_pipe = Pipeline().then(FetchStep()) + results = sub_pipe.run(sub_ctxs, workers=len(queries)) # parallel + return ctx.replace(agent_output=merge(results)) # N → 1 +``` + +`sub_pipe.run()` is a top-level runner call, so `async_boundary` and `workers` on its inner steps fire normally. From the outer pipeline's perspective, `MultiSearchStep` is a black box that takes one context and returns one context — the fan-out is an internal implementation detail. + +### requires/provides for nested pipelines + +When a `Pipeline` is used as a `Step` inside another pipeline, its `requires` and `provides` are computed automatically at construction time from its inner steps — no manual annotation needed. + +```python +class Pipeline: + def __init__(self, steps): + self.steps = steps + self.requires, self.provides = self._infer_contracts(steps) + + @staticmethod + def _infer_contracts(steps): + provided_so_far = set() + external_requires = set() + for step in steps: + external_requires |= step.requires - provided_so_far + provided_so_far |= step.provides + return frozenset(external_requires), frozenset(provided_so_far) +``` + +- `requires` = everything the pipeline needs from the outside (what its first steps need that no earlier inner step provides) +- `provides` = union of everything any inner step writes + +The outer pipeline validates against these aggregated values at construction time, so nesting never breaks the contract. + +**Deliberate constraint:** `_infer_contracts` assumes all `Branch` children always run. It has no concept of conditional branches where only some children execute. If one branch provided a field that a later step required but other branches did not, static validation would pass while the pipeline could fail at runtime. Conditional branching — where a branch may or may not run depending on context — is out of scope; all branches in a `Branch` are always executed. + +--- + +## Branch + +A `Branch` is a step that runs multiple pipelines in parallel and joins before returning. It is just a `Step` — no special pipeline mode needed. + +```python +pipe = ( + Pipeline() + .then(AgentStep()) + .then(EvaluateStep()) + .branch( + Pipeline().then(ReflectStep()), + Pipeline().then(LogStep()), + ) + .then(UpdateStep()) # only runs after both branches complete +) +``` + +`wait` is implicit — any step after a `Branch` waits for all branches to finish. + +### Context merging + +Each branch receives the same context reference. Since `StepContext` is frozen, no copy is needed — branches cannot mutate what they receive. When all branches complete, their output contexts are merged back into one before the next step runs. + +The merge function receives the list of output contexts and returns a single context: + +```python +Branch( + Pipeline().then(ReflectStep()), + Pipeline().then(LogStep()), + merge=lambda ctxs: dataclasses.replace( + ctxs[0], + metadata={**ctxs[0].metadata, **ctxs[1].metadata} + ) +) +``` + +**Built-in merge strategies:** + +| Strategy | Behaviour | +|---|---| +| `raise_on_conflict` | raises if two branches write the same field — safe default, no silent data loss | +| `last_write_wins` | last branch's value wins on conflict — simple but lossy | +| `namespaced` | branches write to `ctx.metadata["branch_0"]` etc., no conflict possible | +| custom `merge=fn` | `fn(ctxs: list[StepContext]) -> StepContext` — full control | + +The actual default when no `merge=` argument is passed is `raise_on_conflict`. The constructor signature makes this explicit: + +```python +def __init__(self, *pipelines, merge=MergeStrategy.RAISE_ON_CONFLICT): + ... +``` + +In practice, branches that write disjoint fields (e.g. Reflect writes `reflection`, Log writes `metadata["log"]`) never conflict and the merge is a no-op — `raise_on_conflict` passes through without raising. + +--- + +## Async Behavior + +"Async" means three different things in this framework, operating at different levels. It is important to keep them separate — they solve different problems. + +| Type | Level | Problem it solves | +|---|---|---| +| Async step | single step | don't block the thread during I/O | +| `async_boundary` | across samples | start the next sample before the current one finishes | +| Branch parallelism | within one sample | run independent work simultaneously on the same data | + +--- + +### 1. Async steps — non-blocking I/O + +**Problem:** A step makes a network call (LLM API, HTTP, subprocess). It should not block the thread while waiting for a response. + +**Solution:** Define the step as a coroutine. The pipeline detects this automatically and awaits it. Sync steps get wrapped with `asyncio.to_thread()` so they are safe in an async context too. + +```python +# Sync step — no changes needed +class AgentStep: + def __call__(self, ctx: StepContext) -> StepContext: ... + +# Async step — native coroutine, awaited by the pipeline +class BrowserExecuteStep: + async def __call__(self, ctx: StepContext) -> StepContext: ... +``` + +```python +# Pipeline runner — handles both transparently +for step in self.steps: + if asyncio.iscoroutinefunction(step.__call__): + ctx = await step(ctx) + else: + ctx = await asyncio.to_thread(step, ctx) +``` + +Pipeline entry points: `pipe.run(contexts)` for sync callers, `await pipe.run_async(contexts)` for async callers (e.g. inside browser-use). + +This type is about **not blocking**. Nothing runs in parallel — the pipeline is still sequential, it just yields the thread during waits. + +--- + +### 2. async_boundary — pipeline across samples + +**Problem:** Reflect and Update are slow (LLM calls). If we wait for them before starting the next sample, throughput is poor. We want to fire them off and immediately move to sample N+1. + +**Solution:** A step declares `async_boundary = True`. Everything from that step onwards runs in a background executor. The pipeline loop does not wait — it moves straight to the next sample. + +```python +class ReflectStep: + async_boundary = True # hand off to background from here + max_workers = 3 # up to 3 reflections running in parallel + +class UpdateStep: + max_workers = 1 # must serialize — writes to shared skillbook +``` + +``` +sample 1: [Agent] [Evaluate] ──fire──► [Reflect] [Update] (background) +sample 2: [Agent] [Evaluate] ──fire──► [Reflect] [Update] (background) +sample 3: [Agent] [Evaluate] ... + ↑ + async_boundary +``` + +This type is about **throughput**. Multiple samples are in-flight simultaneously, at different stages of the pipeline. The caller only waits for steps before the boundary. + +Note: `max_workers` controls how many background instances of a step run concurrently. Steps that write shared state (like `UpdateStep`) must use `max_workers = 1` to avoid races. + +**Background pool is per step class, shared across pipeline instances.** `ReflectStep.max_workers = 3` means a single pool of 3 threads for all `ReflectStep` instances. This avoids pool proliferation and makes `max_workers` a straightforward capacity knob independent of how many pipelines are running. + +**Pool lifecycle:** The `ThreadPoolExecutor` for each step class is created lazily at first use (not at class definition or pipeline construction) and persists for the process lifetime. Callers that need explicit cleanup can call `StepClass._executor.shutdown(wait=True)`. If two users of the same step class need different concurrency limits (e.g. different LLM backends behind the same step type), they should subclass rather than share the class attribute. + +**Boundary rules:** +- The **first** step with `async_boundary = True` is the handoff point. Only one boundary per pipeline. +- If multiple steps in the same pipeline declare `async_boundary = True`, the pipeline raises `PipelineConfigError` at construction time. A duplicate boundary is almost always a copy-paste mistake, not a deliberate choice. +- `async_boundary` inside a `Branch` child pipeline raises `PipelineConfigError` at construction time. Branch children always block until joined; detaching mid-branch is incoherent and there is no valid interpretation. +- `async_boundary` inside a `Pipeline`-as-`Step` raises a **warning** at construction time (not an error). When a pipeline is used as a step inside another pipeline, there is no "next sample" to move to — the outer pipeline is blocked waiting for the inner one to return a context. The boundary is ignored and the inner pipeline runs fully synchronously. The warning surfaces this declared intent being ignored so callers can investigate. The same pipeline definition works both as a top-level runner (where `async_boundary` fires) and as a nested step (where it warns and is ignored) — no reconfiguration needed. + +--- + +### 3. Branch parallelism — concurrent work on the same sample + +**Problem:** Two independent steps could run at the same time on the same sample (e.g. reflect and log), but a linear pipeline forces them to be sequential. + +**Solution:** `Branch` forks the context, runs each sub-pipeline in parallel, then joins before the next step. In sync mode it uses `ThreadPoolExecutor`; in async mode it uses `asyncio.gather()`. + +```python +pipe = ( + Pipeline() + .then(EvaluateStep()) + .branch( + Pipeline().then(ReflectStep()), # runs in parallel + Pipeline().then(LogStep()), # runs in parallel + ) + .then(UpdateStep()) # waits for both branches +) +``` + +```python +# Branch internals (async mode) +async def __call__(self, ctx: StepContext) -> StepContext: + results = await asyncio.gather( + *[p(ctx) for p in self.pipelines], + return_exceptions=True, # all branches run to completion even if one fails + ) + failures = [r for r in results if isinstance(r, BaseException)] + if failures: + raise BranchError(failures) # caller sees all branch failures, not just the first + return self.merge(results) +``` + +`return_exceptions=True` is required for consistent error handling: without it, the first branch failure cancels all remaining branches and the `SampleResult` would silently drop their work. With it, all branches complete and the runner captures the full failure set. + +This type is about **latency within a single sample**. Nothing moves to the next sample — the pipeline waits for the join before continuing. + +--- + +### Rule of thumb + +| Question | Answer | +|---|---| +| Does the step wait on I/O? | `async def __call__` | +| Do I want to process more samples while previous ones are still learning? | `async_boundary` on the step where the handoff happens | +| Can two steps on the same sample run simultaneously? | `Branch` | +| Do I want N samples going through the pipeline at the same time? | `workers=N` on `run()` | + +Each mechanism is independent. They compose freely — you can have async steps inside branches, behind an `async_boundary`, run with multiple workers. + +--- + +## Concurrency Model + +Parallelism is declared on the **step**, not the pipeline. The pipeline executor reads these at runtime: + +```python +class ReflectStep: + async_boundary = True # hand off to background threads from here + max_workers = 3 # up to 3 running in parallel + +class UpdateStep: + max_workers = 1 # must serialize (writes to shared skillbook) +``` + +**Fan-out (same step, different samples):** +Controlled by `max_workers` on the step. Each step class has a single shared `ThreadPoolExecutor` — `ReflectStep.max_workers = 3` means one pool of 3 threads regardless of how many pipeline instances are running. + +**Pipeline split (pipelining across samples):** +`async_boundary = True` on a step tells the runner to hand off everything from that step onwards to background threads, freeing the caller to start the next sample immediately. + +``` +sample 1: [AgentStep] [EvaluateStep] ──► [ReflectStep] [UpdateStep] +sample 2: [AgentStep] [EvaluateStep] ──► ... (background) + ↑ + async_boundary +``` + +This replaces the hardcoded `steps[:2]` / `steps[2:]` split that existed in the old `AsyncLearningPipeline`. + +### workers vs max_workers — independent pools + +These two knobs control different thread pools and do not interact: + +| Knob | Pool | Controls | +|---|---|---| +| `pipe.run(contexts, workers=N)` | foreground pool | how many contexts run through pre-boundary steps simultaneously | +| `step.max_workers = K` | background pool per step class | how many instances of that step run in the background simultaneously | + +A sample leaves the foreground pool when it crosses the `async_boundary` point and enters the background step's pool. With `workers=4` and `ReflectStep.max_workers=3`, you can have 4 samples in Agent/Evaluate and 3 reflections running concurrently — two separate pools, no multiplication. + +Mental model: `workers` controls throughput *into* the pipeline; `max_workers` controls throughput *through* each slow background step. + +**LLM rate limits:** `workers` and `max_workers` are independent pools, but total concurrent outbound LLM calls = foreground calls + background calls. With `workers=4` and `ReflectStep.max_workers=3`, up to 7 LLM requests may be in-flight simultaneously. Account for this when configuring per-provider rate limits. + +--- + +## Error Handling + +Failure semantics differ depending on which side of the `async_boundary` a step is on. + +**Foreground steps** (before the boundary): the runner catches exceptions per sample and records them in a `SampleResult`. The pipeline then moves to the next sample. + +```python +# Pipeline runner (foreground loop) +for ctx in contexts: + try: + for step in self.foreground_steps: + ctx = step(ctx) + self._submit_to_background(ctx) + results.append(SampleResult(sample=ctx.sample, output=ctx, error=None, failed_at=None)) + except Exception as e: + results.append(SampleResult(sample=ctx.sample, output=None, error=e, failed_at=type(step).__name__)) +``` + +**Background steps** (after the boundary): the caller has already moved on, so exceptions cannot propagate. Background failures are captured and attached to the `SampleResult` — nothing is dropped silently. + +```python +@dataclass +class SampleResult: + sample: Any + output: StepContext | None # None if a step failed + error: Exception | None # set if any step failed + failed_at: str | None # name of the step class that failed + cause: Exception | None = None # for BranchError: the inner step exception +``` + +Every sample produces a result — either successful with `output` set, or failed with `error` and `failed_at` set. After `run()` completes (or after `wait_for_learning()`), callers can inspect results for failures. + +When a `Branch` step fails, `failed_at` is `"Branch"` and `error` is a `BranchError`. `cause` carries the inner exception from the failing branch so callers can see which inner step actually failed, not just the outer wrapper. + +Retry logic is the responsibility of individual steps, not the pipeline. + +**Shutdown:** `wait_for_background(timeout=N)` raises `TimeoutError` if background steps have not drained within `N` seconds. Individual step implementations are responsible for their own per-call timeouts (e.g. LLM API call timeouts). + +**Monitoring:** `background_stats()` returns a `dict` with `active` and `completed` counts for background threads. Thread-safe — can be called from any thread while the pipeline is running. This is the public API for monitoring background progress; callers should not access `_bg_lock` or `_bg_threads` directly. + +**Foreground progress:** `run()` and `run_async()` accept an optional `on_sample_done` callback (`Callable[[SampleResult], None] | None`). It fires once per context after foreground steps complete (or fail), before background steps start. The callback must not block the event loop — lightweight operations like `tqdm.update()` are fine. Defaults to `None` (no-op). This is the foreground-side complement to `background_stats()`. + +--- + +## Pipeline Hooks + +Hooks let external code observe pipeline execution without modifying data flow. They solve a different problem than steps: steps transform data (`StepContext` in, `StepContext` out), hooks observe transitions (step started, step finished). + +The motivating use case is hosted/web deployments that need operational concerns — progress streaming, metrics, logging, billing — wired into the pipeline without modifying the step chain or the pipeline engine for each new concern. + +### Separation of concerns + +The pipeline has three distinct concerns, each with its own mechanism: + +| Concern | Mechanism | Who owns it | +|---|---|---| +| Data flow | Steps (`requires`/`provides`, `__call__`) | Step author | +| Observation | Hooks (`before_step`/`after_step`) | Deployment environment | +| Lifecycle control | `cancel_token` (see Cancellation below) | Caller | + +Steps own data. Hooks observe execution. Cancellation controls lifecycle. These three never overlap — a hook cannot modify context, and cancellation is not a hook. + +### Hook protocol + +```python +@runtime_checkable +class PipelineHook(Protocol): + def before_step(self, step_name: str, ctx: StepContext) -> None: ... + def after_step(self, step_name: str, ctx: StepContext) -> None: ... +``` + +**Design constraints:** + +- **`-> None`, not `-> StepContext`** — hooks observe, they do not transform. Context flow stays exclusively in the step chain via `requires`/`provides`. This eliminates the "second communication channel" problem — hooks cannot inject data that a later step silently depends on. +- **`step_name: str`**, not the step object — hooks know what ran, but cannot call, inspect, or mutate the step instance. This prevents hooks from becoming an implicit dependency of step behavior. +- **Non-blocking** — hooks must not block the event loop. They are in the hot path between steps. Heavy work (HTTP POST, disk write) should be dispatched to a background task or queue, not done inline. Same constraint as `on_sample_done`. +- **No ordering guarantees between hooks** — hooks in the list are called sequentially in insertion order, but a hook must not depend on side effects of another hook. If ordering matters, combine them into one hook. +- **Exception isolation** — if a hook raises, the pipeline logs the error and continues. A broken metrics hook must not kill the pipeline. Hook exceptions are never surfaced in `SampleResult`. + +### Pipeline integration + +Hooks are set at construction time — they are structural, like steps. A pipeline's observation behavior is fixed for its lifetime. + +```python +class Pipeline: + def __init__(self, steps=None, hooks=None): + self._hooks = list(hooks or []) + ... +``` + +The step execution loop calls hooks around each foreground step: + +```python +for step in foreground_steps: + step_name = type(step).__name__ + for hook in self._hooks: + hook.before_step(step_name, ctx) + ctx = await step(ctx) + for hook in self._hooks: + hook.after_step(step_name, ctx) +``` + +Hooks fire for **foreground steps only**. Background steps (after `async_boundary`) do not trigger hooks — the caller has already moved on, and hook callbacks from background threads would violate the non-blocking contract. Background observability is handled via `background_stats()`. + +### Branch and nesting behavior + +- **Branch:** hooks fire once for the `Branch` step as a whole (`step_name = "Branch"`), not for each inner step of each child pipeline. Branch children are an internal implementation detail — hooks observe the outer pipeline's step sequence only. This keeps hook output predictable regardless of how many branches exist or how deep they nest. +- **Nested Pipeline-as-Step:** same rule. The outer pipeline fires hooks for the nested pipeline step (`step_name = "MySubPipeline"`), not for its inner steps. If the nested pipeline has its own hooks, those fire independently within its own execution. + +### Example: progress streaming for a web app + +```python +class ProgressHook: + """Pushes step events to an async queue for SSE streaming.""" + + def __init__(self, queue: asyncio.Queue): + self._queue = queue + + def before_step(self, step_name: str, ctx: StepContext) -> None: + self._queue.put_nowait({"type": "step_started", "step": step_name}) + + def after_step(self, step_name: str, ctx: StepContext) -> None: + self._queue.put_nowait({"type": "step_done", "step": step_name}) +``` + +```python +# Web endpoint wiring (not part of pipeline/) +queue = asyncio.Queue() +pipe = Pipeline(steps, hooks=[ProgressHook(queue)]) +asyncio.create_task(pipe.run_async(contexts)) +# SSE endpoint reads from queue +``` + +The hook implementation lives in the hosted deployment code, not in `pipeline/`. The pipeline engine provides the protocol and the call sites — nothing more. + +--- + +## Cancellation + +`cancel_token` lets a caller stop a running pipeline between steps. The motivating use case is a web app where the user clicks "Stop" and the server needs to halt processing without waiting for the remaining steps or samples to complete. + +### CancellationToken + +```python +class CancellationToken: + """Thread-safe cancellation signal.""" + + def __init__(self) -> None: + self._cancelled = threading.Event() + + def cancel(self) -> None: + """Signal cancellation. Thread-safe, idempotent.""" + self._cancelled.set() + + @property + def is_cancelled(self) -> bool: + return self._cancelled.is_set() +``` + +`threading.Event` rather than `asyncio.Event` because the token must be cancellable from any thread — a web endpoint handler, a background task, a signal handler. The pipeline checks it synchronously between steps, so no async machinery is needed. + +### Pipeline integration + +`cancel_token` is passed per-invocation on `run()` and `run_async()`, not on `__init__`. A token is scoped to a single execution — each web request creates a fresh token. The pipeline object stays reusable across runs. + +```python +pipe.run(contexts, cancel_token=token) +await pipe.run_async(contexts, cancel_token=token) +``` + +The runner checks the token at two points: + +1. **Before each foreground step** — if cancelled, the current sample gets `error=PipelineCancelled()` and `failed_at` set to the step that would have run next. +2. **Before each new sample** — if cancelled, remaining samples are not started. Samples already in-flight (via `workers > 1`) complete their current step but are cancelled before the next one. + +```python +for step in foreground_steps: + if cancel_token is not None and cancel_token.is_cancelled: + result.error = PipelineCancelled() + result.failed_at = type(step).__name__ + return result + ctx = await step(ctx) +``` + +### Contextvar bridge — making the token visible inside steps + +The pipeline checks the token between steps. Code *inside* a step (e.g. an LLM client making a streaming API call) may also want to check it — but steps, roles, and LLM clients do not receive the token as a parameter. + +The pipeline bridges this gap with a `contextvars.ContextVar`. Before running foreground steps, `run_async()` sets the current cancel token in the contextvar: + +```python +from contextvars import ContextVar + +cancel_token_var: ContextVar[CancellationToken | None] = ContextVar( + "cancel_token_var", default=None +) +``` + +```python +# Inside Pipeline.run_async() +_reset = cancel_token_var.set(cancel_token) +try: + # ... process samples, run steps +finally: + cancel_token_var.reset(_reset) +``` + +Any code in the call stack — a step, a role, an LLM client — can read the token without any signature changes: + +```python +# Inside LLM client code or any code inside a step — no parameter changes +token = cancel_token_var.get(None) +if token is not None and token.is_cancelled: + raise PipelineCancelled("Cancelled during LLM call") +``` + +`asyncio.to_thread()` (used by the pipeline for sync steps) automatically copies context variables to the worker thread, so the token is visible in sync steps too. + +**Why a contextvar and not a parameter:** The call chain from pipeline to LLM client crosses four layers (pipeline → step → role → client). Threading a parameter through every layer would require changing every method signature in between — steps and roles that have no business knowing about cancellation. A contextvar is the standard Python mechanism for request-scoped data that crosses layers without explicit plumbing. + +### What cancellation does NOT do + +- **It does not interrupt a running step by default.** Cancellation is checked *between* steps by the pipeline. Code inside a step can opt in to intra-step cancellation by reading `cancel_token_var` (see above) — but this is a step/client-level concern, not a pipeline-level one. +- **It does not cancel background steps.** Background work (after `async_boundary`) runs in separate threads and is not interrupted. `wait_for_background()` still works normally. If you need to cancel background work, shut down the step-class executors directly. +- **It does not affect hooks.** Hooks still fire for the step that was executing when cancellation was detected — `after_step` is called, then the cancellation check runs before the *next* step. + +### PipelineCancelled + +```python +class PipelineCancelled(Exception): + """Raised (internally) when a cancel_token is triggered between steps. + + Surfaces in ``SampleResult.error`` — never propagated to the caller + of ``run()`` / ``run_async()``. Callers check for this type to + distinguish cancellation from step failures. + """ +``` + +`PipelineCancelled` follows the same error-handling pattern as step exceptions: it is caught per-sample and recorded in `SampleResult`, not propagated. The runner continues to the next sample (which will also be cancelled if the token is still set). This means `run()` always returns a complete list of `SampleResult` — some successful, some failed, some cancelled. + +### Example: web app cancel endpoint + +```python +# Start a run +token = CancellationToken() +active_runs[run_id] = token +task = asyncio.create_task(pipe.run_async(contexts, cancel_token=token)) + +# Cancel endpoint +@app.post("/runs/{run_id}/cancel") +async def cancel_run(run_id: str): + active_runs[run_id].cancel() + return {"status": "cancelling"} +``` + +--- + +## Summary Table + +| Concept | Unit | Threading | Communication | +|---|---|---|---| +| `Step` | single unit of work | always sync | via `StepContext` | +| `Pipeline` | ordered step list for one input | `workers=N` across inputs | via `StepContext` | +| `Branch` | parallel pipeline list | always parallel internally | copy + merge of `StepContext` | +| `Pipeline` as a `Step` | reuse / nesting | inherits parent context | via `StepContext` | +| `PipelineHook` | observation point | runs in caller thread | `-> None` (read-only) | +| `CancellationToken` | lifecycle signal | thread-safe (`threading.Event`) | checked between steps | + +--- + +## What Was Rejected and Why + +**`PipelineProcess` (external wrapper):** +Adding a separate class to wrap pipelines with executor/queue machinery was considered. Rejected — it adds an indirection layer without benefit for this project's use case. Concurrency is declared on steps instead. + +**Special async pipeline subclass:** +Having an `AsyncPipeline` type was considered. Rejected — it mixes sequential logic with concurrency concerns in the same class. The `async_boundary` marker on steps is data-driven and doesn't require subclassing. + +**Full DAG executor (auto-inferred parallelism):** +The `requires`/`provides` graph already contains enough information to infer which steps can run in parallel. Deferred — `Branch` covers the explicit fork/join case; automatic DAG inference can be added later if needed. + +**Alternative `requires`/`provides` declaration styles:** +Four alternatives to plain set class attributes were considered: + +- `__init_subclass__` keyword args (`class MyStep(Step, requires={"agent_output"})`): moves the declaration to the class header but requires inheriting from a base `Step` class, eliminating the structural Protocol advantage — any object with the right attributes is a step without needing to inherit anything. +- `ClassVar` annotations (`requires: ClassVar[frozenset[str]] = ...`): more type-checker friendly but adds verbosity with no semantic change. +- Function decorator wrapping `__call__`: removes class boilerplate for stateless steps but introduces two styles (decorated functions vs classes with collaborators like `self.reflector`), inconsistency not worth the reduction. +- Decomposed signature / Hamilton-style (steps receive named fields as parameters instead of `StepContext`): elegant zero-annotation contracts — `requires` and `provides` are inferred from function signature at zero cost. Rejected because it loses explicit ordering control (order is inferred from data dependencies, not declared; independent steps have undefined order), collapses the two-tier `StepContext`/`metadata` structure into a flat dict (integration-specific data collides with shared fields), and makes side-effect steps with no consumed output impossible to anchor in the sequence. + +Plain set class attributes with pipeline normalization to `frozenset` at construction time is the right balance: explicit, readable, no inheritance required, and the ordering and context model stay intact. + +**Alternative hook/cancellation designs:** +Three alternatives to the observation-only `PipelineHook` + separate `cancel_token` design were considered: + +- Context-modifying hooks (`before_step` returns `StepContext`): hooks could transform context between steps — powerful but creates a second data-flow channel invisible to `requires`/`provides` validation. A hook could inject a field that a later step silently depends on, and the pipeline validator would not catch the dependency. Rejected to preserve the invariant that all data flow goes through the step chain. +- Cancellation as a hook (`CancellationHook` that raises in `before_step`): keeps everything in one mechanism, but mixes observation and control. If hooks are supposed to be safe to fail (exception isolation), a cancellation hook that *must* propagate its exception breaks that contract. Rejected — cancellation is a lifecycle concern, not an observation concern, so it gets its own parameter. +- Cancellation via `metadata` on `StepContext`: put a `CancellationToken` in `metadata` and have each step check it. Follows "behavior on the step" but couples every step to a cancellation concept, and steps that forget to check it silently ignore cancellation. Rejected — cancellation should be guaranteed by the pipeline, not opt-in per step. +- Additional `run_async` callback parameters (no hook protocol): add `on_step_done` and `cancel_token` as parameters on `run()`/`run_async()`, following the `on_sample_done` precedent. Minimal and consistent, but each new operational concern (metrics, billing, auth context) requires adding another parameter to the pipeline's public API, which accumulates over time. The hook protocol pays a small upfront design cost to avoid this parameter growth. + +--- + +## External Libraries Considered + +This pattern is known as **Pipes and Filters**. Several open source libraries implement variants of it. None were adopted — reasons below. + +**[Kedro](https://kedro.org/)** — closest to the `requires`/`provides` model. Nodes declare explicit named inputs and outputs; pipelines are composable. The gap: requires a "data catalog" abstraction for named datasets, has no `async_boundary` concept, and is oriented toward ML/ETL rather than agentic loops. Fighting the data catalog to pass a `StepContext` would cost more than writing the primitives cleanly. + +**[Hamilton](https://github.com/dagworks-inc/hamilton)** — lightest-weight equivalent. Functions declare inputs as parameters and outputs as return types; the framework infers the DAG. No server, no UI. The gap: no built-in async boundary, no fork/join `Branch`, no per-step `max_workers`. Gets contract validation for free but requires building all concurrency from scratch anyway. + +**[Pypeln](https://github.com/cgarciae/pypeln)** — designed for exactly the "process N samples through concurrent stages" problem. Has sync, thread, and async modes. The gap: no typed contracts, no `Branch`, no nested pipelines. Gets the `async_boundary`-style throughput but not the structural guarantees. + +**[Dagster](https://dagster.io/)** — closest overall feature set. Ops (≈ Steps) with typed inputs/outputs, jobs (≈ Pipelines), graph-based branching. The gap: it is a platform, not a library. Brings a scheduler, UI, asset catalog, and significant operational overhead. Too heavy to embed inside ACE. + +**Conclusion:** The specific combination of `async_boundary`, per-step `max_workers`, `Pipeline`-as-`Step` nesting, and `SampleResult` error wrapping is not provided by any of the above out of the box. Adapting any of them would cost as much as writing the ~300-line core cleanly. + +**What is borrowed rather than written:** `concurrent.futures.ThreadPoolExecutor` for the background step pools, and `asyncio.gather` (or `anyio` task groups) for `Branch` internals. diff --git a/docs/design/RR_DESIGN.md b/docs/design/RR_DESIGN.md new file mode 100644 index 0000000000000000000000000000000000000000..4b274ed35fa2a5e0df6a436e87a3b95ac2c7fff1 --- /dev/null +++ b/docs/design/RR_DESIGN.md @@ -0,0 +1,388 @@ +# Recursive Reflector (RR) Design + +Design document for the Recursive Reflector (`ace/steps/rr_step.py`). The RR is a PydanticAI-powered trace analyser that uses tool calls to execute Python code in a sandbox, decompose complex inputs via recursive child sessions, and produce structured reflections from agent execution traces. + +--- + +## Overview + +The Recursive Reflector replaces the single-pass `Reflector` with an iterative tool-calling agent. Instead of asking the LLM for a one-shot analysis, RR gives the LLM two tools — `execute_code` and `recurse` — and lets it explore trace data programmatically and decompose large inputs into focused sub-problems. + +**Key properties:** + +- `RRStep` is a subclass of `RecursiveAgent` (`ace/core/recursive_agent.py`). +- Satisfies both `StepProtocol` and `ReflectorLike` — usable as a pipeline step or a drop-in reflector replacement. +- Uses a single tool-using PydanticAI agent with `PromptedOutput(ReflectorOutput)`. +- The same RR agent gathers evidence with tools, records intermediate observations, and returns the final structured `ReflectorOutput`. +- Two-tier compaction (microcompaction + full summarization) handles context-window pressure. +- Depth-based recursion via the `recurse` tool decomposes large/complex inputs. +- PydanticAI's `UsageLimits` enforces token and request budgets. +- Produces `ReflectorOutput` with an enriched `raw["rr_trace"]` dict for observability. + +```python +from ace.steps.rr_step import RRStep, RRConfig + +# Drop-in replacement for Reflector +ace = ACELiteLLM(llm, reflector=RRStep("gpt-4o-mini", config=RRConfig(max_requests=30))) + +# Or as a pipeline step +pipe = Pipeline([..., RRStep("gpt-4o-mini"), ...]) +``` + +--- + +## Architecture + +### Inheritance + +``` +RecursiveAgent (ace/core/recursive_agent.py) + ├── execute_code tool (generic) + ├── recurse tool (generic, depth-based) + ├── Two-tier compaction + ├── Budget management (UsageLimits) + ├── create_sandbox() helper + └── on_compaction() callback + +RRStep(RecursiveAgent) (ace/steps/rr_step.py) + ├── RR-specific prompt building + ├── Trace/sandbox setup + ├── output_validator tool (ensure exploration before concluding) + ├── Timeout/error fallback with ground-truth comparison + └── Online mode skill evaluation +``` + +### Agent Loop + +``` +┌───────────────────────────────────────────────────────────────┐ +│ RRStep._run_reflection() │ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ PydanticAI Agent (model, output_type=ReflectorOutput) │ │ +│ │ │ │ +│ │ Tools: │ │ +│ │ ┌──────────────┐ ┌──────────┐ │ │ +│ │ │ execute_code │ │ recurse │ │ │ +│ │ │ (sandbox) │ │ (child │ │ │ +│ │ │ │ │ session) │ │ │ +│ │ └──────┬───────┘ └────┬─────┘ │ │ +│ │ │ │ │ │ +│ │ ▼ ▼ │ │ +│ │ TraceSandbox Child RRStep │ │ +│ │ exec() env (own sandbox, │ │ +│ │ own budget) │ │ +│ │ │ │ +│ │ Output: │ │ +│ │ ┌───────────────────────────────────────────────────┐ │ │ +│ │ │ ReflectorOutput (structured, validated) │ │ │ +│ │ │ + output_validator enforces exploration depth │ │ │ +│ │ └───────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ UsageLimits(total_tokens_limit, request_limit) │ +│ → compaction on context window pressure │ +│ → BudgetExhausted when total budget spent │ +└───────────────────────────────────────────────────────────────┘ +``` + +### Tools + +| Tool | Signature | Defined in | Description | +|------|-----------|------------|-------------| +| `execute_code` | `(code: str) -> str` | `RecursiveAgent` | Run Python in the `TraceSandbox`. Variables persist across calls, so the tool owns working state for evidence gathering: define variables, extract slices, compute checks, and verify contradictions. Tool output should stay terse and factual. It must not be used to print reflections, summaries, lessons, insights, analysis, or final reflection prose; those belong in `ReflectorOutput`. Raises `ModelRetry` on exceptions. | +| `think` | `(thought: str, evidence_refs: list[str] \| None) -> dict` | `RRStep` | Scratch prose channel for short working notes during the run (e.g. "mismatch confirmed, one more passenger-count check"). Notes are surfaced in `output.raw["thoughts"]` for inspection but **do not** propagate to the SkillManager. Conclusions, root cause, and key insight must therefore go in `ReflectorOutput`, not here. Persistent state for handoff to a sub-`recurse` belongs in a sandbox variable, not in `think`. | +| `recurse` | `(prompt: str, context_code: str) -> str` | `RecursiveAgent` | Spawn a child session with its own sandbox. Child inherits data and helpers. Use `context_code` to prepare the child's data. Not available at max depth. | +| `output_validator` | (on output) | `RRStep` | Ensures the RR agent has used `execute_code` at least once before producing its final `ReflectorOutput`. | + +RR uses one tool-capable structured-output agent. It may call `execute_code`, +`think`, skillbook inspection tools, and `recurse`, then stops using tools and +returns `ReflectorOutput` directly. There is no second conversion agent. +RR specializes the generic `execute_code` tool description for this step so the +model sees it as an evidence workbench rather than a prose-reporting channel. +RR also defaults to `temperature=0.0` for deterministic evidence analysis unless +the caller passes explicit `model_settings`. +For small traces, the generated data summary tells RR to use only a few focused +code checks and avoid transcript walkthroughs. + +### Dual Protocol Support + +```python +class RRStep(RecursiveAgent): + # StepProtocol — place in any Pipeline + requires = frozenset({"trace", "skillbook"}) + provides = frozenset({"reflections"}) + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: ... + + # ReflectorLike — use as drop-in reflector in runners + def reflect(self, *, question, agent_output, skillbook, ...) -> ReflectorOutput: ... +``` + +--- + +## Configuration + +### AgenticConfig (base) + +Defined in `ace/core/recursive_agent.py`. All fields inherited by `RRConfig`. + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `max_tokens` | `500_000` | Total token budget per agent run. When exhausted → `BudgetExhausted`. | +| `max_requests` | `50` | Safety cap on LLM requests per agent run. When hit → `BudgetExhausted`. | +| `context_window` | `128_000` | Model context window size. | +| `max_depth` | `2` | Max recursion depth. At max depth, `recurse` tool is not registered. | +| `child_budget_fraction` | `0.5` | Fraction of remaining token budget given to each child session. | +| `max_compactions` | `3` | Safety cap on full summarization rounds per session. | +| `microcompact_keep_recent` | `3` | Number of most recent tool results preserved during microcompaction. | +| `timeout` | `60.0` | Seconds per sandbox `execute()` call. Uses `signal.SIGALRM` on Unix. | +| `max_output_chars` | `20_000` | Per-execution stdout/stderr truncation limit. | +| `usage_callback` | `None` | Optional `(RequestUsage, model_id) -> None` hook fired once per completed pydantic-ai request (orchestrator turn, child session, compaction summary). Callback exceptions are swallowed, so a broken meter never crashes a run. Implemented via `ace.core.metered_model.MeteredModel`. | + +### RRConfig (alias for RecursiveConfig) + +Defined in `ace/implementations/rr/config.py`. Extends `AgenticConfig`. + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `max_output_chars` | `50_000` | Override: larger limit for trace analysis output. | + +All other fields are inherited from `AgenticConfig` with the same defaults. + +```python +from ace.steps.rr_step import RRConfig + +config = RRConfig( + max_requests=20, + max_depth=2, + timeout=60.0, + max_output_chars=50_000, +) +``` + +--- + +## Dependencies + +### AgenticDeps (base) + +Defined in `ace/core/recursive_agent.py`. + +| Field | Type | Description | +|-------|------|-------------| +| `config` | `AgenticConfig` | Configuration | +| `sandbox` | `Any` | TraceSandbox or compatible (used by `execute_code` and `recurse` tools) | +| `depth` | `int` | Current recursion depth | +| `max_depth` | `int` | Maximum recursion depth | +| `iteration` | `int` | Number of `execute_code` calls (incremented by the tool) | +| `run_session_fn` | `Callable` | Callback for spawning child sessions (wired by `RecursiveAgent.run()`) | +| `parent_usage_tokens` | `int` | Token usage from parent (for child budget computation) | + +### RRDeps + +Defined in `ace/implementations/rr/tools.py`. Extends `AgenticDeps`. + +| Field | Type | Description | +|-------|------|-------------| +| `trace_data` | `dict[str, Any]` | The canonical traces dict | +| `skillbook_text` | `str` | Skillbook text | + +--- + +## TraceSandbox + +Lightweight `exec()`-based sandbox for running LLM-generated Python code. Located in `ace/core/sandbox.py`. + +**Not a security sandbox.** Restricts builtins as defence-in-depth but relies on trusting the LLM not to generate malicious code. + +### Pre-loaded Namespace + +| Variable | Type | Description | +|----------|------|-------------| +| `traces` | `Any` | Raw trace payload (injected by `RRStep`) | +| `skillbook` | `str` | Skillbook text (injected by `RRStep`) | +| `helper_registry` | `dict` | Metadata for registered reusable helper functions | +| `register_helper` | `Callable` | Define and persist helper code for later calls and child sessions | +| `list_helpers` | `Callable` | Return registered helper names and descriptions | +| `run_helper` | `Callable` | Invoke a registered helper by name | +| `SHOW_VARS` | `Callable` | Print available variables (debugging) | +| `json`, `re`, `math`, `collections` | module | Standard library modules | +| `datetime`, `timedelta`, `date`, `time`, `timezone` | class | datetime classes | + +### Blocked Builtins + +`open`, `eval`, `exec`, `compile`, `input`, `globals`, `locals`, `breakpoint`, `memoryview` — all set to `None`. `__import__` is replaced with a safe import that only allows pre-loaded modules. + +### ExecutionResult + +```python +@dataclass +class ExecutionResult: + stdout: str = "" + stderr: str = "" + final_value: Any = None + exception: Optional[Exception] = None + + @property + def success(self) -> bool: + return self.exception is None +``` + +### Timeout Behaviour + +- **Unix (main thread):** Uses `signal.SIGALRM`. Raises `ExecutionTimeoutError` after `config.timeout` seconds. +- **Windows / non-main thread:** No timeout enforcement. + +### Runtime Helper Registry + +- `register_helper(name, source, description)` executes helper source code, stores it, and records metadata. +- Registered helpers persist across `execute_code` calls within the same session. +- Child sessions (via `recurse`) inherit registered helpers automatically. + +--- + +## Compaction + +When the agent's context window fills up, two-tier compaction kicks in: + +``` +agent running + ↓ +PydanticAI: UsageLimitExceeded + ↓ +Budget exhausted? → YES: raise BudgetExhausted → fallback output + → NO: context window hit, continue ↓ + ↓ +Tier 1: microcompact(messages, keep_recent=3) + - Clear old execute_code tool results + - Keep last 3 tool results intact + - Keep all model messages (reasoning chain) + ↓ +Changed? → YES: retry with compacted history + → NO: fall through to tier 2 ↓ + ↓ +Tier 2: summarize_and_compact() + - compaction_count++ (cap at max_compactions=3) + - LLM summarizes progress (1 request from budget) + - Save pre-compaction context to sandbox `history` variable + - Replace history with [summary + continuation prompt] + - Retry with compacted history +``` + +### Compaction Callback + +`RecursiveAgent.on_compaction()` saves compaction metadata to the sandbox's `history` variable so the agent can reference prior context after compaction. + +--- + +## Recursion + +The `recurse` tool enables depth-based decomposition: + +- Root agent runs at `depth=0` with `recurse` available (if `max_depth > 0`) +- Each `recurse` call spawns a child at `depth + 1` with its own sandbox and budget +- At `depth == max_depth`, `recurse` is not registered — the agent must analyze directly +- Child sandbox inherits all non-internal, non-callable variables from parent +- Registered helpers are rehydrated in child sandboxes +- Child budget: `remaining_tokens * child_budget_fraction` + +--- + +## Timeout / Fallback + +When `BudgetExhausted` is raised (token or request budget spent): + +1. `RRStep._build_budget_exhausted_output()` constructs a `ReflectorOutput` with `raw["timeout"] = True`. +2. If `agent_output` and `ground_truth` are available, `_build_timeout_output()` includes a simple correct/incorrect assessment. + +When any other exception occurs, a minimal `ReflectorOutput` is returned with `raw["error"]`. + +--- + +## Online Mode Skill Evaluation + +When `ctx.mode == "online"` and the skillbook is non-empty, `RRStep` appends skill evaluation instructions to the prompt. The agent: + +1. Scans trace text for skill ID citations (`[section-NNNNN]`) +2. Verifies each cited ID exists in the skillbook +3. Classifies each as `helpful`, `harmful`, or `neutral` +4. Includes results in the `skill_tags` output field + +In offline mode, skill evaluation is skipped (traces may be from external agents with no skill IDs). + +--- + +## Traces Input + +The `traces` variable in the sandbox contains the raw data structure: + +```python +{ + "question": str, # The question/task + "ground_truth": str | None, # Expected answer + "feedback": str | None, # Environment feedback + "steps": [ # Agent execution steps + { + "role": "agent", + "reasoning": str, + "answer": str, + "skill_ids": list[str], + } + ], +} +``` + +For arbitrary trace inputs, the agent discovers the structure via `execute_code` and decomposes via `recurse` if needed. + +--- + +## rr_trace Output Schema + +`RRStep` enriches `ReflectorOutput.raw` with execution metadata: + +```python +{ + "rr_trace": { + "total_iterations": int, # Number of execute_code calls + "subagent_calls": list, # Reserved for future use + "timed_out": bool, # Whether budget was exhausted + "compactions": int, # Number of compaction rounds + "depth": int, # Recursion depth of this session + }, + "usage": { + "input_tokens": int, + "output_tokens": int, + "total_tokens": int, + "requests": int, + }, +} +``` + +--- + +## Observability + +Logfire auto-instruments PydanticAI agents, providing: + +- Per-agent-run traces with spans for each LLM request and tool call +- Token usage tracking +- Latency metrics +- No explicit opt-in step required in the pipeline + +The `rr_trace` dict in `ReflectorOutput.raw` provides programmatic access to iteration counts and metadata. + +--- + +## Public API + +```python +from ace.steps.rr_step import ( + RRStep, # Main entry point (RecursiveAgent subclass) + RRConfig, # Configuration (alias for RecursiveConfig) + RRDeps, # PydanticAI RunContext dependencies + TraceSandbox, # Sandbox for code execution + ExecutionResult, # Result of sandbox.execute() + ExecutionTimeoutError, +) +``` diff --git a/docs/design/SKILLBOOK_V2_PLAN.md b/docs/design/SKILLBOOK_V2_PLAN.md new file mode 100644 index 0000000000000000000000000000000000000000..6eb088978aed89f8b924dba38672440e3fb264ff --- /dev/null +++ b/docs/design/SKILLBOOK_V2_PLAN.md @@ -0,0 +1,245 @@ +# Skillbook v2 — Design & Execution Plan + +Status: approved. This document is the frozen design for the skillbook refactor. + +--- + +## Vision + +- **Issues are the primary object.** A skill's core content is a prose description of the problem it addresses, with scope expressed inline. +- **Insights are mandatory for context skills, optional for harness.** Context skills must carry the imperative action the agent should follow — that's the whole point. Harness skills may be pure issue catalogs (a problem in the runtime environment, no agent-side workaround available yet); if a harness workaround does exist, it goes in `insight`. +- **Fine-grained categories stay structured.** The old specific category / topic labels are preserved as `keywords`; they do not get collapsed into free text and they are not replaced by the binary `section`. +- **Scope is emergent.** New issues start narrow; the SkillManager widens the scope text recursively as it sees the same issue recur across domains/traces. No separate scope field — widening = rewriting the `issue` prose. +- **Skillbook search can go hybrid.** BM25 + dense, fused via RRF. Flat structure retained. +- **Dashboard-first mindset.** Schema + provenance must support issue dashboards, occurrence heatmaps, effectiveness KPIs. + +--- + +## Final `Skill` schema + +```python +@dataclass +class Skill: + id: str + section: Literal["context", "harness"] # pipeline-facing split only + keywords: List[str] # fine-grained category/topic labels (required, normalized) + issue: str # prose problem + scope inline — required + insight: Optional[str] # imperative action — required for context, optional for harness + occurrences: List[InsightSource] # append-only audit chain; auto-appended on every mutation + active: bool = True + used_count: int = 0 + helpful_count: int = 0 + harmful_count: int = 0 + neutral_count: int = 0 + embedding: Optional[List[float]] = None # stored in sidecar .npz, not JSON + created_at: str + updated_at: str +``` + +**Dropped fields:** `content`, `justification`, `evidence`. +**Rename:** `sources` → `occurrences`. +**Section semantics:** `section` is no longer the old free-form category field. It is only the binary split `context|harness`. Fine-grained categorization now lives in `keywords`. +**Invariants enforced in `Skillbook.add_skill` / `update_skill`:** +- `section ∈ {"context", "harness"}` — reject otherwise. +- `keywords` required, non-empty, normalized by stripping / lowercasing / de-duping while preserving order. +- `issue` always required, non-empty. +- `insight` required + non-empty when `section="context"`; may be `None` or empty when `section="harness"`. +- Any mutation invalidates `embedding` (set to `None` so it recomputes on next retrieval). + +--- + +## Storage — split embeddings + +- `skillbook.json` — diffable. `Skill` entries never carry `embedding`. +- `skillbook.embeddings.npz` — `numpy.savez_compressed`, keyed by `skill_id`, float32. **Cache only.** Can be deleted and recomputed lazily. +- `save_to_file(path)` writes both. +- `load_from_file(path)` loads JSON; loads `.npz` if present (silent no-op otherwise). +- Schema version check on load: JSON must contain `"schema_version": "2"`. Missing/mismatched → `raise ValueError("Skillbook format v2 required — regenerate")`. Hard break confirmed. + +--- + +## Tool surface — atomic, no micro-tools + +Full signatures. `issue` required on every mutation; `keywords` required on add and optional on update (omit to keep current); `insight` required when `section="context"`, optional when `section="harness"`: + +```python +add_skill(section, issue, keywords, insight=None) -> {ok, skill_id} # insight required iff section="context" +update_skill(skill_id, issue, keywords=None, insight=None) -> {ok} # omit keywords / insight to keep current values +tag_skill(skill_id, delta) -> {ok} # delta ∈ {-1, 0, 1} +remove_skill(skill_id, reason) -> {ok} # SOFT default — sets active=False, keeps history +search_skills(query, top_k=5, section=None, keywords=None) -> [...] +read_skill(skill_id) -> {id, section, keywords, issue, insight, counters, active, occurrences} +``` + +**`add_skill` / `update_skill` auto-append an `InsightSource`** from the current trace. Every mutation is recorded in `occurrences`. `tag_skill` auto-appends an observation entry too. + +No `widen_scope` / `codify_insight` / `add_occurrence` micro-tools — all mutations go through the atomic `update_skill` to prevent partial/stale states. + +--- + +## Provenance wiring (closes current gap) + +**Problem today:** `Skill.sources` is always `[]` because SM tools never thread `insight_source=`. Grepped to confirm. + +**Fix:** + +1. `UpdateStep.__call__` builds an `InsightSource` from `ctx.trace` (`trace_uid`, `source_system`, `trace_id`, `sample_question`) + `ctx.epoch` + `reflections[0].error_identification` + `reflections[0].key_insight`. +2. `SkillManager.update_skills(..., source: InsightSource)` — new required kwarg. +3. `SMDeps.current_source: InsightSource` — available to every tool. +4. `add_skill` / `update_skill` / `tag_skill` tools derive per-op `InsightSource` (copying identity, setting `operation_type`, appending op-specific `error_identification` / `learning_text`), and pass `insight_source=` through to the underlying `Skillbook` method. + +Result: `skill.occurrences` populates naturally. Dashboard has data. + +--- + +## Embedding input formula + +```python +parts = [issue] +if insight is not None: + parts.append(insight) +if keywords: + parts.append(f"Keywords: {', '.join(keywords)}") +embedding_input = "\n\n".join(parts) +``` + +So search / dedup consumers can match on problem text, action text, and structured category labels. Invalidate on any mutation of `issue`, `insight`, or `keywords`. + +--- + +## Prompt rendering + +`Skillbook.as_prompt()` remains a compatibility / helper surface. It should render only skills where `active=True`, grouped by section, using the new `issue` / `insight` fields: + +``` +## context +- [context-00007] + Keywords: airline, booking_api, cabin_class + Issue: In tau-airline's update_reservation_flights API, cabin class is a single param applied to all legs/passengers — no per-leg or per-passenger differentiation. + Insight: Before offering per-passenger or per-leg upgrades, immediately tell the user that cabin class is all-or-nothing, then present only all-or-nothing options. + +## harness +- [harness-00003] + Keywords: tau2, rate_limit, retries + Issue: tau2 runner retries Bedrock 429 with 60s exponential backoff, blocking the whole pipeline. Observed in airline + retail runs. +``` + +This phase does **not** decide rollout retrieval or prompt-injection policy. `as_prompt()` is kept as a generic rendering helper for debugging, exports, and backward-compatible callers. + +--- + +## SM prompt rewrite (`ace/implementations/prompts.py`) + +- Declare the two-section taxonomy and the insight-required-iff-context invariant. +- Declare the distinction between binary `section` (`context|harness`) and fine-grained `keywords`. +- Require `issue` on every ADD/UPDATE; require `insight` only when `section="context"`. +- Require non-empty `keywords` on ADD. Guide: 1-5 short stable labels such as domain, subsystem, API family, or behavior category. +- Guide: write `issue` as problem + applicability inline (start narrow — single domain / single API / single endpoint). +- **Recursive widening rule:** if `search_skills` returns a semantically overlapping issue from another domain, call `update_skill` with a broader `issue` that covers both contexts, rather than creating a new skill. +- When broadening or merging a skill, update `keywords` too: keep useful existing labels, add genuinely new ones, and drop stale labels that no longer fit. +- **Duplicate-avoidance:** always `search_skills` before `add_skill`. +- Soft-delete semantics: `remove_skill` for skills that are harmful or outdated; audit chain is preserved and `active=False` skills are excluded from normal active-skill views. + +--- + +## Skillbook search / retrieval (tooling only) + +File: `ace/implementations/skill_rendering.py`. + +`retrieve_top_k(skillbook, query, *, top_k=5, section=None, keywords=None)`: +1. Optional `section` pre-filter (`skillbook._sections` already indexed). +2. Optional `keywords` filter / boost against `skill.keywords`. +3. BM25 rank over `issue + insight + keywords` text (lexical). +4. Dense cosine rank over embeddings. Query embedding failure → `raise` (already done). +5. Reciprocal Rank Fusion (k=60). Return top-k. + +Add dep: `rank-bm25` (MIT, ~50 LOC wrapping). No infra. + +This section applies to `search_skills` / inspection flows only. Agent-side retrieval and prompt-injection policy are explicitly deferred. + +--- + +## Files to touch + +Core (CLAUDE.md-gated — user pre-approved): +- `ace/core/skillbook.py` — `Skill` rewrite, `UpdateOperation` rewrite (drop content/justification/evidence fields, add keywords/issue/insight), `add_skill`/`update_skill`/`remove_skill` (default `soft=True`), `to_dict`/`from_dict` with `schema_version="2"` check, sidecar save/load, `as_prompt()` field rendering update, `to_llm_dict`, `_apply_operation`. +- `ace/core/insight_source.py` — no change (fields already sufficient). + +Integration: +- `ace/deduplication/detector.py` — embedding input changed to `issue + insight + keywords`. Line 178 needs update (`s.content` → new formula). Invalidation hook on update (skill.embedding = None). +- `ace/deduplication/prompts.py` — referencing `skill_a.content` (lines 54, 56, 113, 114) → update to `issue` + `keywords` context. +- `ace/deduplication/operations.py` — referencing `.content` writes (lines 113, 153) → update to `.issue` / `.insight`. +- `ace/implementations/sm_tools.py` — rewrite all tool signatures (`add_skill`, `update_skill`, `tag_skill`, `remove_skill`, `search_skills`, `read_skill`). Thread `ctx.deps.current_source` into every mutation. +- `ace/implementations/skill_manager.py` — accept `source: InsightSource` on `update_skills`, store on `SMDeps.current_source`. +- `ace/implementations/prompts.py` — full SM prompt rewrite. +- `ace/implementations/rr/tools.py` — `read_skill` return dict (line 89) → new fields. `search_skillbook` return (line 122) → new fields. +- `ace/implementations/skill_rendering.py` — `render_skills_xml` (line 47) → new fields + hybrid BM25+RRF + `section` / `keywords` support. +- `ace/implementations/helpers.py` — line 59 renders `skill.content` → swap to issue/insight. +- `ace/steps/update.py` — build `InsightSource` from `ctx.trace` and pass to `SM.update_skills(source=...)`. +- `ace/steps/export_markdown.py` — lines 44, 46-47, 49-50 reference old fields → update. + +Tests / examples: +- `tests/` — fixtures will break on load (different field names); update. +- `examples/` — skim for any `.content` / `.justification` / `.evidence` reads. + +New dep: `rank-bm25`. + +--- + +## Smoke test + +Run after changes land: + +```bash +uv run ace-eval e2e \ + --benchmark tau-bench-airline \ + --traces results/e2e/run_784b73163157/collection \ + --agent-model bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 \ + --reflector-model bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 \ + --skill-manager-model bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 \ + --user-model bedrock/openai.gpt-oss-120b-1:0 \ + --reflector-type rr \ + --num-trials 1 --max-num-steps 50 --max-workers 1 \ + --no-benchmark --logfire --verbose +``` + +Verify: +- No crashes. +- `skillbook.json` conforms to v2 shape. +- `skillbook.embeddings.npz` created. +- Every skill has `len(occurrences) >= 1`. +- Every skill has `len(keywords) >= 1`. +- Context skills have non-null `insight`; harness skills may or may not. +- Logfire shows `sm.session` → `add_skill` / `update_skill` with atomic signatures. + +--- + +## Out of scope + +- Dashboard UI — data shape is sufficient; build later. +- Agent-side retrieval / prompt injection policy — defer to a later plan; this document does not decide whether skills are fetched by a pre-step, tool calls, or some other rollout path. +- Cross-encoder reranker — not worth it below ~2000 skills. +- Multi-vector embeddings (content + use-case split) — not needed; single concat works. +- Hierarchical taxonomy — explicitly rejected. Structured flat `keywords` are sufficient. +- Query expansion / HyDE — defer until retrieval misses observed in production. +- SQLite migration — JSON+sidecar is right for <5K skills. + +--- + +## Open decisions + +- **`UpdateOperation` audit-log fields:** drop `content/justification/evidence`, add `issue/insight`. Keep structure identical otherwise. +- **Section validation:** add a module-level constant `VALID_SECTIONS = frozenset({"context", "harness"})` and validate in `add_skill` + `update_skill` (via `section=` lookup from existing skill). +- **Keyword normalization:** store `keywords` as short lowercase identifiers; de-dupe while preserving order. +- **`_generate_id` prefix:** stays as `section.split()[0].lower()` → yields `context-00001` / `harness-00001` naturally. +- **Hard purge:** expose `Skillbook.purge(skill_id)` as a module-level method NOT wired to any SM tool. Human-operator / CLI only. + +--- + +## Already done in this branch + +- [x] `ace/implementations/skill_rendering.py:96-101` — `retrieve_top_k` raises on embedding failure (no silent fallback). +- [x] `ace/core/recursive_agent.py` — `span_label` threaded through `run_agent_with_compaction` and `RecursiveAgent`; SkillManager emits `sm.session` spans distinct from RR's `rr.session`. +- [x] `ace/implementations/rr/config.py` — `cache_prompts` / `cache_ttl` added to `RecursiveConfig` (was previously an `AttributeError`). +- [x] `ace-eval/src/ace_eval/e2e/training.py` — `_train_sequential` surfaces `SampleResult.error` instead of silently swallowing. +- [x] `ace/implementations/skill_manager.py` — passes `span_label="sm"` to superclass. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 0000000000000000000000000000000000000000..d2a932f6a0da86eabb35743a87c6d26de3c244c5 --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,109 @@ +# Installation + +## For Users + +=== "uv" + + ```bash + uv add ace-framework + ``` + +=== "With extras" + + ```bash + uv add 'ace-framework[all]' # All optional features + uv add 'ace-framework[instructor]' # Structured outputs (Instructor) + uv add 'ace-framework[langchain]' # LangChain integration + uv add 'ace-framework[browser-use]' # Browser automation + uv add 'ace-framework[claude-code]' # Claude Code CLI integration + uv add 'ace-framework[claude-sdk]' # Anthropic SDK integration steps + uv add 'ace-framework[observability]' # Opik monitoring + cost tracking + uv add 'ace-framework[deduplication]' # Skill deduplication (embeddings) + uv add 'ace-framework[transformers]' # Local model support + ``` + +## For Contributors + +=== "UV (Recommended)" + + ```bash + git clone https://github.com/kayba-ai/agentic-context-engine + cd agentic-context-engine + uv sync # Installs everything (10-100x faster than pip) + ``` + +=== "uv" + + ```bash + git clone https://github.com/kayba-ai/agentic-context-engine + cd agentic-context-engine + uv add -e . + ``` + +## Requirements + +- **Python 3.12** +- An API key for your LLM provider + +## Configure Your LLM + +The recommended way to set up your API keys and model selection: + +```bash +ace setup +``` + +This interactive wizard validates your API key and model, then saves config to `ace.toml` (model names, safe to commit) and `.env` (API keys, gitignored). See [Setup](setup.md) for full details. + +### Manual alternative + +If you prefer not to use the wizard, set environment variables directly: + +```bash +export OPENAI_API_KEY="sk-..." +``` + +Or create a `.env` file (add to `.gitignore`): + +```bash +OPENAI_API_KEY=sk-... +``` + +## Verify Installation + +```python +from ace import ACELiteLLM + +# Uses ace.toml + .env from `ace setup` +agent = ACELiteLLM.from_setup() +print(agent.ask("Hello!")) +``` + +Or without `ace setup`: + +```python +agent = ACELiteLLM.from_model("gpt-4o-mini") +print(agent.ask("Hello!")) +``` + +## Set Up Coding Agent Skills (Optional) + +If you want the hosted `kayba` CLI or `kayba setup`, install the cloud extra first: + +```bash +uv add 'ace-framework[cloud]' +``` + +Then, if you use Claude Code, install the Kayba pipeline skill: + +```bash +kayba setup +``` + +This installs the evaluation pipeline skill to `.claude/skills/` and prints CLI instructions. See [Hosted API](../integrations/hosted-api.md) for details. + +## What to Read Next + +- [Setup](setup.md) — configure models and API keys +- [Quick Start](quick-start.md) — build your first self-learning agent +- [How ACE Works](../concepts/overview.md) — understand the architecture diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md new file mode 100644 index 0000000000000000000000000000000000000000..21355e88969ae2b7e345b5384f018bfa7ce92e88 --- /dev/null +++ b/docs/getting-started/quick-start.md @@ -0,0 +1,151 @@ +# Quick Start + +Get a self-learning agent running in under a minute. + +## Simplest Example + +If you've run `ace setup` (see [Setup](setup.md)), you can load your config automatically: + +```python +from ace import ACELiteLLM + +agent = ACELiteLLM.from_setup() + +# Ask related questions — the agent learns patterns across them +answer1 = agent.ask("If all cats are animals, is Felix (a cat) an animal?") +answer2 = agent.ask("If all birds fly, can penguins (birds) fly?") + +print(f"Learned {len(agent.skillbook.skills())} strategies") + +# Save and reload later +agent.save("my_agent.json") +``` + +Or specify a model directly (API key must be in the environment): + +```python +agent = ACELiteLLM.from_model("gpt-4o-mini") +``` + +## Choose Your Integration + +=== "LiteLLM" + + The simplest path. Supports 100+ LLM providers. + + ```python + from ace import ACELiteLLM + + agent = ACELiteLLM.from_model("gpt-4o-mini") + answer = agent.ask("Your question") + agent.save("learned.json") + ``` + +=== "LangChain" + + Wrap any LangChain Runnable (chains, agents, graphs) with learning. + + ```python + from ace import LangChain + + runner = LangChain.from_model(your_chain, ace_model="gpt-4o-mini") + results = runner.run([{"input": "Your task"}]) + runner.save("chain_expert.json") + ``` + +=== "Browser-Use" + + Browser automation that learns navigation patterns. + + ```python + from ace import BrowserUse + from langchain_openai import ChatOpenAI + + runner = BrowserUse.from_model( + browser_llm=ChatOpenAI(model="gpt-4o"), + ace_model="gpt-4o-mini", + ) + results = runner.run("Find the top post on Hacker News") + runner.save("browser_expert.json") + ``` + +=== "Claude Code" + + Self-improving coding agent using the Claude Code CLI. + + ```python + from ace import ClaudeCode + + runner = ClaudeCode.from_model(working_dir="./my_project") + results = runner.run("Add unit tests for utils.py") + runner.save("coding_expert.json") + ``` + +## Full Pipeline Example + +For full control, use the three ACE roles directly: + +```python +from ace import ( + ACE, Agent, Reflector, SkillManager, + Sample, SimpleEnvironment, +) + +# Create roles (each takes a model string directly) +agent = Agent("gpt-4o-mini") +reflector = Reflector("gpt-4o-mini") +skill_manager = SkillManager("gpt-4o-mini") + +# Build the adaptive pipeline +runner = ACE.from_roles( + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + environment=SimpleEnvironment(), +) + +# Train on samples +samples = [ + Sample(question="What is the capital of France?", context="", ground_truth="Paris"), + Sample(question="What is 2 + 2?", context="", ground_truth="4"), +] + +results = runner.run(samples, epochs=2) +print(f"Learned {len(runner.skillbook.skills())} strategies") +runner.save("trained.json") +``` + +## Loading Saved Agents + +```python +from ace import ACELiteLLM + +# Resume from a saved skillbook +agent = ACELiteLLM.from_model("gpt-4o-mini", skillbook_path="my_agent.json") +answer = agent.ask("New question") # Uses previously learned strategies +``` + +## Trying Different Models + +```python +from ace import ACELiteLLM + +# OpenAI +agent = ACELiteLLM.from_model("gpt-4o-mini") + +# Anthropic +agent = ACELiteLLM.from_model("claude-sonnet-4-5-20250929") + +# Google +agent = ACELiteLLM.from_model("gemini-pro") + +# Local (Ollama) +agent = ACELiteLLM.from_model("ollama/llama2") +``` + +## What to Read Next + +- [How ACE Works](../concepts/overview.md) — understand the three-role architecture +- [The Skillbook](../concepts/skillbook.md) — how strategies are stored and evolve +- [Full Pipeline Guide](../guides/full-pipeline.md) — build custom ACE pipelines +- [Integrations](../integrations/index.md) — LangChain, Browser-Use, Claude Code diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md new file mode 100644 index 0000000000000000000000000000000000000000..4772046d2b42ab20a8ea9f0ac75023619723eddc --- /dev/null +++ b/docs/getting-started/setup.md @@ -0,0 +1,226 @@ +# Setup + +Configure your LLM provider and model selection for ACE. + +## Guided Setup (Recommended) + +The `ace setup` command walks you through configuration interactively — it validates the connection first, and only asks for credentials if needed. + +```bash +ace setup +``` + +``` +ACE Setup + +Step 1: Choose your model + + Examples: gpt-4o-mini, claude-sonnet-4-20250514, ollama/llama2 + Search models: ace models <query> + + Default model: gpt-4o-mini + v Connected! (gpt-4o-mini via openai, 203ms) + Using OPENAI_API_KEY + +Step 2: Role assignment + + ACE uses three roles. You can assign a different model to each, + or use the same model for all (recommended to start). + + Use this model for all roles? [Y/n]: n + + Agent (executes tasks) [gpt-4o-mini]: claude-sonnet-4-20250514 + ! No credentials found for anthropic + ANTHROPIC_API_KEY: sk-ant-... + v Connected! (claude-sonnet-4-20250514 via anthropic, 347ms) + v Saved credentials to .env + + Reflector (analyses results) [gpt-4o-mini]: + Skill Manager (updates skillbook) [gpt-4o-mini]: + +v Saved model config to ace.toml + + Configuration summary: + default: gpt-4o-mini + agent: claude-sonnet-4-20250514 +``` + +The wizard tries the connection immediately — if your credentials are already in the environment (via `.env`, exported variables, or cloud auth like AWS), it just works. It only prompts for keys when the connection actually fails. + +This creates two files: + +| File | Contains | Commit to git? | +|------|----------|----------------| +| `.env` | API keys only | No (gitignore it) | +| `ace.toml` | Model names per role | Yes (no secrets) | + +Then in your code: + +```python +from ace import ACELiteLLM + +ace = ACELiteLLM.from_setup() +answer = ace.ask("What is 2+2?") +``` + +## Manual Setup + +If you prefer not to use the CLI, set environment variables directly. + +### 1. Set API keys + +=== "Shell" + + ```bash + export OPENAI_API_KEY="sk-..." + export ANTHROPIC_API_KEY="sk-ant-..." + ``` + +=== ".env file" + + ```bash + # .env (add to .gitignore) + OPENAI_API_KEY=sk-... + ANTHROPIC_API_KEY=sk-ant-... + ``` + +### 2. Use in code + +```python +from ace import ACELiteLLM + +# Single model for all roles +ace = ACELiteLLM.from_model("gpt-4o-mini") +``` + +```python +from ace import ACELiteLLM, ACEModelConfig, ModelConfig + +# Different models per role +ace = ACELiteLLM.from_config(ACEModelConfig( + default=ModelConfig(model="gpt-4o-mini"), + agent=ModelConfig(model="claude-sonnet-4-20250514"), +)) +``` + +## Per-Role Model Selection + +ACE has three roles, each making LLM calls. You can assign different models to optimise cost vs quality: + +| Role | What it does | Recommendation | +|------|-------------|----------------| +| **Agent** | Executes tasks, produces answers | Strong reasoning model | +| **Reflector** | Analyses results, extracts lessons | Good analysis, lower cost OK | +| **Skill Manager** | Updates the skillbook | Structured output reliability | + +Example `ace.toml`: + +```toml +[default] +model = "gpt-4o-mini" + +[agent] +model = "claude-sonnet-4-20250514" +max_tokens = 4096 + +[reflector] +model = "gpt-4o-mini" +``` + +Roles without an explicit section use `[default]`. + +## Discovering Models + +### Search available models + +Use multiple terms to narrow results — all terms must match: + +```bash +ace models claude # All Claude models +ace models haiku us # Only US-region Haiku models +ace models gpt 4o # GPT-4o variants +ace models --provider openai # All OpenAI models +``` + +Output shows model name, provider, pricing, and whether your API key is configured: + +``` +Model Provider Input $/M Output $/M Key +------------------------------------------------------------------------------------------ +us.anthropic.claude-haiku-4-5-20251001-v1:0 bedrock_converse $1.10 $5.50 v +claude-haiku-4-5-20251001 anthropic $1.00 $5.00 x + +Showing 20 of 40 models. Narrow your search: ace models <query> or use --limit 40 +``` + +### Validate a specific model + +```bash +ace validate us.anthropic.claude-haiku-4-5-20251001-v1:0 +``` + +Makes a tiny test call (3 tokens) to confirm the key, model, and network all work. + +## Supported Providers + +ACE uses [LiteLLM](https://docs.litellm.ai/) for model access. Any model string LiteLLM supports will work: + +| Provider | Model Example | Env Variable | +|----------|--------------|--------------| +| OpenAI | `gpt-4o-mini` | `OPENAI_API_KEY` | +| Anthropic | `claude-sonnet-4-20250514` | `ANTHROPIC_API_KEY` | +| AWS Bedrock | `us.anthropic.claude-haiku-4-5-20251001-v1:0` | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` + `AWS_REGION_NAME` | +| Google Gemini | `gemini/gemini-2.0-flash` | `GEMINI_API_KEY` | +| DeepSeek | `deepseek/deepseek-chat` | `DEEPSEEK_API_KEY` | +| Groq | `groq/llama-3.1-70b` | `GROQ_API_KEY` | +| Ollama (local) | `ollama/llama2` | --- | +| Azure OpenAI | `azure/gpt-4` | `AZURE_API_KEY` | +| OpenRouter | `openrouter/anthropic/claude-3.5-sonnet` | `OPENROUTER_API_KEY` | + +100+ providers supported. Run `ace models` to search the full catalog. + +## Troubleshooting + +### "No ace.toml found" + +Run `ace setup` or use `ACELiteLLM.from_model("gpt-4o-mini")` instead of `from_setup()`. + +### "Invalid API key" + +```bash +# Re-validate +ace validate gpt-4o-mini + +# Re-run setup to fix +ace setup +``` + +### "Model not found" + +The model string may have a typo. `ace validate` and `ace setup` suggest alternatives: + +```bash +ace validate claud-sonnet +# x Model 'claud-sonnet' not found at the provider. +# Did you mean: +# - claude-sonnet-4-20250514 +# - claude-3-5-sonnet-20241022 +``` + +### "Could not detect a provider" + +Use the `provider/model-name` format: + +```bash +# Instead of just "llama2": +ollama/llama2 +groq/llama-3.1-70b +``` + +Search for the correct model string: `ace models llama` + +## What to Read Next + +- [Quick Start](quick-start.md) --- build your first self-learning agent +- [How ACE Works](../concepts/overview.md) --- understand the three-role architecture +- [Integrations](../integrations/index.md) --- LangChain, Browser-Use, Claude Code diff --git a/docs/guides/async-learning.md b/docs/guides/async-learning.md new file mode 100644 index 0000000000000000000000000000000000000000..b84fa1afb1dfec5cb4e9fc7ac09b469bd47e649e --- /dev/null +++ b/docs/guides/async-learning.md @@ -0,0 +1,90 @@ +# Async Learning + +By default, learning (Reflect, Tag, Update, Apply) runs synchronously after each sample. With async learning, the Agent returns immediately while learning continues in the background. + +## Architecture + +```mermaid +graph LR + S1[Sample 1] --> A[Agent] + S2[Sample 2] --> A + S3[Sample 3] --> A + A -->|foreground| E[Environment] + E -->|background| R1[Reflector 1] + E --> R2[Reflector 2] + E --> R3[Reflector 3] + R1 --> Q[Queue] + R2 --> Q + R3 --> Q + Q -->|serialized| SM[SkillManager] + SM --> SK[Skillbook] +``` + +- **Reflectors** run concurrently (safe — they only read the skillbook) +- **SkillManager** runs sequentially (required — it writes to the skillbook) +- The Agent uses whatever skillbook state is available (eventual consistency) + +## Basic Usage + +Pass `wait=False` to `run()`: + +```python +from ace import ACE + +runner = ACE.from_roles( + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + environment=environment, +) + +# Agent returns fast — learning continues in background +results = runner.run(samples, epochs=3, wait=False) + +# Use results immediately +for r in results: + print(r) + +# Wait before saving +runner.wait_for_background() +runner.save("learned.json") +``` + +## Monitoring Progress + +```python +stats = runner.learning_stats +# {'active': 5, 'completed': 25} +``` + +## With ACELiteLLM + +```python +from ace import ACELiteLLM, Sample, SimpleEnvironment + +agent = ACELiteLLM.from_model("gpt-4o-mini") + +samples = [Sample(question="...", context="", ground_truth="...")] +results = agent.learn(samples, environment=SimpleEnvironment(), wait=False) + +# Agent is immediately available +answer = agent.ask("New question") + +# Wait when you need to save +agent.wait_for_background() +agent.save("learned.json") +``` + +## Why This Architecture + +| Component | Parallelizable? | Reason | +|-----------|----------------|--------| +| Reflector | Yes | Only reads the skillbook, produces independent analysis | +| SkillManager | No | Writes to the skillbook, handles deduplication | + +This gives ~3x faster learning when the Reflector LLM calls run concurrently. + +## What to Read Next + +- [Full Pipeline Guide](full-pipeline.md) — synchronous pipeline setup +- [Testing](testing.md) — test async learning with MagicMock diff --git a/docs/guides/complete-guide.md b/docs/guides/complete-guide.md new file mode 100644 index 0000000000000000000000000000000000000000..9248807d03ee7c1a685493a41bf64455d678a5d4 --- /dev/null +++ b/docs/guides/complete-guide.md @@ -0,0 +1,4 @@ +# Complete ACE Guide + +!!! tip "See also" + The full content for this page will be migrated from [COMPLETE_GUIDE_TO_ACE.md](../old_docs/COMPLETE_GUIDE_TO_ACE.md). diff --git a/docs/guides/composing-pipelines.md b/docs/guides/composing-pipelines.md new file mode 100644 index 0000000000000000000000000000000000000000..12d04398461a2b3ebac3a8e176509c138af52ebc --- /dev/null +++ b/docs/guides/composing-pipelines.md @@ -0,0 +1,283 @@ +# Composing Custom Pipelines + +ACE is built on a composable pipeline engine. Every runner (`ACE`, `BrowserUse`, +`LangChain`, `ClaudeCode`, `TraceAnalyser`) is a thin wrapper around a `Pipeline` +made of steps. You can compose your own pipelines by mixing and matching these +steps — or writing custom ones. + +## Three Levels of ACE + +| Level | Pattern | Control | +|-------|---------|---------| +| **Zero-config** | `ACELiteLLM.from_model("gpt-4o-mini")` | Roles + pipeline auto-created | +| **Role customisation** | `ACE.from_roles(agent=..., reflector=..., ...)` | Custom roles, pipeline auto-composed | +| **Pipeline composition** | `Pipeline([AgentStep(...), ...])` | Full control over step ordering | + +This guide covers **Level 3** — composing pipelines directly. + +## Anatomy of an ACE Pipeline + +Every ACE pipeline is a sequence of steps, each with a `requires`/`provides` +contract that declares what context fields it reads and writes: + +``` +AgentStep ─────> EvaluateStep ─────> ReflectStep ─────> UpdateStep + provides: provides: provides: provides: + agent_output trace reflections skill_manager_output + (also mutates skillbook + via the SM's tools) +``` + +The pipeline validates these contracts at construction time — if a step requires +a field that no earlier step provides, you'll get an error immediately. + +## Composing from Steps + +All pipeline classes and ACE steps are importable from `ace`: + +```python +from ace import ( + # Pipeline engine + Pipeline, Branch, MergeStrategy, StepProtocol, SampleResult, + # ACE context + ACEStepContext, SkillbookView, + # Roles + Agent, Reflector, SkillManager, + # Steps + AgentStep, EvaluateStep, learning_tail, + # Types + Sample, Skillbook, SimpleEnvironment, +) + +skillbook = Skillbook() + +pipe = Pipeline([ + AgentStep(Agent("gpt-4o-mini"), skillbook), + EvaluateStep(SimpleEnvironment()), + *learning_tail(Reflector("gpt-4o-mini"), SkillManager("gpt-4o-mini"), skillbook), +]) +``` + +## Using `learning_tail()` + +The `learning_tail()` helper returns the standard learning step sequence: + +```python +from ace import learning_tail, Reflector, SkillManager, Skillbook + +steps = learning_tail( + Reflector(llm), + SkillManager(llm), + Skillbook(), + dedup_manager=my_dedup_manager, # optional + checkpoint_dir="/tmp/checkpoints", # optional +) +# Returns: [ReflectStep, UpdateStep, +# DeduplicateStep, CheckpointStep] +``` + +Use it when building custom integrations that provide their own execute step but +want the standard learning pipeline. + +## Inspecting Runner Presets with `build_steps()` + +Every runner has a `build_steps()` classmethod that returns the step list it +would use internally. This lets you inspect, modify, and recompose: + +```python +from ace import ACE, Pipeline, ACERunner, Skillbook + +# Get the default steps +steps = ACE.build_steps( + agent=my_agent, + reflector=my_reflector, + skill_manager=my_skill_manager, + environment=my_env, +) + +# Insert a custom step after EvaluateStep +steps.insert(2, MyLoggingStep()) + +# Build your own pipeline and runner +skillbook = Skillbook() +pipe = Pipeline(steps) +runner = ACERunner(pipeline=pipe, skillbook=skillbook) +results = runner.run(samples) +``` + +All runners support `build_steps()`: `ACE`, `BrowserUse`, `ClaudeCode`, +`LangChain`, and `TraceAnalyser`. + +## Writing Custom Steps + +A step is any object satisfying `StepProtocol` — no base class needed: + +```python +from ace import ACEStepContext + +class MyLoggingStep: + requires = frozenset({"agent_output"}) + provides = frozenset() + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + print(f"Agent answered: {ctx.agent_output.final_answer}") + return ctx +``` + +Key rules: + +- `requires`: frozenset of context field names this step reads +- `provides`: frozenset of context field names this step writes +- `__call__`: receives and returns `ACEStepContext` (use `ctx.replace(...)` for updates) +- Steps should be stateless — no internal counters + +## Mixing Integrations + +You can compose steps from different integrations into one pipeline. For example, +combining a browser-use execute step with custom learning: + +```python +from ace import Pipeline, learning_tail, Reflector, SkillManager, Skillbook +from ace.integrations.browser_use import BrowserExecuteStep, BrowserToTrace + +skillbook = Skillbook() +pipe = Pipeline([ + BrowserExecuteStep(browser_llm), + BrowserToTrace(), + MyCustomFilterStep(), # your custom step + *learning_tail(Reflector(llm), SkillManager(llm), skillbook), +]) +``` + +Integration steps live in `ace.integrations` since they have +framework-specific dependencies. + +## Running the Pipeline + +### With a runner + +The simplest way to run a custom pipeline is through `ACERunner`: + +```python +from ace import ACERunner, Sample, Skillbook + +runner = ACERunner(pipeline=pipe, skillbook=skillbook) +results = runner.run( + [Sample(question="What is 2+2?", ground_truth="4")], + epochs=1, +) +``` + +### Directly + +You can also run the pipeline directly by constructing contexts yourself: + +```python +from ace import Pipeline, ACEStepContext, SkillbookView, Sample, Skillbook + +ctx = ACEStepContext( + sample=Sample(question="What is 2+2?", ground_truth="4"), + skillbook=SkillbookView(skillbook), +) + +results = pipe.run([ctx]) +pipe.wait_for_background() # wait for async learning steps +``` + +## Branching (Parallel Steps) + +The pipeline engine supports parallel branches for steps that can run +concurrently: + +```python +from ace import Pipeline, Branch, MergeStrategy + +pipe = Pipeline([ + AgentStep(agent, skillbook), + Branch( + [EvaluateStep(env_a), EvaluateStep(env_b)], + merge=MergeStrategy.LAST, + ), + *learning_tail(reflector, skill_manager, skillbook), +]) +``` + +See the [Pipeline Engine docs](../pipeline/branching.md) for full branching +and merge strategy details. + +## Using RRStep (Recursive Reflector) + +`RRStep` satisfies both `StepProtocol` and `ReflectorLike`, so it can be used +in two ways: + +### As a drop-in reflector replacement + +Pass it anywhere a `Reflector` is expected: + +```python +from ace import ACELiteLLM +from ace.rr import RRStep, RRConfig + +ace = ACELiteLLM.from_model("gpt-4o-mini", reflector=RRStep("gpt-4o-mini", config=RRConfig(max_requests=10))) +``` + +### As a pipeline step + +Place it directly in a pipeline (it provides `reflections`): + +```python +from ace import Pipeline, learning_tail, SkillManager, Skillbook +from ace.rr import RRStep, RRConfig + +skillbook = Skillbook() +rr = RRStep("gpt-4o-mini", config=RRConfig(max_requests=15)) + +pipe = Pipeline([ + MyExecuteStep(), + MyToTrace(), + rr, # replaces ReflectStep — provides "reflections" + *learning_tail(None, SkillManager("gpt-4o-mini"), skillbook)[1:], # skip ReflectStep +]) +``` + +### With recursion enabled + +Allow the RR to decompose large batch inputs via recursive child sessions: + +```python +from ace.rr import RRStep, RRConfig + +rr = RRStep( + "gpt-4o", + config=RRConfig(max_requests=40, max_depth=1), # depth=1 allows one level of recursion +) +``` + +## Available Steps + +All steps are importable from `ace`: + +| Step | Purpose | +|------|---------| +| `AgentStep` | Execute Agent role | +| `EvaluateStep` | Run TaskEnvironment evaluation | +| `ReflectStep` | Run Reflector role (async boundary) | +| `UpdateStep` | Run the agentic SkillManager; its tools mutate the skillbook directly | +| `DeduplicateStep` | Merge near-duplicate skills | +| `CheckpointStep` | Save skillbook to disk | +| `LoadTracesStep` | Load JSONL trace files | +| `ExportSkillbookMarkdownStep` | Export skillbook as markdown | +| `ObservabilityStep` | Generic observability hook | +| `PersistStep` | Persist step output | +| `OpikStep` | Log traces to Opik | +| `RRStep` | Recursive Reflector | + +Integration steps (in `ace.integrations`): + +| Step | Integration | +|------|-------------| +| `BrowserExecuteStep` / `BrowserToTrace` | browser-use | +| `LangChainExecuteStep` / `LangChainToTrace` | LangChain | +| `ClaudeCodeExecuteStep` / `ClaudeCodeToTrace` | Claude Code | +| `ClaudeSDKExecuteStep` / `ClaudeSDKToTrace` | Anthropic Python SDK | +| `OpenClawToTraceStep` | OpenClaw | diff --git a/docs/guides/full-pipeline.md b/docs/guides/full-pipeline.md new file mode 100644 index 0000000000000000000000000000000000000000..2cd125c52763a908322737704c9dd3666091b7b4 --- /dev/null +++ b/docs/guides/full-pipeline.md @@ -0,0 +1,232 @@ +# Full Pipeline Guide + +This guide walks through building a complete ACE pipeline from scratch — choosing components, defining an environment, running training, and saving results. + +## Components + +A full pipeline needs four things: + +1. **LLM Client** — the language model powering all three roles +2. **Three Roles** — Agent, Reflector, SkillManager +3. **Environment** — evaluates agent outputs +4. **Samples** — training data with questions and ground truth + +## Step 1: Create the Roles + +Each role takes a model string directly. Supports any [LiteLLM model](https://docs.litellm.ai/) or PydanticAI-native identifier: + +```python +from ace import Agent, Reflector, SkillManager + +agent = Agent("gpt-4o-mini") +reflector = Reflector("gpt-4o-mini") +skill_manager = SkillManager("gpt-4o-mini") +``` + +Optionally use a cheaper model for learning: + +```python +agent = Agent("gpt-4o") +reflector = Reflector("gpt-4o-mini") +skill_manager = SkillManager("gpt-4o-mini") +``` + +## Step 3: Define an Environment + +The environment evaluates agent outputs. Extend `TaskEnvironment` and implement `evaluate()`: + +```python +from ace import TaskEnvironment, EnvironmentResult + +class MathEnvironment(TaskEnvironment): + def evaluate(self, sample, agent_output): + correct = str(sample.ground_truth).lower() in str(agent_output.final_answer).lower() + return EnvironmentResult( + feedback="Correct!" if correct else f"Incorrect. Expected: {sample.ground_truth}", + ground_truth=sample.ground_truth, + metrics={"accuracy": 1.0 if correct else 0.0}, + ) +``` + +Or use the built-in `SimpleEnvironment` for basic ground-truth matching: + +```python +from ace import SimpleEnvironment + +environment = SimpleEnvironment() +``` + +## Step 4: Prepare Samples + +```python +from ace import Sample + +samples = [ + Sample(question="What is 2+2?", context="", ground_truth="4"), + Sample(question="Capital of France?", context="", ground_truth="Paris"), + Sample(question="Who wrote Hamlet?", context="", ground_truth="Shakespeare"), +] +``` + +## Step 5: Build and Run the Pipeline + +```python +from ace import ACE + +runner = ACE.from_roles( + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + environment=environment, +) + +results = runner.run(samples, epochs=3) +``` + +## Step 6: Save the Skillbook + +```python +runner.save("trained.json") +print(f"Learned {len(runner.skillbook.skills())} strategies") +``` + +## Complete Example + +```python +from ace import ( + ACE, Agent, Reflector, SkillManager, + Sample, SimpleEnvironment, +) + +# Roles (each takes a model string directly) +agent = Agent("gpt-4o-mini") +reflector = Reflector("gpt-4o-mini") +skill_manager = SkillManager("gpt-4o-mini") + +# Pipeline +runner = ACE.from_roles( + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + environment=SimpleEnvironment(), +) + +# Training data +samples = [ + Sample(question="What is 2+2?", context="", ground_truth="4"), + Sample(question="Capital of France?", context="", ground_truth="Paris"), +] + +# Train and save +results = runner.run(samples, epochs=3) +runner.save("trained.json") +``` + +## Checkpoints + +Save the skillbook automatically during long training runs: + +```python +runner = ACE.from_roles( + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + environment=environment, + checkpoint_dir="./checkpoints", + checkpoint_interval=10, # Save every 10 samples +) +``` + +This creates: + +- `ace_checkpoint_10.json`, `ace_checkpoint_20.json`, etc. +- `ace_latest.json` (always the most recent) + +## Deduplication + +Prevent duplicate skills from accumulating (requires `uv add ace-framework[deduplication]`): + +```python +from ace import DeduplicationConfig, DeduplicationManager + +dedup = DeduplicationManager(DeduplicationConfig( + enabled=True, + embedding_model="text-embedding-3-small", + similarity_threshold=0.85, +)) + +runner = ACE.from_roles( + ..., + dedup_manager=dedup, + dedup_interval=10, +) +``` + +## Custom Prompts + +The default prompts are v2.1 and work well out of the box. You can pass your own templates via the `prompt_template` parameter: + +```python +agent = Agent(llm, prompt_template="Your custom agent prompt with {skillbook}, {question}, {context}") +reflector = Reflector(llm, prompt_template="Your custom reflector prompt ...") +skill_manager = SkillManager(llm, prompt_template="Your custom skill manager prompt ...") +``` + +See [Prompt Engineering](prompts.md) for template variables and more examples. + +## Testing Without API Calls + +Use `test` as the model to get PydanticAI's built-in test model, or use `unittest.mock` to patch the agent's `run_sync` method: + +```python +agent = Agent("test") +reflector = Reflector("test") +skill_manager = SkillManager("test") +``` + +## Observability + +Add Opik tracing to any pipeline via `extra_steps` (requires `uv add ace-framework[observability]`): + +```python +from ace import ACE, OpikStep, register_opik_litellm_callback + +runner = ACE.from_roles( + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + environment=environment, + extra_steps=[OpikStep(project_name="my-experiment")], +) + +# Optionally add per-LLM-call cost tracking +register_opik_litellm_callback(project_name="my-experiment") +``` + +See [Opik Observability](../integrations/opik.md) for full details. + +## Going Deeper: Manual Pipeline Composition + +The `ACE.from_roles()` runner composes a `Pipeline` internally. You can build the +same pipeline yourself for full control over step ordering, branching, and +custom steps: + +```python +from ace import Pipeline, AgentStep, EvaluateStep, learning_tail + +pipe = Pipeline([ + AgentStep(agent, skillbook), + EvaluateStep(environment), + *learning_tail(reflector, skill_manager, skillbook), +]) +``` + +See [Composing Pipelines](composing-pipelines.md) for the complete guide. + +## What to Read Next + +- [Composing Pipelines](composing-pipelines.md) — compose custom pipelines from steps +- [Async Learning](async-learning.md) — parallel Reflector execution +- [Prompt Engineering](prompts.md) — customize prompt templates +- [Integration Pattern](integration.md) — wrap existing agents instead +- [Opik Observability](../integrations/opik.md) — monitor costs and traces diff --git a/docs/guides/integration.md b/docs/guides/integration.md new file mode 100644 index 0000000000000000000000000000000000000000..0d69c18947f135fc4e4e524e9a0087b7d04e6701 --- /dev/null +++ b/docs/guides/integration.md @@ -0,0 +1,172 @@ +# Integration Pattern + +Use the integration pattern when you have an **existing agent** (browser-use, +LangChain, Claude Code, the Anthropic SDK, or a custom framework) and want to +add ACE learning on top. + +!!! note "Full Pipeline vs Integration" + The [Full Pipeline](full-pipeline.md) uses all three ACE roles. The integration pattern skips the ACE Agent — your external agent handles execution, and ACE only learns from the results. + +## Three Steps + +Every integration follows the same pattern: + +``` +1. INJECT — Add skillbook strategies to the agent's context +2. EXECUTE — Run the external agent normally +3. LEARN — Reflector + SkillManager update the skillbook +``` + +## Using Built-In Runners + +ACE provides runners for popular frameworks. Each uses `from_model()` for quick setup or `from_roles()` for full control: + +=== "Browser-Use" + + ```python + from ace import BrowserUse + from langchain_openai import ChatOpenAI + + runner = BrowserUse.from_model( + browser_llm=ChatOpenAI(model="gpt-4o"), + ace_model="gpt-4o-mini", + ) + results = runner.run(["Find top HN post", "Check weather in NYC"]) + runner.save("browser_expert.json") + ``` + +=== "LangChain" + + ```python + from ace import LangChain + + runner = LangChain.from_model(your_chain, ace_model="gpt-4o-mini") + results = runner.run([{"input": "Summarize this document"}]) + runner.save("chain_expert.json") + ``` + +=== "Claude Code" + + ```python + from ace import ClaudeCode + + runner = ClaudeCode.from_model(working_dir="./my_project") + results = runner.run(["Add tests for utils.py", "Fix the login bug"]) + runner.save("code_expert.json") + ``` + +## Direct SDK Steps + +The Anthropic SDK integration is step-based rather than runner-based. Use it +when you want direct Messages API access, tool use, validated result models, +and Logfire observability inside your own pipeline: + +```python +from ace import Pipeline, Reflector, SkillManager, Skillbook, learning_tail +from ace.integrations import ClaudeSDKExecuteStep, ClaudeSDKToTrace + +skillbook = Skillbook() +pipe = Pipeline([ + ClaudeSDKExecuteStep(model="claude-sonnet-4-20250514"), + ClaudeSDKToTrace(), + *learning_tail(Reflector("gpt-4o-mini"), SkillManager("gpt-4o-mini"), skillbook), +]) +``` + +## Construction Patterns + +All integration runners offer two construction paths: + +### from_model() — Quick Setup + +Builds ACE roles automatically from a model string: + +```python +runner = BrowserUse.from_model( + browser_llm=ChatOpenAI(model="gpt-4o"), + ace_model="gpt-4o-mini", # Model for Reflector + SkillManager + skillbook_path="saved.json", # Optional: resume from saved skillbook +) +``` + +### from_roles() — Full Control + +Provide pre-built role instances: + +```python +from ace import Reflector, SkillManager + +runner = BrowserUse.from_roles( + browser_llm=ChatOpenAI(model="gpt-4o"), + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), + skillbook_path="saved.json", + dedup_config=my_dedup_config, + checkpoint_dir="./checkpoints", +) +``` + +## Common Options + +All integration runners share these parameters: + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `skillbook` | Existing `Skillbook` instance | `None` (creates empty) | +| `skillbook_path` | Path to load skillbook from | `None` | +| `dedup_config` | Deduplication configuration | `None` | +| `dedup_interval` | Samples between dedup runs | `10` | +| `checkpoint_dir` | Directory for checkpoint files | `None` | +| `checkpoint_interval` | Samples between checkpoints | `10` | + +## Lifecycle Methods + +All runners expose: + +```python +runner.save("path.json") # Save skillbook +runner.wait_for_background() # Wait for async learning +runner.learning_stats # Background progress dict +runner.skillbook # Current Skillbook instance +runner.get_strategies() # Formatted strategies string +``` + +## Building a Custom Integration + +For frameworks not covered by the built-in runners, you can compose a custom pipeline using steps. + +The pattern: **Execute Step** (runs your agent) + **ToTrace Step** (extracts learning signal) + **learning_tail()** (standard learning pipeline). + +```python +from pipeline import Pipeline +from ace import Skillbook, Reflector, SkillManager +from ace.steps import learning_tail +from ace.runners import ACERunner + +# Your custom execute step would implement the Step protocol +# See the Pipeline Engine docs for details on building custom steps + +skillbook = Skillbook() + +steps = [ + MyCustomExecuteStep(...), + MyCustomToTraceStep(), + *learning_tail( + Reflector("gpt-4o-mini"), + SkillManager("gpt-4o-mini"), + skillbook, + ), +] + +runner = ACERunner(pipeline=Pipeline(steps), skillbook=skillbook) +``` + +See [Pipeline Engine: Building Custom Steps](../pipeline/custom-steps.md) for the Step protocol. + +## What to Read Next + +- [LiteLLM Integration](../integrations/litellm.md) — simplest self-improving agent +- [Browser-Use Integration](../integrations/browser-use.md) — browser automation details +- [LangChain Integration](../integrations/langchain.md) — chain/agent wrapping +- [Claude Code Integration](../integrations/claude-code.md) — coding tasks +- [Claude SDK Integration](../integrations/claude-sdk.md) — direct Anthropic API steps diff --git a/docs/guides/prompts.md b/docs/guides/prompts.md new file mode 100644 index 0000000000000000000000000000000000000000..07040f5f54ddbc12dbe711326cbcf93855b47ae4 --- /dev/null +++ b/docs/guides/prompts.md @@ -0,0 +1,95 @@ +# Prompt Engineering + +ACE uses specialized prompt templates for each role. The framework includes multiple prompt versions with different trade-offs. + +## Default Prompts + +`ace` ships with v2.1 prompts built in. All three roles (`Agent`, `Reflector`, `SkillManager`) use them by default — no extra imports needed. + +!!! tip "Recommendation" + The built-in v2.1 prompts work well out of the box. Only provide custom prompts when you need domain-specific instructions. + +## Overriding Prompts + +Pass a `prompt_template` string to any role constructor: + +```python +from ace import Agent, Reflector, SkillManager + +agent = Agent("gpt-4o-mini", prompt_template="Your custom agent prompt ...") +reflector = Reflector("gpt-4o-mini", prompt_template="Your custom reflector prompt ...") +skill_manager = SkillManager("gpt-4o-mini", prompt_template="Your custom skill manager prompt ...") +``` + +## Template Variables + +### Agent Prompt + +| Variable | Description | +|----------|-------------| +| `{skillbook}` | Current skillbook in markdown format | +| `{question}` | The input question | +| `{context}` | Additional context | +| `{reflection}` | Optional reflection from a previous attempt | + +### Reflector Prompt + +| Variable | Description | +|----------|-------------| +| `{skillbook}` | Current skillbook in markdown format | +| `{question}` | The original question | +| `{agent_output}` | The agent's response | +| `{ground_truth}` | Expected answer | +| `{feedback}` | Environment feedback | + +### SkillManager Prompt + +| Variable | Description | +|----------|-------------| +| `{skillbook}` | Current skillbook in markdown format | +| `{reflection}` | Reflector's analysis | +| `{question_context}` | Description of the task domain | +| `{progress}` | Current training progress | + +## Custom Prompts + +You can provide your own prompt templates. They must include the required template variables: + +```python +custom_agent_prompt = """ +Skillbook: {skillbook} +Question: {question} +Context: {context} + +Generate a JSON response with: +- reasoning: Your step-by-step thought process +- skill_ids: List of skillbook IDs you used +- final_answer: Your answer +""" + +agent = Agent(llm, prompt_template=custom_agent_prompt) +``` + +## Formatting Skillbook for External Agents + +Integration runners inject the skillbook into external agent prompts using a wrapper function: + +```python +from ace import wrap_skillbook_context + +context = wrap_skillbook_context(skillbook) +# Returns formatted strategies with usage instructions +``` + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| JSON parse failures | Increase `max_tokens`, use Instructor, or try v2.1 prompts | +| Empty skill_ids | Agent not citing skills — check skillbook has content | +| Poor answer quality | Switch to v2.1 prompts or try a larger model | + +## What to Read Next + +- [Full Pipeline Guide](full-pipeline.md) — use prompts in a complete pipeline +- [The Skillbook](../concepts/skillbook.md) — what goes into `{skillbook}` diff --git a/docs/guides/testing.md b/docs/guides/testing.md new file mode 100644 index 0000000000000000000000000000000000000000..65ddfaf72e2e0d7a37e06f1cc219f8df3628dc16 --- /dev/null +++ b/docs/guides/testing.md @@ -0,0 +1,242 @@ +# Testing + +## Running Tests + +=== "pytest (recommended)" + + ```bash + uv run pytest # All tests + uv run pytest -m unit # Unit tests only + uv run pytest -m integration # Integration tests only + uv run pytest tests/test_skillbook.py # Specific file + uv run pytest -v # Verbose output + ``` + +=== "unittest" + + ```bash + python -m unittest discover -s tests + python -m unittest discover -s tests -v # Verbose + ``` + +## Testing Without API Calls + +Use a mock LLM to test pipeline wiring without making real API calls. Any object with `complete()` and `complete_structured()` methods satisfies the `LLMClientLike` protocol: + +```python +from unittest.mock import MagicMock +from ace import Agent, Reflector, SkillManager + +mock_llm = MagicMock() +mock_llm.complete.return_value = '{"reasoning": "test", "final_answer": "4", "skill_ids": []}' + +agent = Agent(mock_llm) +reflector = Reflector(mock_llm) +skill_manager = SkillManager(mock_llm) +``` + +## Unit Testing + +### Testing the Skillbook + +```python +from ace import Skillbook + +def test_add_skill(): + skillbook = Skillbook() + skill = skillbook.add_skill( + section="Test", + content="Test strategy", + metadata={"helpful": 0, "harmful": 0, "neutral": 0}, + ) + assert len(skillbook.skills()) == 1 + assert skill.content == "Test strategy" + +def test_save_load(tmp_path): + skillbook = Skillbook() + skillbook.add_skill(section="Test", content="Strategy") + + path = str(tmp_path / "test.json") + skillbook.save_to_file(path) + + loaded = Skillbook.load_from_file(path) + assert len(loaded.skills()) == 1 +``` + +### Testing the Agent + +```python +from unittest.mock import MagicMock +from ace import Agent, Skillbook + +def test_agent_generate(): + mock_llm = MagicMock() + mock_llm.complete.return_value = '{"reasoning": "2+2=4", "final_answer": "4", "skill_ids": []}' + + agent = Agent(mock_llm) + output = agent.generate( + question="What is 2+2?", + context="", + skillbook=Skillbook(), + ) + assert output.final_answer is not None + assert output.reasoning is not None +``` + +### Testing Reflector and SkillManager + +```python +from unittest.mock import MagicMock +from ace import Agent, Reflector, SkillManager, Skillbook + +def make_mock_llm(): + mock = MagicMock() + mock.complete.return_value = '{"reasoning": "test", "final_answer": "4", "skill_ids": []}' + return mock + +def test_reflector(): + mock_llm = make_mock_llm() + reflector = Reflector(mock_llm) + agent = Agent(mock_llm) + + output = agent.generate(question="Test", context="", skillbook=Skillbook()) + reflection = reflector.reflect( + question="Test", + agent_output=output, + skillbook=Skillbook(), + ground_truth="expected", + feedback="Correct", + ) + assert reflection.key_insight is not None + +def test_skill_manager(): + sm = SkillManager(make_mock_llm()) + # ... similar pattern with reflection input +``` + +## Integration Testing + +### End-to-End Learning Cycle + +```python +from unittest.mock import MagicMock +from ace import ( + ACE, Agent, Reflector, SkillManager, + Sample, SimpleEnvironment, +) + +def test_full_learning_cycle(): + mock_llm = MagicMock() + mock_llm.complete.return_value = '{"reasoning": "test", "final_answer": "answer", "skill_ids": []}' + + runner = ACE.from_roles( + agent=Agent(mock_llm), + reflector=Reflector(mock_llm), + skill_manager=SkillManager(mock_llm), + environment=SimpleEnvironment(), + ) + + samples = [Sample(question="Test", context="", ground_truth="answer")] + results = runner.run(samples, epochs=1) + + assert len(results) == 1 +``` + +### Testing Checkpoints + +```python +def test_checkpoints(tmp_path): + mock_llm = MagicMock() + mock_llm.complete.return_value = '{"reasoning": "test", "final_answer": "A", "skill_ids": []}' + + runner = ACE.from_roles( + agent=Agent(mock_llm), + reflector=Reflector(mock_llm), + skill_manager=SkillManager(mock_llm), + environment=SimpleEnvironment(), + checkpoint_dir=str(tmp_path), + checkpoint_interval=1, + ) + + samples = [Sample(question="Q", context="", ground_truth="A")] + runner.run(samples, epochs=1) + + # Check that checkpoint files were created + checkpoints = list(tmp_path.glob("ace_*.json")) + assert len(checkpoints) > 0 +``` + +## Common Test Patterns + +### Fixtures + +```python +import pytest +from unittest.mock import MagicMock +from ace import Agent, Reflector, SkillManager, Skillbook + +@pytest.fixture +def mock_llm(): + mock = MagicMock() + mock.complete.return_value = '{"reasoning": "test", "final_answer": "4", "skill_ids": []}' + return mock + +@pytest.fixture +def skillbook(): + return Skillbook() + +@pytest.fixture +def agent(mock_llm): + return Agent(mock_llm) +``` + +### Mocking LLM Responses + +```python +from unittest.mock import MagicMock + +def test_with_mock(): + mock_llm = MagicMock() + mock_llm.complete.return_value = '{"reasoning": "...", "final_answer": "4", "skill_ids": []}' + + agent = Agent(mock_llm) + # ... +``` + +## CI Configuration + +```yaml +# .github/workflows/test.yml +name: Tests +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + - run: uv sync + - run: uv run pytest -v +``` + +## Code Quality + +```bash +uv run black ace/ tests/ examples/ # Format +uv run mypy ace/ # Type check +uv run pre-commit run --all-files # All hooks +``` + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| Import errors | Run `uv sync` to install all dependencies | +| API key errors in tests | Use `MagicMock` for unit tests (see above) | +| Flaky async tests | Increase timeout or use `wait_for_background()` | +| Coverage too low | `--cov-fail-under=25` is the threshold | + +## What to Read Next + +- [Full Pipeline Guide](full-pipeline.md) — what you're testing +- [Async Learning](async-learning.md) — testing async pipelines diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000000000000000000000000000000000..572292cfd240f6a63395dcd5ff18e7bfd20075e7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,103 @@ +# ACE Framework + +**Agentic Context Engineering** — a framework for self-improving language model agents. + +ACE enables AI agents to learn from their own execution feedback through three collaborative roles: **Agent**, **Reflector**, and **SkillManager**. Learned strategies accumulate in a **Skillbook** that makes every subsequent call smarter. + +## The Learning Loop + +```mermaid +graph LR + S[Sample] --> A[Agent] + A --> E[Environment] + E -->|feedback| R[Reflector] + R -->|analyzes| SM[SkillManager] + SM -->|updates| SK[Skillbook] + SK -.->|context| A +``` + +Each pass through the loop discovers new strategies, reinforces what works, and prunes what doesn't. + +## Get Started in 30 Seconds + +```python +from ace import ACELiteLLM + +agent = ACELiteLLM.from_model("gpt-4o-mini") + +answer = agent.ask("If all cats are animals, is Felix (a cat) an animal?") + +agent.save("learned.json") +``` + +## Install + +```bash +uv add ace-framework +``` + +## Quick Links + +<div class="grid cards" markdown> + +- **Getting Started** + + --- + + Install the framework and run your first self-improving agent. + + [:octicons-arrow-right-24: Installation](getting-started/installation.md) + [:octicons-arrow-right-24: Quick Start](getting-started/quick-start.md) + +- **Concepts** + + --- + + Understand the Skillbook, Roles, Insight Levels, and Update Operations. + + [:octicons-arrow-right-24: Overview](concepts/overview.md) + +- **Guides** + + --- + + Build full pipelines, integrate with existing agents, tune prompts. + + [:octicons-arrow-right-24: Full Pipeline](guides/full-pipeline.md) + [:octicons-arrow-right-24: Integration Guide](guides/integration.md) + +- **Integrations** + + --- + + Ready-made runners for LiteLLM, LangChain, browser-use, and Claude Code. + + [:octicons-arrow-right-24: Integrations Overview](integrations/index.md) + +- **Pipeline Composition** + + --- + + Compose custom pipelines by mixing and matching steps for full control. + + [:octicons-arrow-right-24: Composing Pipelines](guides/composing-pipelines.md) + [:octicons-arrow-right-24: Pipeline Engine](pipeline/index.md) + +</div> + +## Available Runners + +| Runner | Framework | Use Case | +|--------|-----------|----------| +| [`ACELiteLLM`](integrations/litellm.md) | LiteLLM (100+ providers) | Simple self-improving agent | +| [`LangChain`](integrations/langchain.md) | LangChain Runnables | Wrap chains/agents with learning | +| [`BrowserUse`](integrations/browser-use.md) | browser-use | Browser automation with learning | +| [`ClaudeCode`](integrations/claude-code.md) | Claude Code CLI | Coding tasks with learning | +| [`ACE`](guides/full-pipeline.md) | Full pipeline | Agent + Reflector + SkillManager | + +## Paper + +This framework implements the method from: + +> *Agentic Context Engineering: Evolving Contexts for Self-Improving Language Models* +> [arXiv:2510.04618](https://arxiv.org/abs/2510.04618) diff --git a/docs/integrations/browser-use.md b/docs/integrations/browser-use.md new file mode 100644 index 0000000000000000000000000000000000000000..892ed3b995e8aa00701cef8e072e012e8366eb09 --- /dev/null +++ b/docs/integrations/browser-use.md @@ -0,0 +1,119 @@ +# Browser-Use Integration + +The `BrowserUse` runner wraps [browser-use](https://github.com/browser-use/browser-use) with ACE learning. The agent automates browser tasks and learns strategies from each run — improving navigation, element selection, and error recovery over time. + +## Installation + +```bash +uv add ace-framework[browser-use] +``` + +## Quick Start + +```python +from ace import BrowserUse +from langchain_openai import ChatOpenAI + +runner = BrowserUse.from_model( + browser_llm=ChatOpenAI(model="gpt-4o"), + ace_model="gpt-4o-mini", +) + +results = runner.run("Find the top post on Hacker News") +runner.save("browser_expert.json") +``` + +## Parameters + +### from_model() + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `browser_llm` | `Any` | — | LLM for browser-use execution | +| `ace_model` | `str` | `"gpt-4o-mini"` | Model for Reflector + SkillManager | +| `ace_max_tokens` | `int` | `2048` | Max tokens for ACE LLM responses | +| `ace_temperature` | `float` | `0.0` | Sampling temperature for ACE roles | + +### from_roles() + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `browser_llm` | `Any` | — | LLM for browser-use execution | +| `reflector` | `ReflectorLike` | — | Reflector instance | +| `skill_manager` | `SkillManagerLike` | — | SkillManager instance | +| `skillbook_path` | `str` | `None` | Load saved skillbook | +| `browser` | `Browser` | `None` | browser-use Browser instance | +| `agent_kwargs` | `dict` | `None` | Extra kwargs for browser-use Agent | +| `dedup_config` | `DeduplicationConfig` | `None` | Deduplication config | +| `checkpoint_dir` | `str` | `None` | Checkpoint directory | + +## Methods + +```python +results = runner.run(tasks, epochs=1) # Run with learning +runner.save("path.json") # Save skillbook +runner.wait_for_background() # Wait for async learning +runner.get_strategies() # View learned strategies +``` + +## How It Works + +1. **INJECT** — Skillbook strategies are added to the task prompt +2. **EXECUTE** — browser-use runs the task (navigation, clicks, form fills) +3. **Extract trace** — ACE extracts a chronological trace of agent thoughts, actions, and results +4. **LEARN** — Reflector analyzes the full trace, SkillManager updates the skillbook + +The extracted trace includes: + +- Agent reasoning at each step +- Browser actions taken (click, type, navigate) +- Page observations +- Success/failure of each action + +## Running Multiple Tasks + +```python +results = runner.run([ + "Find the top post on Hacker News", + "Search for ACE framework on GitHub", + "Check the weather in NYC", +]) +``` + +## Example: Domain Checker + +```python +from ace import BrowserUse +from langchain_openai import ChatOpenAI + +runner = BrowserUse.from_model( + browser_llm=ChatOpenAI(model="gpt-4o"), + ace_model="gpt-4o-mini", +) + +domains = ["example.com", "test.org", "sample.net"] +for domain in domains: + runner.run(f"Check if {domain} is available for registration") + +# After several runs, the agent learns: +# - Which registrar sites to use +# - How to navigate the domain search UI +# - How to interpret availability results +runner.save("domain_checker.json") +``` + +## Resuming from a Saved Skillbook + +```python +runner = BrowserUse.from_model( + browser_llm=ChatOpenAI(model="gpt-4o"), + ace_model="gpt-4o-mini", + skillbook_path="browser_expert.json", +) +``` + +## What to Read Next + +- [Integration Pattern](../guides/integration.md) — how the INJECT/EXECUTE/LEARN pattern works +- [The Skillbook](../concepts/skillbook.md) — how learned strategies are stored +- [Opik Observability](opik.md) — monitor browser automation costs diff --git a/docs/integrations/claude-code.md b/docs/integrations/claude-code.md new file mode 100644 index 0000000000000000000000000000000000000000..c76f035eee38f643503ea41148e8498eb99e586b --- /dev/null +++ b/docs/integrations/claude-code.md @@ -0,0 +1,126 @@ +# Claude Code Integration + +The `ClaudeCode` runner wraps the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) with ACE learning. The agent runs coding tasks in your project directory and learns strategies from each execution — improving code generation, debugging, and project-specific patterns over time. + +## Quick Start + +```python +from ace import ClaudeCode + +runner = ClaudeCode.from_model(working_dir="./my_project") + +results = runner.run("Add unit tests for utils.py") +runner.save("coding_expert.json") +``` + +## Installation + +```bash +uv add 'ace-framework[claude-code]' +``` + +## Prerequisites + +- Claude Code CLI installed and authenticated +- A project directory with source code + +## Parameters + +### from_model() + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `working_dir` | `str` | `None` | Path to the project directory | +| `ace_model` | `str` | `"gpt-4o-mini"` | Model for Reflector + SkillManager | +| `ace_max_tokens` | `int` | `2048` | Max tokens for ACE LLM responses | +| `ace_temperature` | `float` | `0.0` | Sampling temperature for ACE roles | + +### from_roles() + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `reflector` | `ReflectorLike` | — | Reflector instance | +| `skill_manager` | `SkillManagerLike` | — | SkillManager instance | +| `working_dir` | `str` | `None` | Project directory | +| `timeout` | `int` | `600` | Execution timeout (seconds) | +| `model` | `str` | `None` | Claude model override | +| `allowed_tools` | `list[str]` | `None` | Allowed Claude Code tools | +| `skillbook_path` | `str` | `None` | Load saved skillbook | +| `dedup_config` | `DeduplicationConfig` | `None` | Deduplication config | +| `checkpoint_dir` | `str` | `None` | Checkpoint directory | + +## Methods + +```python +results = runner.run(tasks, epochs=1) # Run with learning +runner.save("path.json") # Save skillbook +runner.wait_for_background() # Wait for async learning +runner.get_strategies() # View learned strategies +``` + +## How It Works + +1. **INJECT** — Skillbook strategies are appended to the task prompt passed to Claude Code for that run +2. **EXECUTE** — Claude Code CLI runs the task in the project directory +3. **Extract trace** — ACE parses Claude Code's `--output-format=stream-json` transcript into a learning trace +4. **LEARN** — Reflector analyzes the trace, SkillManager updates the skillbook + +The stock `ClaudeCode` runner does **not** wire `PersistStep`, so it does not +update `CLAUDE.md` automatically. Persist learned strategies with +`runner.save(...)`, or compose a custom pipeline that adds `PersistStep` if you +want file-based prompt injection outside the runner. + +## Pipeline Skill + +The **kayba-pipeline** skill gives Claude Code a 7-stage evaluation and improvement pipeline that can be triggered directly from the chat. Install it with: + +```bash +kayba setup +``` + +This copies the skill into `.claude/skills/kayba-pipeline/`. The pipeline stages are: + +1. **Analyze traces** — extract patterns from agent execution transcripts +2. **Compute metrics** — score traces against quality dimensions +3. **Build rubric** — generate a structured evaluation rubric +4. **Plan fixes** — propose concrete improvements +5. **HITL review** — optional human-in-the-loop approval gate +6. **Apply fixes** — execute the approved changes +7. **Verify** — confirm fixes pass the rubric + +Trigger the pipeline by saying **"run the pipeline"** or **"kayba pipeline"** in Claude Code with a traces folder in your project. + +To skip skill installation: `kayba setup --no-skills`. See [Hosted API](hosted-api.md#agent-setup) for all `kayba setup` options. + +## How It Works (continued) + +The agent learns project-specific patterns like: + +- Code style and conventions +- Common debugging approaches +- Test patterns and frameworks used +- Module structure and dependencies + +## Running Multiple Tasks + +```python +results = runner.run([ + "Add unit tests for utils.py", + "Fix the bug in the login handler", + "Refactor the database module to use connection pooling", +]) +``` + +## Resuming from a Saved Skillbook + +```python +runner = ClaudeCode.from_model( + working_dir="./my_project", + skillbook_path="coding_expert.json", +) +``` + +## What to Read Next + +- [Integration Pattern](../guides/integration.md) — how the INJECT/EXECUTE/LEARN pattern works +- [The Skillbook](../concepts/skillbook.md) — how learned strategies are stored diff --git a/docs/integrations/claude-sdk.md b/docs/integrations/claude-sdk.md new file mode 100644 index 0000000000000000000000000000000000000000..afb6c2faaca7a393b0208728f6828ccdbab548be --- /dev/null +++ b/docs/integrations/claude-sdk.md @@ -0,0 +1,121 @@ +# Claude SDK Integration + +The Claude SDK integration provides composable ACE steps for the +[Anthropic Python SDK](https://docs.anthropic.com/en/api/client-sdks). Use it +when you want direct Messages API access inside your own pipeline instead of a +prebuilt runner. + +## Quick Start + +```python +from ace import Pipeline, Reflector, SkillManager, Skillbook, learning_tail +from ace.integrations import ClaudeSDKExecuteStep, ClaudeSDKToTrace + +skillbook = Skillbook() +pipe = Pipeline([ + ClaudeSDKExecuteStep(model="claude-sonnet-4-20250514"), + ClaudeSDKToTrace(), + *learning_tail(Reflector("gpt-4o-mini"), SkillManager("gpt-4o-mini"), skillbook), +]) +``` + +## Installation + +```bash +uv add ace-framework[claude-sdk] +``` + +For observability, also install and configure Logfire: + +```bash +uv add ace-framework[logfire] +``` + +```python +from ace.observability import configure_logfire + +configure_logfire() +``` + +## What It Provides + +- `ClaudeSDKExecuteStep` — injects skillbook context, calls the Anthropic + Messages API, and writes a validated `ClaudeSDKResult` to `ctx.trace` +- `ClaudeSDKToTrace` — converts `ClaudeSDKResult` into the standard ACE trace + dict consumed by `ReflectStep` +- `ClaudeSDKResult` — Pydantic model with validated output, token usage, + latency, tool calls, and raw response access +- `ToolCall` — Pydantic model for captured Claude tool invocations + +## Parameters + +### ClaudeSDKExecuteStep + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `model` | `str` | `"claude-sonnet-4-20250514"` | Claude model ID | +| `system_prompt` | `str \| None` | `None` | Base system prompt | +| `max_tokens` | `int` | `4096` | Maximum output tokens | +| `temperature` | `float` | `0.0` | Sampling temperature | +| `tools` | `list[dict] \| None` | `None` | Anthropic tool definitions | +| `api_key` | `str \| None` | `None` | Optional API key override | +| `base_url` | `str \| None` | `None` | Optional API base URL | +| `inject_skillbook` | `bool` | `True` | Prepend skillbook context to the system prompt | +| `client` | `Any` | `None` | Injected Anthropic client for testing or custom transport | + +## Observability + +When Logfire is configured, the step emits three layers of observability: + +1. Step-level `logfire.span(...)` around `ClaudeSDKExecuteStep` +2. Structured `logfire.info(...)` and `logfire.error(...)` events with tokens, + latency, stop reason, and tool counts +3. `logfire.instrument_anthropic(client)` auto-instrumentation for the + underlying SDK calls + +The result model also captures: + +- `input_tokens` +- `output_tokens` +- `total_tokens` +- `latency_seconds` +- `stop_reason` +- `tool_calls` + +## Tool Use + +```python +tools = [ + { + "name": "get_weather", + "description": "Get the current weather for a city.", + "input_schema": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + }, + } +] + +execute = ClaudeSDKExecuteStep( + model="claude-sonnet-4-20250514", + tools=tools, +) +``` + +If Claude returns tool use blocks, they are captured on +`ClaudeSDKResult.tool_calls` as validated `ToolCall` models. + +## Validation + +`ClaudeSDKExecuteStep` validates its configuration with Pydantic before the +client is constructed. `ClaudeSDKResult` and `ToolCall` are also Pydantic +models, so invalid token counts, latency values, or malformed tool calls are +rejected early. + +## What to Read Next + +- [Integration Pattern](../guides/integration.md) — the shared + INJECT/EXECUTE/LEARN design +- [Composing Pipelines](../guides/composing-pipelines.md) — mix SDK steps with + other ACE steps diff --git a/docs/integrations/hosted-api.md b/docs/integrations/hosted-api.md new file mode 100644 index 0000000000000000000000000000000000000000..48157e47e0185d5e162ad63b7467098c0f82901e --- /dev/null +++ b/docs/integrations/hosted-api.md @@ -0,0 +1,453 @@ +# Kayba Hosted API + +The Kayba hosted API lets you upload traces, generate insights, and pull optimised prompts without running ACE roles locally. The `kayba` CLI wraps every API endpoint. + +## Prerequisites + +1. A Kayba API key (set `KAYBA_API_KEY` or pass `--api-key` to every command). +2. An Anthropic API key (set `ANTHROPIC_API_KEY`) — used server-side for LLM calls when generating insights. +3. Install the `cloud` extra: + +```bash +uv tool install 'ace-framework[cloud]' --python 3.12 +``` + +Quote the extra in `zsh`/`bash` so `[cloud]` is not treated as a shell glob. + +Or if you installed from source: + +```bash +uv sync +``` + +## Authentication + +Every command reads `KAYBA_API_KEY` from the environment. You can also pass it explicitly: + +```bash +export KAYBA_API_KEY=your-key-here +kayba traces list +``` + +The default API endpoint is `https://use.kayba.ai/api`. Override it with `KAYBA_API_URL` or `--base-url`. + +## Where do traces come from? + +Kayba does **not** auto-ingest local transcripts from Claude Code, Codex, Cursor, +or other coding agents. The hosted API and web UI only show traces that you +explicitly upload. + +- **Claude Code:** session transcripts are typically written under + `~/.claude/projects/<slug>/*.jsonl` +- **Codex:** local session logs are discoverable under + `~/.codex/sessions/YYYY/MM/DD/*.jsonl` +- **Cursor:** no auto-ingest; locate or export the transcript files from your + setup first, then upload them manually + +Copy-pasteable examples: + +```bash +kayba traces upload ~/.claude/projects/<slug>/ +kayba traces upload ~/.codex/sessions/2026/04/10/ +``` + +You can point `kayba traces upload` at a single file, a glob-expanded file list, +or a directory. Directories are walked recursively, so uploading the project or +day folder is usually the simplest option. + +## CLI Reference + +### Trace management + +```bash +# List uploaded traces +kayba traces list +kayba traces list --json # machine-parseable output + +# View a trace +kayba traces show TRACE_ID +kayba traces show TRACE_ID --meta # metadata only, no content +kayba traces show TRACE_ID --json + +# Upload traces +kayba traces upload trace.md +kayba traces upload session.jsonl # common for Claude Code / OpenClaw +kayba traces upload traces/ # directory (recursive) +kayba traces upload --type json traces/ # force file type +cat trace.md | kayba upload - # pipe from stdin (top-level alias) + +# Delete traces +kayba traces delete ID1 ID2 +kayba traces delete ID1 --force # skip confirmation +``` + +Files larger than 350k characters are rejected by the API. The CLI skips them +locally and tells you to split or trim the trace first. Supported types are +auto-detected from the extension: `.md`/`.markdown` → `md`, +`.json`/`.jsonl` → `json`, everything else → `txt`. + +### Run the pipeline + +The `run` command combines trace selection and pipeline execution: + +```bash +# Interactive mode (visual checkbox selector) +kayba run + +# Select all traces +kayba run --all --wait + +# Explicit trace IDs +kayba run --traces ID1 --traces ID2 + +# Custom model, epochs, and reflector mode +kayba run --all --model claude-opus-4-6 --epochs 3 --reflector-mode recursive --wait + +# Machine-parseable output (for agents/scripts) +kayba run --all --json +``` + +In interactive mode (`kayba run` with no flags), a visual checkbox selector lets you pick traces with arrow keys, space to toggle, and enter to confirm. Requires the `questionary` package (included in the `cloud` extra). + +In programmatic mode (`--traces`, `--all`, or `--json`), no prompts are shown — suitable for agents and scripts. + +Options: + +| Flag | Description | +|------|-------------| +| `--traces ID` | Trace IDs to analyse (repeatable) | +| `--all` | Select all uploaded traces | +| `--model` | `claude-sonnet-4-6` or `claude-opus-4-6` | +| `--epochs N` | Number of analysis epochs | +| `--reflector-mode` | `recursive` or `standard` | +| `--anthropic-key` | Anthropic API key for server-side LLM calls | +| `--wait` | Poll until the job completes | +| `--json` | Machine-parseable JSON output | + +### Generate insights + +```bash +# From all uploaded traces +kayba insights generate --wait + +# Specific traces +kayba insights generate --traces ID1 --traces ID2 + +# Custom model and epochs +kayba insights generate --model claude-opus-4-6 --epochs 3 --wait +``` + +### List and triage insights + +```bash +# List all +kayba insights list + +# Filter by status +kayba insights list --status pending + +# JSON output +kayba insights list --json + +# Accept specific insights +kayba insights triage --accept ID1 --accept ID2 + +# Accept all pending +kayba insights triage --accept-all + +# Reject with a note +kayba insights triage --reject ID1 --note "Too vague" +``` + +### Generate and pull prompts + +```bash +# Generate a prompt from accepted insights +kayba prompts generate + +# Generate with a label and save to file +kayba prompts generate --label "v2-coding" -o prompt.md + +# List prompt versions +kayba prompts list + +# Pull latest prompt +kayba prompts pull + +# Pull specific version +kayba prompts pull --id PROMPT_ID -o skillbook-prompt.md + +# Pretty-print full JSON +kayba prompts pull --pretty + +# Install the latest prompt into Claude Code's instruction file +kayba prompts install --target claude-code + +# Install a local prompt export into AGENTS.md +kayba prompts install --input prompt.md --target universal +``` + +To install the generated prompt into `CLAUDE.md`, `AGENTS.md`, or +`.cursorrules` without duplicating prior runs, see +[Using your generated prompt](#using-your-generated-prompt). + +### Integrations + +Manage connections to external trace platforms (MLflow, LangSmith). + +```bash +# List configured integrations +kayba integrations list +kayba integrations list --json + +# Interactively configure an integration +kayba integrations configure mlflow +kayba integrations configure langsmith + +# Test a connection +kayba integrations test langsmith +kayba integrations test mlflow +``` + +The `configure` command prompts for each field interactively: + +- **MLflow**: tracking URI, auth type (none/basic/bearer/databricks), token, username, experiment name +- **LangSmith**: API URL (defaults to `https://api.smith.langchain.com`, use `https://eu.api.smith.langchain.com` for EU), API key, project name + +After saving, the connection is automatically tested. Credentials are stored in your Kayba account settings (DynamoDB), accessible from both the CLI and the web dashboard. + +### Job status and materialisation + +```bash +# Check job status +kayba status JOB_ID + +# Poll until complete +kayba status JOB_ID --wait --interval 10 + +# Materialise results into the skillbook +kayba materialize JOB_ID +``` + +### Batch pre-processing + +The `batch` command groups traces into batches before analysis. It works in two modes: + +**Prepare mode** (default) — extracts trace metadata and prints a classification prompt: + +```bash +kayba batch traces/ +``` + +This writes a skeleton `batches.json` and prints a prompt to stdout. Pipe it to an LLM (e.g. Claude Code) to fill in the batch assignments. + +**Apply mode** — validates and optionally uploads a batch plan: + +```bash +# Validate only +kayba batch traces/ --apply batches.json + +# Validate and upload each batch +kayba batch traces/ --apply batches.json --upload +``` + +Options: + +| Flag | Description | +|------|-------------| +| `--prompt FILE` | Custom classification prompt template | +| `-o FILE` | Output batch plan file (default: `batches.json`) | +| `--apply FILE` | Apply an existing batch plan | +| `--upload` | Upload each batch (requires `--apply`) | +| `--min-batch-size N` | Minimum traces per batch (default: 10) | +| `--max-batch-size N` | Maximum traces per batch (default: 30) | + +### Agent setup + +```bash +# Print CLI instructions and install pipeline skills +kayba setup + +# Append to a project agent file +kayba setup --append-to AGENTS.md + +# Skip skill installation +kayba setup --no-skills + +# Install into a different project +kayba setup --project-dir /path/to/project +``` + +Options: + +| Flag | Description | +|------|-------------| +| `--append-to FILE` | Append instructions to file instead of printing (recommended: `AGENTS.md`) | +| `--skills/--no-skills` | Install Claude Code pipeline skills (default: enabled) | +| `--project-dir DIR` | Project root directory (default: current directory) | + +By default `kayba setup` copies the **kayba-pipeline** skill into `.claude/skills/`. This skill orchestrates a 7-stage evaluation pipeline (analyze traces → compute metrics → build rubric → plan fixes → HITL review → apply fixes → verify). See [Claude Code](claude-code.md#pipeline-skill) for details. + +## End-to-end workflows + +### Interactive (human at terminal) + +```bash +# 1. Upload traces +kayba traces upload traces/ + +# 2. Run the pipeline (interactive trace selector) +kayba run + +# 3. Review insights +kayba insights list --status pending +kayba insights triage --accept-all + +# 4. Generate a prompt +kayba prompts generate -o prompt.md + +# 5. Install it into your agent +kayba prompts install --target claude-code +``` + +### Programmatic (agent or script) + +```bash +# 1. Upload traces +kayba traces upload traces/ + +# 2. List what was uploaded +TRACES=$(kayba traces list --json | jq -r '.[].id') + +# 3. Run the pipeline on all traces +JOB_ID=$(kayba run --all --json | jq -r '.jobId') + +# 4. Wait for completion +kayba status $JOB_ID --wait + +# 5. Accept all insights and generate prompt +kayba insights triage --accept-all +kayba prompts generate -o prompt.md + +# 6. Install the latest prompt into your agent +kayba prompts install --target codex +``` + +## Python client + +The `KaybaClient` class can be used directly in Python code: + +```python +from ace.cli.client import KaybaClient + +client = KaybaClient(api_key="your-key") + +# Trace management +traces = client.list_traces() +trace = client.get_trace("conv-123") +client.delete_trace("conv-123") +result = client.upload_traces([ + {"filename": "trace.md", "content": "...", "fileType": "md"}, +]) + +# Run pipeline +job = client.generate_insights( + trace_ids=["conv-123", "conv-456"], + model="claude-sonnet-4-6", +) + +# Check status +status = client.get_job(job["jobId"]) + +# List and triage +insights = client.list_insights(status="pending") +client.triage_insight(insights["insights"][0]["id"], "accepted") + +# Generate and pull prompts +client.generate_prompt() +prompts = client.list_prompts() +prompt = client.get_prompt(prompts[0]["id"]) + +# Integrations +integrations = client.get_integrations() +client.update_integration("langsmith", { + "enabled": True, + "apiUrl": "https://eu.api.smith.langchain.com", + "apiKey": "lsv2_pt_...", +}) +result = client.test_integration("langsmith") +``` + +## API endpoints + +| Method | Path | Client method | +|--------|------|---------------| +| `GET` | `/traces` | `list_traces()` | +| `POST` | `/traces` | `upload_traces()` | +| `GET` | `/traces/:id` | `get_trace()` | +| `DELETE` | `/traces/:id` | `delete_trace()` | +| `POST` | `/traces/batch` | `get_traces()` | +| `POST` | `/insights/generate` | `generate_insights()` | +| `GET` | `/insights` | `list_insights()` | +| `PATCH` | `/insights/:id` | `triage_insight()` | +| `GET` | `/jobs/:id` | `get_job()` | +| `POST` | `/jobs/:id` | `materialize_job()` | +| `POST` | `/prompts/generate` | `generate_prompt()` | +| `GET` | `/prompts` | `list_prompts()` | +| `GET` | `/prompts/:id` | `get_prompt()` | +| `GET` | `/integrations` | `get_integrations()` | +| `PUT` | `/integrations/:name` | `update_integration()` | +| `POST` | `/integrations/:name/test` | `test_integration()` | + +## Coding agent setup + +**Quick (current session):** Tell your coding agent to run `kayba setup`. The agent will see +the full CLI reference in its context and know how to use every command. The pipeline skill is +also installed to `.claude/skills/`, giving Claude Code access to the 7-stage evaluation pipeline. + +**Persistent (all future sessions):** Append instructions to your project's agent file: + +```bash +kayba setup --append-to AGENTS.md # universal (Claude Code, Cursor, Copilot, Windsurf, etc.) +kayba setup --append-to CLAUDE.md # Claude Code only +kayba setup --append-to .cursorrules # Cursor only +``` + +`AGENTS.md` is the recommended target — it's the universal standard supported by 20+ coding agents. + +To skip skill installation (e.g. for non-Claude-Code agents), pass `--no-skills`. + +This setup step is separate from prompt installation. Once you have accepted +insights and generated a prompt, use `kayba prompts install` to update +`AGENTS.md`, `CLAUDE.md`, or `.cursorrules` with the generated prompt content. + +## Environment variables + +| Variable | Description | +|----------|-------------| +| `KAYBA_API_KEY` | API key (required) | +| `KAYBA_API_URL` | Base URL (default: `https://use.kayba.ai/api`) | +| `ANTHROPIC_API_KEY` | Passed to server for LLM calls via `--anthropic-key` | + +## Using your generated prompt + +`kayba prompts generate -o prompt.md` writes a Markdown prompt block built from +your accepted insights. It does **not** update `CLAUDE.md`, `AGENTS.md`, or +`.cursorrules` automatically; you choose where to apply it. + +Use the built-in installer to update the right file without duplicating old +blocks: + +```bash +# Install the latest prompt from Kayba into Claude Code +kayba prompts install --target claude-code + +# Install the latest prompt into a universal agent file +kayba prompts install --target universal + +# Install a local export into Cursor +kayba prompts install --input prompt.md --target cursor +``` + +The installer manages a dedicated Kayba block, so re-running it replaces the +previous prompt instead of appending duplicates. diff --git a/docs/integrations/index.md b/docs/integrations/index.md new file mode 100644 index 0000000000000000000000000000000000000000..71c50296e5a873ea73d96850b524059284e1793b --- /dev/null +++ b/docs/integrations/index.md @@ -0,0 +1,86 @@ +# Integrations Overview + +ACE provides both runners and step-based integrations for popular agentic +frameworks. Some integrations are full runners, while others are composable +pipeline steps you can drop into a custom `Pipeline`. + +## Available Integrations + +| Runner | Framework | Input | Insight Level | +|--------|-----------|-------|--------------| +| [`ACELiteLLM`](litellm.md) | LiteLLM (100+ providers) | Questions | Micro | +| [`LangChain`](langchain.md) | LangChain Runnables | Chain inputs | Meso | +| [`BrowserUse`](browser-use.md) | browser-use | Task strings | Meso | +| [`ClaudeCode`](claude-code.md) | Claude Code CLI | Task strings | Meso | +| [`Claude SDK`](claude-sdk.md) | Anthropic Python SDK | Task strings or `ACESample` | Meso | +| [OpenClaw](openclaw.md) | OpenClaw transcripts | JSONL trace files | Meso | +| [MCP Server](mcp.md) | MCP (stdio) | Tool calls | Micro | +| [MCP Client Setup](mcp-client-setup.md) | Claude Code, Cursor, Windsurf | — | Setup Guide | +| [Opik](opik.md) | Opik observability | — | Monitoring | +| [Tracing](tracing.md) | Kayba tracing SDK | `@trace` / `start_span` | Cloud | +| [Hosted API](hosted-api.md) | Kayba hosted API | Trace files | Cloud | + +## The Pattern + +All integration runners follow the same three-step pattern: + +``` +1. INJECT — Add skillbook strategies to the agent's context +2. EXECUTE — Run the external agent normally +3. LEARN — Reflector + SkillManager update the skillbook +``` + +## Quick Construction + +Every runner offers a `from_model()` factory that builds ACE roles automatically: + +```python +from ace import BrowserUse, LangChain, ClaudeCode + +# Browser automation +browser = BrowserUse.from_model(browser_llm=my_llm, ace_model="gpt-4o-mini") + +# LangChain chain/agent +chain = LangChain.from_model(my_runnable, ace_model="gpt-4o-mini") + +# Claude Code CLI +coder = ClaudeCode.from_model(working_dir="./project", ace_model="gpt-4o-mini") +``` + +For direct Anthropic API usage without a runner, compose the SDK steps directly: + +```python +from ace import Pipeline, Reflector, SkillManager, Skillbook, learning_tail +from ace.integrations import ClaudeSDKExecuteStep, ClaudeSDKToTrace + +skillbook = Skillbook() +pipe = Pipeline([ + ClaudeSDKExecuteStep(model="claude-sonnet-4-20250514"), + ClaudeSDKToTrace(), + *learning_tail(Reflector("gpt-4o-mini"), SkillManager("gpt-4o-mini"), skillbook), +]) +``` + +## Shared Features + +All runners share these capabilities: + +- **Skillbook persistence** — `save()` / load via `skillbook_path` +- **Checkpointing** — automatic saves during long runs +- **Deduplication** — prevent duplicate skills +- **Background learning** — `wait=False` for async learning +- **Progress tracking** — `learning_stats` property + +## Which Integration Should I Use? + +- **Building a Q&A or reasoning agent?** Use [ACELiteLLM](litellm.md) +- **Have an existing LangChain chain or agent?** Use [LangChain](langchain.md) +- **Automating browser tasks?** Use [BrowserUse](browser-use.md) +- **Running coding tasks with Claude Code?** Use [ClaudeCode](claude-code.md) +- **Calling Anthropic directly from your own pipeline?** Use [Claude SDK](claude-sdk.md) +- **Want to monitor costs and traces?** Add [Opik](opik.md) +- **Learning from OpenClaw session transcripts?** Use [OpenClaw](openclaw.md) +- **Exposing ACE as an MCP tool provider?** Use the [MCP Server](mcp.md) and the [MCP Client Setup](mcp-client-setup.md) guide +- **Want to send traces to Kayba from your code?** Use [Tracing](tracing.md) +- **Want to use the hosted API instead of running locally?** Use the [Hosted API](hosted-api.md) CLI +- **Using a different framework?** See the [Integration Guide](../guides/integration.md) to build a custom runner diff --git a/docs/integrations/langchain.md b/docs/integrations/langchain.md new file mode 100644 index 0000000000000000000000000000000000000000..3d6cfbdee9e3d5680deb8e231422d3ff29a5d5bb --- /dev/null +++ b/docs/integrations/langchain.md @@ -0,0 +1,96 @@ +# LangChain Integration + +The `LangChain` runner wraps any LangChain Runnable (chains, `AgentExecutor`, LangGraph graphs) with ACE learning. The runner extracts execution traces and learns strategies from them. + +## Installation + +```bash +uv add ace-framework[langchain] +``` + +## Quick Start + +```python +from ace import LangChain + +runner = LangChain.from_model(your_chain, ace_model="gpt-4o-mini") + +results = runner.run([ + {"input": "Summarize this document"}, + {"input": "Extract key entities"}, +]) + +runner.save("chain_expert.json") +``` + +## Parameters + +### from_model() + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `runnable` | `Any` | — | LangChain Runnable (chain, AgentExecutor, graph) | +| `ace_model` | `str` | `"gpt-4o-mini"` | Model for Reflector + SkillManager | +| `ace_max_tokens` | `int` | `2048` | Max tokens for ACE LLM responses | +| `ace_temperature` | `float` | `0.0` | Sampling temperature for ACE roles | + +### from_roles() + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `runnable` | `Any` | — | LangChain Runnable | +| `reflector` | `ReflectorLike` | — | Reflector instance | +| `skill_manager` | `SkillManagerLike` | — | SkillManager instance | +| `skillbook_path` | `str` | `None` | Load saved skillbook | +| `output_parser` | `Callable` | `None` | Custom output extraction | +| `dedup_config` | `DeduplicationConfig` | `None` | Deduplication config | +| `checkpoint_dir` | `str` | `None` | Checkpoint directory | + +## Methods + +```python +results = runner.run(inputs, epochs=1) # Run with learning +results = runner.invoke(single_input) # Single input convenience +runner.save("path.json") # Save skillbook +runner.wait_for_background() # Wait for async learning +``` + +## How It Works + +1. **INJECT** — Skillbook strategies are added to the chain input +2. **EXECUTE** — LangChain runs the chain normally +3. **Extract trace** — ACE extracts intermediate steps, tool calls, and reasoning +4. **LEARN** — Reflector analyzes the trace, SkillManager updates the skillbook + +The runner handles simple chains, `AgentExecutor` (with `intermediate_steps`), and LangGraph graphs automatically. + +## Input Types + +The runner accepts any input your chain expects: + +```python +# String input +runner.run(["What is ACE?"]) + +# Dict input +runner.run([{"input": "query", "context": "..."}]) + +# Message list +runner.run([[HumanMessage(content="Hello")]]) +``` + +## Resuming from a Saved Skillbook + +```python +runner = LangChain.from_model( + your_chain, + ace_model="gpt-4o-mini", + skillbook_path="chain_expert.json", +) +``` + +## What to Read Next + +- [Integration Pattern](../guides/integration.md) — how the INJECT/EXECUTE/LEARN pattern works +- [Insight Levels](../concepts/insight-levels.md) — meso-level learning from traces +- [Opik Observability](opik.md) — monitor chain execution costs diff --git a/docs/integrations/litellm.md b/docs/integrations/litellm.md new file mode 100644 index 0000000000000000000000000000000000000000..ccaad66084e2da6f04524d74360c47da21f475f0 --- /dev/null +++ b/docs/integrations/litellm.md @@ -0,0 +1,164 @@ +# LiteLLM Integration + +`ACELiteLLM` is the simplest way to get a self-improving agent. It bundles Agent, Reflector, SkillManager, and Skillbook into a single class with `ask()` and `learn()` methods. + +## Quick Start + +```python +from ace import ACELiteLLM + +agent = ACELiteLLM.from_model("gpt-4o-mini") + +# Ask questions — learns patterns across them +answer = agent.ask("If all cats are animals, is Felix (a cat) an animal?") + +# Save and reload +agent.save("learned.json") +``` + +## Parameters + +### from_model() + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `model` | `str` | `"gpt-4o-mini"` | LiteLLM model identifier | +| `max_tokens` | `int` | `2048` | Max tokens for responses | +| `temperature` | `float` | `0.0` | Sampling temperature | +| `api_key` | `str` | `None` | API key (or use env variable) | +| `base_url` | `str` | `None` | Custom API endpoint | +| `skillbook_path` | `str` | `None` | Path to load saved skillbook | +| `environment` | `TaskEnvironment` | `None` | Evaluation environment | +| `dedup_config` | `DeduplicationConfig` | `None` | Skill deduplication config | +| `is_learning` | `bool` | `True` | Enable/disable learning | +| `opik` | `bool` | `False` | Enable Opik observability (pipeline traces + LiteLLM per-call cost tracking) | +| `opik_project` | `str` | `"ace-framework"` | Opik project name for organizing traces | +| `opik_tags` | `list[str]` | `None` | Tags applied to every Opik trace | + +## Methods + +### ask() + +Direct agent call using the current skillbook: + +```python +answer = agent.ask("Your question", context="Optional context") +``` + +### learn() + +Run the full ACE learning pipeline over samples: + +```python +from ace import Sample, SimpleEnvironment + +samples = [ + Sample(question="What is 2+2?", context="", ground_truth="4"), +] +results = agent.learn(samples, environment=SimpleEnvironment(), epochs=3) +``` + +### learn_from_feedback() + +Learn from the last `ask()` interaction: + +```python +agent.ask("What is the capital of France?") +agent.learn_from_feedback(feedback="Correct!", ground_truth="Paris") +``` + +### learn_from_traces() + +Learn from pre-recorded execution traces: + +```python +results = agent.learn_from_traces(traces, epochs=1) +``` + +### Lifecycle + +```python +agent.save("path.json") # Save skillbook +agent.load("path.json") # Load skillbook +agent.enable_learning() # Turn on learning +agent.disable_learning() # Turn off learning +agent.wait_for_background() # Wait for async learning +agent.learning_stats # Background progress +agent.skillbook # Current Skillbook +agent.get_strategies() # Formatted strategies +``` + +## Using a Cheaper Learning Model + +Use a strong model for the Agent and a cheaper one for learning: + +```python +from ace import ACELiteLLM, Agent, Reflector, SkillManager + +ace = ACELiteLLM( + agent=Agent("gpt-4o"), + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), +) +``` + +## Deduplication + +Prevent duplicate skills from accumulating: + +```python +from ace import DeduplicationConfig + +agent = ACELiteLLM.from_model( + "gpt-4o-mini", + dedup_config=DeduplicationConfig( + enabled=True, + embedding_model="text-embedding-3-small", + similarity_threshold=0.85, + ), +) +``` + +## Supported Providers + +Any model supported by [LiteLLM](https://docs.litellm.ai/): + +```python +# OpenAI +agent = ACELiteLLM.from_model("gpt-4o-mini") + +# Anthropic +agent = ACELiteLLM.from_model("claude-sonnet-4-5-20250929") + +# Google +agent = ACELiteLLM.from_model("gemini-pro") + +# Local (Ollama) +agent = ACELiteLLM.from_model("ollama/llama2") + +# Custom endpoint +agent = ACELiteLLM.from_model("gpt-4o-mini", base_url="https://your-endpoint.com") +``` + +## Opik Observability + +Enable tracing and cost tracking with a single flag: + +```python +ace = ACELiteLLM.from_model("gpt-4o-mini", opik=True, opik_project="my-experiment") + +# Both tracing modes are enabled: +# 1. Pipeline traces (OpikStep) — one trace per sample with ACE context +# 2. LiteLLM callback — per-LLM-call token/cost tracking + +results = ace.learn(samples, environment=SimpleEnvironment(), epochs=3) +# View traces at http://localhost:5173 → project "my-experiment" +``` + +See [Opik Observability](opik.md) for full details, environment variables, and manual setup. + +## What to Read Next + +- [Full Pipeline Guide](../guides/full-pipeline.md) — for more control over the pipeline +- [Async Learning](../guides/async-learning.md) — background learning with `wait=False` +- [Opik Observability](opik.md) — monitor costs and traces diff --git a/docs/integrations/mcp-client-setup.md b/docs/integrations/mcp-client-setup.md new file mode 100644 index 0000000000000000000000000000000000000000..fa909cfa80e8419231ac7763a27040b868096438 --- /dev/null +++ b/docs/integrations/mcp-client-setup.md @@ -0,0 +1,142 @@ +# ACE MCP Client Setup + +The ACE MCP server runs over `stdio`, so any MCP client that can launch a +local command can connect to it. + +This guide focuses on wiring `ace-mcp` into popular clients. For the full +tool reference, environment variables, and safety controls, see the +[MCP Server guide](mcp.md). + +## Prerequisites + +1. Install ACE with the MCP extra: + + ```bash + pip install "ace-framework[mcp]" + # or + uv add "ace-framework[mcp]" + ``` + +2. Set the model and provider credentials you want the server to use: + + ```bash + export ACE_MCP_DEFAULT_MODEL="gpt-4o-mini" + export OPENAI_API_KEY="sk-..." + ``` + +3. Verify the server starts: + + ```bash + ace-mcp + ``` + + It should log startup information to stderr and then wait for stdio input. + +## Claude Code + +Anthropic recommends managing Claude Code MCP servers with the `claude mcp` +commands. A user-scoped server can be added with: + +```bash +claude mcp add-json -s user ace '{ + "type": "stdio", + "command": "ace-mcp", + "env": { + "ACE_MCP_DEFAULT_MODEL": "gpt-4o-mini", + "OPENAI_API_KEY": "sk-..." + } +}' +``` + +Useful variants: + +- `-s project` stores the server in `.mcp.json` for the current repo. +- `claude mcp list` shows configured servers. +- `claude mcp get ace` prints the saved config. + +Once added, you can ask Claude Code to use ACE directly: + +```text +Use ace.ask with session_id "repo-default" to summarize the conventions in this repo. +``` + +## Cursor + +Cursor supports local stdio MCP servers. Add a server from the MCP settings UI +or your MCP config using this shape: + +```json +{ + "mcpServers": { + "ace": { + "command": "ace-mcp", + "env": { + "ACE_MCP_DEFAULT_MODEL": "gpt-4o-mini", + "OPENAI_API_KEY": "sk-..." + } + } + } +} +``` + +After saving, refresh MCP servers in Cursor and confirm the ACE tools appear. + +## Windsurf + +Windsurf exposes MCP configuration through **Windsurf Settings** > +**Cascade** > **MCP Servers**. Add a stdio server using the same command/env +shape: + +```json +{ + "mcpServers": { + "ace": { + "command": "ace-mcp", + "env": { + "ACE_MCP_DEFAULT_MODEL": "gpt-4o-mini", + "OPENAI_API_KEY": "sk-..." + } + } + } +} +``` + +Restart the MCP connection if the tools do not appear immediately. + +## Smoke Test with MCP Inspector + +Before debugging a client-specific setup, verify the server generically with +the MCP Inspector: + +```bash +npx @modelcontextprotocol/inspector ace-mcp +``` + +If the server starts and the six ACE tools appear, the remaining work is +client configuration rather than ACE itself. + +## Troubleshooting + +### `ace-mcp` is not found + +- Confirm the package was installed with the `mcp` extra. +- Run `which ace-mcp` (or the equivalent on your platform) and use the full + path in the client config if needed. + +### The client connects but no tools appear + +- Start `ace-mcp` manually first to confirm it launches cleanly. +- Check stderr logs from the server. +- Set `ACE_MCP_LOG_LEVEL=DEBUG` for more verbose logging. + +### Save/load should stay inside a safe directory + +Set `ACE_MCP_SKILLBOOK_ROOT` to constrain `ace.skillbook.save` and +`ace.skillbook.load` to a specific directory. + +## References + +- [Anthropic: Claude Code MCP](https://docs.anthropic.com/en/docs/claude-code/mcp) +- [Anthropic: Claude Code settings and scopes](https://code.claude.com/docs/en/settings) +- [Cursor MCP docs](https://docs.cursor.com/advanced/model-context-protocol) +- [Windsurf MCP docs](https://docs.windsurf.com/en/windsurf/cascade/mcp) diff --git a/docs/integrations/mcp.md b/docs/integrations/mcp.md new file mode 100644 index 0000000000000000000000000000000000000000..58c6686021c6798229de4f04a20de3dfe76548a7 --- /dev/null +++ b/docs/integrations/mcp.md @@ -0,0 +1,178 @@ +# ACE MCP Server + +ACE (Agentic Context Engine) provides an optional MCP server that exposes ACE as a tool provider over the [Model Context Protocol](https://modelcontextprotocol.io/). Orchestration frameworks, IDEs (Cursor, Windsurf, Claude Code), and other MCP clients can connect to the server and use ACE skills at runtime. + +The integration is fully opt-in. Installing ACE without the `mcp` extra does not pull in the MCP SDK, and attempting to start `ace-mcp` without that extra will fail with an install hint instead of breaking normal ACE imports. + +> Need client-specific setup steps? See the [MCP Client Setup](mcp-client-setup.md) guide. + +## Installation + +```bash +uv add "ace-framework[mcp]" +``` + +## Running the Server + +Start the server using the provided CLI entrypoint: + +```bash +ace-mcp +``` + +By default it communicates over `stdio`, making it ready for integration as a local tool provider in any MCP-compatible client. + +Example with a specific model: + +```bash +ACE_MCP_DEFAULT_MODEL=bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 ace-mcp +``` + +## Configuration + +All settings are read from environment variables with the `ACE_MCP_` prefix: + +| Variable | Default | Description | +|----------|---------|-------------| +| `ACE_MCP_DEFAULT_MODEL` | `gpt-4o-mini` | LiteLLM model identifier used when creating new sessions. Any [LiteLLM-supported model](https://docs.litellm.ai/docs/providers) works (e.g. `bedrock/...`, `anthropic/...`, `openai/...`). | +| `ACE_MCP_SAFE_MODE` | `false` | When `true`, blocks `ace.learn.sample`, `ace.learn.feedback`, `ace.skillbook.save`, and `ace.skillbook.load`. Read-only tools (`ace.ask`, `ace.skillbook.get`) remain available. | +| `ACE_MCP_ALLOW_SAVE_LOAD` | `true` | When `false`, blocks `ace.skillbook.save` and `ace.skillbook.load` independently of safe mode. | +| `ACE_MCP_MAX_SAMPLES_PER_CALL` | `25` | Maximum number of samples accepted in a single `ace.learn.sample` call. | +| `ACE_MCP_MAX_PROMPT_CHARS` | `100000` | Maximum total characters across question + context fields. | +| `ACE_MCP_SESSION_TTL_SECONDS` | `3600` | Idle time (seconds) before a session is garbage-collected. | +| `ACE_MCP_LEARN_TIMEOUT_SECONDS` | `300` | Maximum seconds for a single `ace.learn.sample` or `ace.learn.feedback` call before returning `ACE_MCP_TIMEOUT`. | +| `ACE_MCP_SKILLBOOK_ROOT` | unset | If set, `ace.skillbook.save` and `ace.skillbook.load` reject paths outside this directory. | +| `ACE_MCP_LOG_LEVEL` | `INFO` | Server log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`). Logs go to stderr. | + +## Tools + +### `ace.ask` + +Ask a question using the current skillbook. Does **not** mutate the skillbook. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | yes | Session identifier for state isolation. | +| `question` | string | yes | The question to answer. | +| `context` | string | no | Additional context for the question. | +| `session_config` | object | no | Override `model`, `temperature`, `max_tokens` for this session. | + +Returns: `answer`, `skill_count`. + +### `ace.learn.sample` + +Provide sample question/answer pairs for ACE to learn from. Runs the full ACE pipeline (Agent, Evaluate, Reflect, Update, Apply). + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | yes | Session identifier. | +| `samples` | array | yes | List of `{question, context?, ground_truth?, metadata?}` items (1–25). | +| `epochs` | int | no | Number of learning passes (default: 1, max: 20). | +| `session_config` | object | no | Override model settings. | + +Returns: `processed`, `failed`, `skill_count_before`, `skill_count_after`, `new_skill_count`. + +Blocked by: `ACE_MCP_SAFE_MODE=true`. + +### `ace.learn.feedback` + +Provide feedback on a previous answer. If a prior `ace.ask` exists for the session, learns directly from that interaction. Otherwise, builds a trace from the provided fields and learns from it. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | yes | Session identifier. | +| `question` | string | yes | The original question. | +| `answer` | string | yes | The answer being evaluated. | +| `feedback` | string | yes | Feedback about answer quality. | +| `context` | string | no | Original context. | +| `ground_truth` | string | no | The correct answer. | +| `session_config` | object | no | Override model settings. | + +Returns: `learned` (always `true` on success — the learning path executed), `skill_count_before`, `skill_count_after`, `new_skill_count`. + +Blocked by: `ACE_MCP_SAFE_MODE=true`. + +### `ace.skillbook.get` + +Retrieve skills and statistics from the active skillbook. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | yes | Session identifier. | +| `limit` | int | no | Max skills to return (default: 20, max: 200). | +| `include_invalid` | bool | no | Include invalidated skills (default: false). | + +Returns: `stats`, `skills[]` (each with `id`, `content`, `topic`, `helpful`, `harmful`, `neutral`). + +### `ace.skillbook.save` + +Save the session's skillbook to a file on disk. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | yes | Session identifier. | +| `path` | string | yes | File path to save to. | + +Returns: `path` (resolved absolute path), `saved_skill_count`. + +Blocked by: `ACE_MCP_SAFE_MODE=true` (`ACE_MCP_FORBIDDEN_IN_SAFE_MODE`) or `ACE_MCP_ALLOW_SAVE_LOAD=false` (`ACE_MCP_SAVE_LOAD_DISABLED`). The path is resolved to a canonical absolute path before validation and file I/O. Must be under `ACE_MCP_SKILLBOOK_ROOT` if configured. + +### `ace.skillbook.load` + +Load a skillbook from disk into the session, replacing the current one. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `session_id` | string | yes | Session identifier. | +| `path` | string | yes | File path to load from. | + +Returns: `path` (resolved absolute path), `skill_count`. + +Blocked by: `ACE_MCP_SAFE_MODE=true` (`ACE_MCP_FORBIDDEN_IN_SAFE_MODE`) or `ACE_MCP_ALLOW_SAVE_LOAD=false` (`ACE_MCP_SAVE_LOAD_DISABLED`). The path is resolved to a canonical absolute path before validation and file I/O. Must be under `ACE_MCP_SKILLBOOK_ROOT` if configured. + +## Session Model + +All state is isolated by `session_id`. Each session holds its own `ACELiteLLM` runner with an independent skillbook. Pass the same `session_id` across calls to persist context within server memory. + +Sessions are garbage-collected after `ACE_MCP_SESSION_TTL_SECONDS` of inactivity. Per-session locks ensure concurrent requests to the same session are serialised. + +Any tool that accepts `session_config` can override the model, temperature, and max_tokens for that session's runner on first creation. Once a session exists, `session_config` on subsequent calls is used only if it creates a new session. + +## Architecture + +The MCP server does **not** use custom pipeline steps. It is a thin async layer over `ACELiteLLM`: + +``` +MCP Client (stdio) + → MCP SDK (handles JSON-RPC framing) + → adapters.py (tool registration, schema generation, error mapping) + → handlers.py (validation, session management, safety guards) + → ACELiteLLM (sync runner — bridged via asyncio.to_thread) + → Pipeline (internal — handles step execution, async_boundary, background learning) +``` + +The handlers use `asyncio.to_thread()` to call the sync `ACELiteLLM` methods from the async MCP event loop. This is the standard Python pattern for bridging async callers to sync APIs. The pipeline engine handles all internal async concerns (step-level `to_thread`, `async_boundary` for background learning) transparently. + +## Testing with the MCP Inspector + +The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) provides a web UI for testing MCP servers interactively: + +```bash +npx @modelcontextprotocol/inspector uv run ace-mcp +``` + +Set `ACE_MCP_DEFAULT_MODEL` in the Inspector's environment variables panel before connecting. + +## File Layout + +``` +ace/integrations/mcp/ + __init__.py ← Package marker + server.py ← Server creation and CLI entrypoint + config.py ← MCPServerConfig (pydantic-settings, env vars) + registry.py ← SessionRegistry (session lifecycle, TTL sweep) + handlers.py ← MCPHandlers (validation, safety, delegation to ACELiteLLM) + adapters.py ← MCP SDK glue (tool registration, schema inlining, error mapping) + models.py ← Pydantic request/response models for all six tools + errors.py ← ACEMCPError hierarchy and MCP error mapping +``` diff --git a/docs/integrations/openclaw.md b/docs/integrations/openclaw.md new file mode 100644 index 0000000000000000000000000000000000000000..f892ea4418202d4cde0c584ef8183d72c112b57f --- /dev/null +++ b/docs/integrations/openclaw.md @@ -0,0 +1,517 @@ +# OpenClaw Integration + +Make your [OpenClaw](https://docs.openclaw.ai) agent **self-improving**. ACE reads session transcripts, extracts what worked and what didn't, and feeds learned strategies back into the agent's context via a skillbook — automatically, every session. + +--- + +## What Is OpenClaw? + +[OpenClaw](https://github.com/openclaw/openclaw) is an open-source, self-hosted AI assistant gateway. It connects AI models (Claude, GPT, etc.) to messaging platforms like Telegram, WhatsApp, Discord, and more. It runs locally and stores all data — sessions, memory, configuration — as files on your machine under `~/.openclaw/`. + +ACE plugs into this by reading session transcripts and building a skillbook of learned strategies that the agent loads at session start. + +--- + +## How It Works + +```mermaid +flowchart TD + A["OpenClaw session ends"] --> B["Transcript saved to<br><code>~/.openclaw/agents/main/sessions/*.jsonl</code>"] + B --> C["<b>ace-learn</b><br>session start or on-demand"] + C --> D["LoadTracesStep → OpenClawToTraceStep"] + D --> E["<b>TraceAnalyser</b><br>Reflect → Update → Apply"] + E --> F["<code>ace_skillbook.json</code><br>machine-readable"] + E --> G["<code>ace_skillbook.md</code><br>human-readable"] + G --> H["AGENTS.md tells agent<br>to read skillbook"] + H --> I["Agent loads strategies<br>into context"] +``` + +1. OpenClaw writes session transcripts to `~/.openclaw/agents/<id>/sessions/*.jsonl` +2. `ace-learn` runs at the start of the next session (or on-demand) +3. **LoadTracesStep** reads JSONL files into raw event lists +4. **OpenClawToTraceStep** converts events into structured traces +5. **TraceAnalyser** runs the learning pipeline (Reflect → Update → Apply) +6. Updated skillbook is written to the workspace volume +7. The agent reads `ace_skillbook.md` into its context and applies relevant strategies + +--- + +## Prerequisites + +Before setting up ACE, you need a working OpenClaw installation. + +!!! info "Platform support" + OpenClaw runs on **Linux**, **macOS**, and **Windows** (via WSL2). All shell commands on this page use bash syntax. On Windows, run them inside your WSL2 environment. The `setup.py` script uses cross-platform Python and works on all three platforms natively. + +### 1. Install OpenClaw + +!!! note "Already have OpenClaw running?" + Skip to [Setup Methods](#setup-methods) below. + +=== "npm (quickest)" + + ```bash + npm install -g openclaw@latest + openclaw onboard --install-daemon + ``` + + The onboard wizard walks you through model provider setup, API keys, and optional channel connections (Telegram, WhatsApp, etc.). + +=== "Docker" + + ```bash + git clone https://github.com/openclaw/openclaw.git + cd openclaw + ./docker-setup.sh + ``` + + The setup script builds the image, runs onboarding, and starts the gateway via Docker Compose. + +=== "From source" + + ```bash + git clone https://github.com/openclaw/openclaw.git + cd openclaw + pnpm install && pnpm ui:build && pnpm build + pnpm openclaw onboard --install-daemon + ``` + +For full details, see the [OpenClaw documentation](https://docs.openclaw.ai). + +### 2. Verify OpenClaw is working + +Make sure the gateway is running and you have at least one completed session: + +```bash +# Check the gateway is up +curl -fsS http://127.0.0.1:18789/healthz + +# Check sessions exist +ls ~/.openclaw/agents/main/sessions/*.jsonl +``` + +### 3. Get an LLM API key for ACE + +ACE needs its own LLM API key to run the reflection model. This is separate from the key OpenClaw uses. Any [LiteLLM-supported provider](https://docs.litellm.ai/docs/providers) works: + +| Provider | Key variable | Example model | +|----------|-------------|---------------| +| Anthropic | `ANTHROPIC_API_KEY` | `anthropic/claude-sonnet-4-6` | +| OpenRouter | `OPENROUTER_API_KEY` | `openrouter/anthropic/claude-sonnet-4-6` | +| AWS Bedrock | `AWS_BEARER_TOKEN_BEDROCK` | `bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0` | +| LiteLLM proxy | `LITELLM_API_KEY` | `anthropic/claude-sonnet-4-5` | + +--- + +## Setup + +Two steps: **install the skill** (copies files + patches AGENTS.md), then **choose how to run** the learning script. + +### Step 1 — Install the skill + +The skill needs to be copied into your OpenClaw workspace and AGENTS.md needs to be updated so the agent knows to use the skillbook. You can do this automatically with the setup script or manually. + +=== "Automatic (setup script)" + + Clone the ACE repo and run the setup script: + + ```bash + git clone https://github.com/Kayba-ai/agentic-context-engine.git + cd agentic-context-engine + python examples/openclaw/setup.py + ``` + + This does two things: + + 1. **Copies** the `kayba-ace/` skill folder to `~/.openclaw/workspace/skills/kayba-ace/` + 2. **Appends** auto-learning instructions to `~/.openclaw/workspace/AGENTS.md` + + Options: + + ```bash + python examples/openclaw/setup.py --no-agents # skip AGENTS.md patching + python examples/openclaw/setup.py --openclaw-home /path/to/.openclaw # custom path + ``` + + The script is idempotent — it won't overwrite generated files (`ace_skillbook.json`, `ace_skillbook.md`, `ace_processed.txt`) and skips the AGENTS.md patch if already present. + +=== "Manual" + + **1. Copy the skill folder** into the OpenClaw workspace: + + ```bash + # Clone the ACE repo (if you haven't already) + git clone https://github.com/Kayba-ai/agentic-context-engine.git + + # Copy the skill + mkdir -p ~/.openclaw/workspace/skills/kayba-ace + cp agentic-context-engine/examples/openclaw/kayba-ace/* \ + ~/.openclaw/workspace/skills/kayba-ace/ + ``` + + **2. Patch AGENTS.md** — append the auto-learning instructions so the agent reads the skillbook at session start: + + ```bash + cat agentic-context-engine/examples/openclaw/AGENTS.md.snippet \ + >> ~/.openclaw/workspace/AGENTS.md + ``` + + Or copy the snippet content manually and paste it at the end of your `AGENTS.md`. The snippet tells the agent to: + + - Run `ace-learn` at session start and report results + - Read `skills/kayba-ace/ace_skillbook.md` into its context + - Cite strategy IDs when applying learned strategies + + !!! warning "Check for duplicates" + If you run the manual steps more than once, make sure you don't append the snippet twice. Look for the `## Auto-Learning` heading in your AGENTS.md — if it's already there, skip this step. + +### Step 2 — Choose how to run learning + +| | Docker (recommended) | Host | +|---|---|---| +| **How it works** | Bakes ACE into the OpenClaw Docker image | Runs ACE on your host machine | +| **Learning trigger** | Agent runs `ace-learn` at session start | Cron job or manual | +| **Pros** | Zero runtime setup, fully automatic | No Docker customization needed | +| **Cons** | Requires rebuilding the image | Agent can't trigger learning itself | + +--- + +## Docker Setup (Recommended) + +Extends your OpenClaw Docker image with Python 3.12 and the ACE framework pre-installed. The agent runs `ace-learn` at session start automatically. + +#### 2a — Get the Dockerfile + +```bash +# From the ACE repo (already cloned in Step 1) +cp examples/openclaw/Dockerfile.ace /path/to/your/openclaw/ +``` + +Or download it directly: + +```bash +curl -o Dockerfile.ace \ + https://raw.githubusercontent.com/Kayba-ai/agentic-context-engine/main/examples/openclaw/Dockerfile.ace +``` + +#### 2b — Build the image + +From your OpenClaw directory: + +```bash +# Build the base OpenClaw image first (if not already built) +docker build -t openclaw:base . + +# Extend with ACE +docker build -t openclaw:local --build-arg OPENCLAW_IMAGE=openclaw:base -f Dockerfile.ace . +``` + +!!! info "What this installs" + The extended image adds ~200MB and includes: + + - **uv** — Python package manager + - **Python 3.12** — via uv standalone builds (the base image ships 3.11) + - **ACE framework** — cloned from GitHub at `/opt/ace` with all dependencies + - **`ace-learn`** — wrapper script at `/usr/local/bin/ace-learn` + +Then point your OpenClaw setup at the new image. In your `.env` file: + +```bash +OPENCLAW_IMAGE=openclaw:local +``` + +#### 2c — Pass your API key + +Add the ACE reflection key to your `docker-compose.yml` environment section (or `.env` file): + +```yaml +services: + openclaw-gateway: + environment: + # ... existing keys ... + # Add ONE of these depending on your provider: + AWS_BEARER_TOKEN_BEDROCK: ${AWS_BEARER_TOKEN_BEDROCK} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} + LITELLM_API_KEY: ${LITELLM_API_KEY} + # Optional: override the default reflection model + ACE_MODEL: ${ACE_MODEL:-bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0} +``` + +The default model is `bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0`. Set `ACE_MODEL` in your `.env` to override. + +#### 2d — Restart and verify + +```bash +docker compose down && docker compose up -d openclaw-gateway +``` + +Send a message to your agent (e.g., via Telegram). It should: + +1. Run `ace-learn` and report what it found +2. Read the skillbook into its context +3. Respond to your message, citing strategy IDs when relevant + +You can also test directly: + +```bash +# Dry run — parses sessions without making LLM calls +docker run --rm -v ~/.openclaw:/home/node/.openclaw openclaw:local ace-learn --dry-run + +# Full run +docker run --rm \ + -v ~/.openclaw:/home/node/.openclaw \ + -e AWS_BEARER_TOKEN_BEDROCK="$AWS_BEARER_TOKEN_BEDROCK" \ + openclaw:local ace-learn +``` + +--- + +## Host Setup + +Run ACE on the host machine (outside Docker). This reads session files directly from disk. Useful if you don't want to customize the Docker image. + +#### 2a — Install ACE dependencies + +From the ACE repo (already cloned in Step 1): + +```bash +cd agentic-context-engine +uv sync +``` + +!!! note "Python 3.12+ required" + Check with `python3 --version`. Install [uv](https://docs.astral.sh/uv/) if you don't have it. + +#### 2b — Configure your API key + +=== "Anthropic" + + ```bash + export ANTHROPIC_API_KEY="sk-ant-..." + ``` + +=== "OpenRouter" + + ```bash + export OPENROUTER_API_KEY="sk-or-..." + export ACE_MODEL="openrouter/anthropic/claude-sonnet-4-6" + ``` + +=== "AWS Bedrock" + + ```bash + export ACE_MODEL="bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0" + ``` + + Uses AWS SDK auth — no explicit key needed if credentials are configured. + +You can also put these in `~/.openclaw/.env` or `~/.env` — the script loads both via `python-dotenv`. + +#### 2c — Verify and run + +```bash +cd /path/to/agentic-context-engine + +# Dry run (no LLM calls, just parse sessions) +uv run python ~/.openclaw/workspace/skills/kayba-ace/learn_from_traces.py --dry-run + +# Learn from all new sessions +uv run python ~/.openclaw/workspace/skills/kayba-ace/learn_from_traces.py + +# Process specific files +uv run python ~/.openclaw/workspace/skills/kayba-ace/learn_from_traces.py \ + ~/.openclaw/agents/main/sessions/f967d602.jsonl + +# Reprocess everything +uv run python ~/.openclaw/workspace/skills/kayba-ace/learn_from_traces.py --reprocess +``` + +#### 2d — Automate (optional) + +=== "Linux / macOS (cron)" + + ```bash + crontab -e + ``` + + Add: + + ``` + */30 * * * * cd /path/to/agentic-context-engine && uv run python ~/.openclaw/workspace/skills/kayba-ace/learn_from_traces.py >> /tmp/ace-openclaw.log 2>&1 + ``` + +=== "Windows (Task Scheduler)" + + Create a scheduled task that runs every 30 minutes: + + ```powershell + # From an elevated PowerShell prompt + $action = New-ScheduledTaskAction ` + -Execute "wsl" ` + -Argument "bash -c 'cd /path/to/agentic-context-engine && uv run python ~/.openclaw/workspace/skills/kayba-ace/learn_from_traces.py >> /tmp/ace-openclaw.log 2>&1'" + $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 30) + Register-ScheduledTask -TaskName "ACE Learn" -Action $action -Trigger $trigger + ``` + + This calls into WSL2 where OpenClaw and ACE are installed. + +=== "Manual" + + Run the script whenever you want to learn from new sessions: + + ```bash + cd /path/to/agentic-context-engine + uv run python ~/.openclaw/workspace/skills/kayba-ace/learn_from_traces.py + ``` + +!!! note "AGENTS.md for host setup" + The setup script already patched AGENTS.md in Step 1. For the host setup, the agent can't run `ace-learn` directly (it's not in the container), so it will report that `ace-learn` is not found and continue normally. Learning happens externally via cron or manual runs; the agent still reads the skillbook at session start. + +--- + +## Output Files + +The learning script writes these files to the skill directory: + +| File | Format | Description | +|---|---|---| +| `ace_skillbook.json` | JSON | Machine-readable skillbook (persists across runs) | +| `ace_skillbook.md` | Markdown | Human-readable skillbook grouped by section | +| `ace_processed.txt` | Text | Tracks which sessions have been processed | + +The agent loads strategies by **reading `ace_skillbook.md` at session start**. This must be an explicit instruction in AGENTS.md — OpenClaw does not auto-inline linked files. The agent uses its file-reading tools to load the content into its context window. + +Once loaded, the agent can cite strategy IDs (e.g., `conversation_style-00003`) when applying them. + +--- + +## Example Skillbook Output + +After processing a few sessions, `ace_skillbook.md` might contain: + +```markdown +## conversation_style + +### `conversation_style-00003` + +Maintain brief, natural responses without performative language + +**Justification:** Establishes consistent conversational tone across interaction types +**Evidence:** Maintained direct, helpful tone across greeting, creative request, +modification, and casual follow-up + +*Tags: helpful=5, harmful=0, neutral=0* + +## debugging + +### `debugging-00005` + +Test litellm calls directly before debugging ace pipeline + +**Justification:** Systematic debugging approach that isolated authentication issues +**Evidence:** Direct litellm.completion() calls worked while ace failed + +*Tags: helpful=1, harmful=0, neutral=0* +``` + +--- + +## Reference + +### Environment variables + +| Variable | Default | Description | +|---|---|---| +| `ACE_MODEL` | `bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0` | LLM for reflection and skill extraction | +| `OPENCLAW_AGENT_ID` | `main` | Agent ID for session discovery | +| `OPENCLAW_HOME` | `$HOME/.openclaw` | OpenClaw home directory (used by `ace-learn` only) | +| `LITELLM_API_KEY` | — | API key (for non-Bedrock providers) | +| `SPH_LITELLM_KEY` | — | Alternative API key variable | +| `AWS_BEARER_TOKEN_BEDROCK` | — | AWS Bedrock bearer token | +| `ANTHROPIC_API_KEY` | — | Anthropic API key | +| `OPENROUTER_API_KEY` | — | OpenRouter API key | + +### CLI arguments + +``` +ace-learn [OPTIONS] [FILES...] + +Options: + --dry-run Parse sessions but skip learning (no LLM calls) + --reprocess Ignore processed log, reprocess all sessions + --agent AGENT_ID OpenClaw agent ID (default: main) + --output DIR Output directory for skillbook files + --opik Enable Opik observability logging + +Positional: + FILES Specific JSONL files to process (skips discovery) +``` + +### Pipeline steps + +**LoadTracesStep** — Reads a JSONL file and parses each line into a list of event dicts. + +**OpenClawToTraceStep** — Converts raw OpenClaw events into a structured trace: + +```python +{ + "question": "User: ...\n\nUser: ...", + "reasoning": "[thinking] ...\n[tool:read] ...\n[response] ...", + "answer": "Last assistant response", + "skill_ids": [], + "feedback": "OpenClaw session: 3 user messages, 1 assistant responses, model: ..., 14605 tokens", + "ground_truth": None +} +``` + +**TraceAnalyser** — Runs the ACE learning tail: + +1. **Reflect** — LLM analyzes the trace for patterns, errors, and effective strategies +2. **Tag** — Scores cited skills as helpful/harmful/neutral +3. **Update** — LLM decides skillbook mutations (ADD, UPDATE, REMOVE, CONSOLIDATE) +4. **Apply** — Commits changes to the in-memory skillbook + +--- + +## Troubleshooting + +??? question "Sessions directory not found" + The agent hasn't completed a session yet, or `OPENCLAW_AGENT_ID` is wrong. Check: + + ```bash + ls ~/.openclaw/agents/ + ``` + +??? question "Nothing new to learn from" + All sessions have been processed. Use `--reprocess` to rerun, or wait for new sessions. + +??? question "`ace-learn` not found in Docker" + Make sure you built with `Dockerfile.ace` and are using the correct image tag: + + ```bash + docker run --rm openclaw:local which ace-learn + ``` + +??? question "Import errors for `ace` (host setup)" + The Docker image includes a cloned copy of the ACE repo at `/opt/ace` with all dependencies pre-installed — this is handled by `Dockerfile.ace`. For the host setup, make sure you run from the ACE repo root with `uv run` so that `ace` is importable: + + ```bash + cd /path/to/agentic-context-engine + uv run python ~/.openclaw/workspace/skills/kayba-ace/learn_from_traces.py + ``` + +??? question "API key errors in Docker" + Make sure your LLM API key is passed through `docker-compose.yml`. Check with: + + ```bash + docker compose exec openclaw-gateway env | grep -E 'API_KEY|BEARER_TOKEN|ACE_MODEL' + ``` + +--- + +## What to Read Next + +- [Integration Pattern](../guides/integration.md) — how the INJECT/EXECUTE/LEARN pattern works +- [The Skillbook](../concepts/skillbook.md) — how learned strategies are stored +- [ACE Design](../ACE_DESIGN.md) — architecture and step reference diff --git a/docs/integrations/opik.md b/docs/integrations/opik.md new file mode 100644 index 0000000000000000000000000000000000000000..3ffa5606b69a3dc5a2f197d6832c7e2ddca47e09 --- /dev/null +++ b/docs/integrations/opik.md @@ -0,0 +1,239 @@ +# Opik Observability + +ACE integrates with [Opik](https://github.com/comet-ml/opik) for tracing, cost tracking, and performance monitoring. All Opik tracing is **explicit opt-in** — it is never auto-enabled just because the package is installed. + +Two independent tracing modes: + +1. **Pipeline step** (`OpikStep`) — client-agnostic, logs one Opik trace per sample with ACE context fields. +2. **LiteLLM callback** (`register_opik_litellm_callback`) — LiteLLM-specific, tracks per-LLM-call tokens and costs. + +## Installation + +```bash +uv add ace-framework[observability] +``` + +## Quick Start + +```python +from ace import ACELiteLLM + +# Easiest: ACELiteLLM enables both tracing modes with one flag +ace = ACELiteLLM.from_model("gpt-4o-mini", opik=True, opik_project="my-experiment") +``` + +```python +from ace import ( + ACE, OpikStep, + Agent, Reflector, SkillManager, + SimpleEnvironment, +) + +# Manual: Add OpikStep via extra_steps +runner = ACE.from_roles( + agent=Agent("gpt-4o-mini"), + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), + environment=SimpleEnvironment(), + extra_steps=[OpikStep(project_name="my-experiment")], +) +``` + +```python +# LLM-level cost tracking only (no pipeline traces) +from ace import register_opik_litellm_callback + +registered = register_opik_litellm_callback(project_name="my-experiment") +``` + +## Starting the Opik Server + +=== "Local (Docker)" + + ```bash + docker run -d -p 5173:5173 --name opik ghcr.io/comet-ml/opik:latest + + # View traces at http://localhost:5173 + ``` + +=== "Comet Cloud" + + ```bash + export COMET_API_KEY="your-api-key" + # Traces appear at https://www.comet.com/opik + ``` + +## OpikStep + +`OpikStep` is a terminal side-effect step that logs one Opik trace per sample. It reads context fields but never mutates them — safe to append to any pipeline. + +### Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `project_name` | `str` | `"ace-framework"` | Opik project for organizing traces | +| `tags` | `list[str]` | `None` | Extra tags attached to every trace | + +### What Gets Logged + +Each trace includes: + +| Field | Source | +|-------|--------| +| **Input** | Question and context from the sample | +| **Output** | Answer, reasoning, and skill IDs from `AgentOutput` | +| **Metadata** | Epoch, step index, skill count, reflection insights, operation counts | +| **Feedback scores** | Accuracy extracted from environment feedback (correct / incorrect) | + +### Trace Hierarchy + +```mermaid +graph TD + P["Project: my-experiment"] + P --> T["Trace: sample_run_001"] + T --> I["Input: question + context"] + T --> O["Output: answer + reasoning + skill_ids"] + T --> M["Metadata: epoch=2, skills=12, ops=3"] + T --> F["Feedback: accuracy=1.0"] + T --> L["LLM Calls (automatic)"] + L --> L1["agent_generate — 450 tokens, $0.0003"] + L --> L2["reflector_reflect — 620 tokens, $0.0004"] + L --> L3["skill_manager_update — 380 tokens, $0.0002"] +``` + +## LLM Cost Tracking + +`OpikStep` does **not** register the LiteLLM callback — the two tracing modes are independent. To get per-LLM-call cost tracking, call `register_opik_litellm_callback()` separately: + +```python +from ace import register_opik_litellm_callback + +success = register_opik_litellm_callback(project_name="cost-tracking") +# Returns True if registered, False if Opik unavailable +``` + +Every LLM call is then automatically tracked with: + +- Input / output tokens +- Model used +- Cost per call +- Latency + +When using `ACELiteLLM` with `opik=True`, both modes are enabled together automatically — no need to call `register_opik_litellm_callback()` manually. + +## Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `OPIK_PROJECT_NAME` | Project name for organizing traces | `ace-framework` | +| `OPIK_DISABLED=true` | Disable all Opik tracing | Not set | +| `OPIK_ENABLED=false` | Alternative way to disable tracing | Not set | +| `OPIK_URL_OVERRIDE` | Custom Opik server URL | `http://localhost:5173/api` | +| `OPIK_WORKSPACE` | Opik workspace name | `default` | + +## Error Handling + +When using `ACELiteLLM` with `opik=True`, errors are **raised immediately**: + +- `ImportError` if the `opik` package is not installed +- `RuntimeError` if the Opik client fails to initialize (bad config, disabled via env vars) + +This ensures you know immediately if tracing is broken, rather than discovering missing traces later. + +When using `OpikStep` directly via `extra_steps`, it soft-imports Opik and silently becomes a no-op if the package is absent — useful for pipelines that should work with or without observability. + +```python +from ace import OPIK_AVAILABLE + +if OPIK_AVAILABLE: + print("Opik tracing is available") +``` + +## Troubleshooting: `~/.opik.config` + +The Opik SDK stores a global config file at `~/.opik.config` (created by `opik.configure()`). This file **overrides environment variables** and can cause silent failures if it contains stale settings. + +If traces aren't appearing, check: + +```bash +cat ~/.opik.config +``` + +A correct config for Comet Cloud looks like: + +```ini +[opik] +url_override = https://www.comet.com/opik/api/ +workspace = your-workspace-name +``` + +Common issues: + +- **Wrong URL**: `https://www.comet.com/api/` (missing `/opik/`) causes 404 errors +- **Wrong workspace**: `workspace = default` instead of your actual workspace name +- **Stale config**: Re-run `opik.configure()` or edit the file directly to fix + +## Disabling Tracing + +```bash +# In CI or tests +OPIK_DISABLED=true pytest tests/ + +# Or via the alternative variable +OPIK_ENABLED=false python my_script.py +``` + +## Full Example + +=== "ACELiteLLM (easiest)" + + ```python + from ace import ACELiteLLM, Sample, SimpleEnvironment + + ace = ACELiteLLM.from_model("gpt-4o-mini", opik=True, opik_project="ace-training") + + samples = [ + Sample(question="What is 2+2?", context="", ground_truth="4"), + Sample(question="Capital of France?", context="", ground_truth="Paris"), + ] + + results = ace.learn(samples, environment=SimpleEnvironment(), epochs=3) + ace.save("trained.json") + + # View traces at http://localhost:5173 → project "ace-training" + ``` + +=== "ACE runner (manual)" + + ```python + from ace import ( + ACE, Agent, Reflector, SkillManager, Skillbook, + SimpleEnvironment, Sample, OpikStep, + register_opik_litellm_callback, + ) + + runner = ACE.from_roles( + agent=Agent("gpt-4o-mini"), + reflector=Reflector("gpt-4o-mini"), + skill_manager=SkillManager("gpt-4o-mini"), + environment=SimpleEnvironment(), + extra_steps=[OpikStep(project_name="ace-training")], + ) + + # Optionally add LLM-level cost tracking + register_opik_litellm_callback(project_name="ace-training") + + samples = [ + Sample(question="What is 2+2?", context="", ground_truth="4"), + Sample(question="Capital of France?", context="", ground_truth="Paris"), + ] + + results = runner.run(samples, epochs=3) + runner.save("trained.json") + ``` + +## What to Read Next + +- [Integration Pattern](../guides/integration.md) — how runners compose pipeline steps +- [Full Pipeline Guide](../guides/full-pipeline.md) — building pipelines from scratch +- [Async Learning](../guides/async-learning.md) — background learning with cost monitoring diff --git a/docs/integrations/tracing.md b/docs/integrations/tracing.md new file mode 100644 index 0000000000000000000000000000000000000000..e76d8007ad270457b0e2c68be90d7a72e60735c7 --- /dev/null +++ b/docs/integrations/tracing.md @@ -0,0 +1,194 @@ +# Tracing + +Send agent traces to Kayba with a few lines of code. The `ace.tracing` module wraps all tracing functionality behind a Kayba-native API — just configure your API key and instrument your functions. + +## Installation + +```bash +pip install ace-framework[tracing] +``` + +## Quick Start + +```python +from ace.tracing import configure, trace, start_span + +configure(api_key="kb-...") + +@trace +def my_agent(query: str) -> str: + with start_span("retrieval") as span: + span.set_inputs({"query": query}) + results = search(query) + span.set_outputs(results) + return synthesize(results) + +my_agent("What is the capital of France?") +``` + +Every call to `my_agent` now produces a trace visible in your Kayba dashboard. + +## Configuration + +### configure() + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `api_key` | `str` | `None` | Kayba API key. Falls back to `KAYBA_API_KEY` env var | +| `base_url` | `str` | `None` | API base URL. Falls back to `KAYBA_API_URL`, then `https://use.kayba.ai` | +| `experiment` | `str` | `None` | Optional experiment name for grouping traces | +| `folder` | `str` | `None` | Optional folder name — traces are filed into this folder in the dashboard | + +### Environment Variables + +| Variable | Description | +|----------|-------------| +| `KAYBA_API_KEY` | API key (alternative to passing `api_key=` directly) | +| `KAYBA_API_URL` | Base URL override (default: `https://use.kayba.ai`) | + +### Minimal Configuration + +If `KAYBA_API_KEY` is set in your environment, configuration is a single line: + +```python +from ace.tracing import configure +configure() +``` + +Or skip the import entirely and configure from `ace`: + +```python +from ace import configure_tracing +configure_tracing(api_key="kb-...") +``` + +## Instrumenting Your Code + +### @trace decorator + +Wrap any function to automatically capture its inputs, outputs, and duration: + +```python +from ace.tracing import trace + +@trace +def classify(text: str) -> str: + return call_llm(f"Classify: {text}") +``` + +Add metadata with optional parameters: + +```python +@trace(name="custom-name", span_type="LLM", attributes={"model": "gpt-4o"}) +def classify(text: str) -> str: + return call_llm(f"Classify: {text}") +``` + +### start_span context manager + +For finer-grained control within a function: + +```python +from ace.tracing import trace, start_span + +@trace +def my_agent(query: str) -> str: + with start_span("retrieve") as span: + span.set_inputs({"query": query}) + docs = vector_search(query) + span.set_outputs({"count": len(docs)}) + + with start_span("generate") as span: + span.set_inputs({"docs": docs}) + answer = llm_generate(docs, query) + span.set_outputs({"answer": answer}) + + return answer +``` + +Spans nest automatically — child spans created inside a parent span are linked in the trace tree. + +### Nested function tracing + +Decorated functions called within other decorated functions produce a nested trace: + +```python +from ace.tracing import trace + +@trace +def retrieve(query: str) -> list[str]: + return vector_search(query) + +@trace +def generate(docs: list[str], query: str) -> str: + return llm_call(docs, query) + +@trace +def agent(query: str) -> str: + docs = retrieve(query) # child span + return generate(docs, query) # child span +``` + +Calling `agent("...")` produces a single trace with three spans in a tree. + +## Folders + +Traces can be organized into folders in the Kayba dashboard. Set the folder at configuration time or change it dynamically: + +```python +from ace.tracing import configure, set_folder, trace + +# Set folder at configure time +configure(api_key="kb-...", folder="production") + +@trace +def my_agent(query: str) -> str: + ... + +# Change folder mid-session +set_folder("staging") + +# Clear folder (traces go to Unfiled) +set_folder(None) +``` + +All traces created after `set_folder()` are tagged with the new folder. Previously sent traces are not affected. + +## Enabling / Disabling + +```python +from ace.tracing import enable, disable + +disable() # temporarily stop sending traces +# ... untraced code ... +enable() # resume +``` + +## Retrieving Traces + +```python +from ace.tracing import get_trace, search_traces + +# Fetch a specific trace by ID +t = get_trace("abc123") + +# Search recent traces +traces = search_traces() + +# Search within a specific experiment +traces = search_traces(experiment_names=["my-experiment"]) +``` + +## Full API Reference + +| Function | Description | +|----------|-------------| +| `configure()` | Set API key, base URL, experiment, and folder | +| `trace` | Decorator — auto-instruments a function | +| `start_span()` | Context manager — create a child span with manual inputs/outputs | +| `set_folder()` | Change the target folder for subsequent traces | +| `get_folder()` | Return the currently configured folder | +| `enable()` | Re-enable tracing after disabling | +| `disable()` | Temporarily stop sending traces | +| `get_trace()` | Retrieve a trace by ID | +| `search_traces()` | Search for traces by experiment | diff --git a/docs/pipeline/api-reference.md b/docs/pipeline/api-reference.md new file mode 100644 index 0000000000000000000000000000000000000000..0a1c67dd5ff21cc4e1ba9b14313f4b0b4b994716 --- /dev/null +++ b/docs/pipeline/api-reference.md @@ -0,0 +1,433 @@ +# API Reference + +Complete reference for all public classes, methods, and enums in the pipeline engine. + +--- + +## `pipeline.context` + +### `StepContext` + +Frozen dataclass passed from step to step. The pipeline engine only reads `sample` and `metadata` — domain-specific fields are added by subclassing. + +```python +@dataclass(frozen=True) +class StepContext: + sample: Any = None + metadata: MappingProxyType = field( + default_factory=lambda: MappingProxyType({}) + ) +``` + +| Method | Signature | Description | +|--------|-----------|-------------| +| `replace` | `(**changes: Any) -> StepContext` | Return a new context with the given fields replaced. Uses `dataclasses.replace` internally. | + +**Behavior:** + +- `metadata` is auto-coerced from `dict` to `MappingProxyType` in `__post_init__` +- Subclasses inherit `.replace()` — it works on all fields including subclass-defined ones + +--- + +## `pipeline.protocol` + +### `StepProtocol` + +Structural protocol that every step (and Pipeline/Branch) must satisfy. + +```python +@runtime_checkable +class StepProtocol(Protocol): + requires: AbstractSet[str] + provides: AbstractSet[str] + + def __call__(self, ctx: StepContext) -> StepContext: ... +``` + +| Attribute | Type | Description | +|-----------|------|-------------| +| `requires` | `AbstractSet[str]` | Metadata keys the step reads | +| `provides` | `AbstractSet[str]` | Metadata keys the step writes | +| `__call__` | `(StepContext) -> StepContext` | Execute the step | + +**Notes:** + +- `AbstractSet[str]` accepts both `set` and `frozenset` +- `@runtime_checkable` enables `isinstance(step, StepProtocol)` checks + +--- + +### `SampleResult` + +Outcome for one sample after the pipeline has run. + +```python +@dataclass +class SampleResult: + sample: Any + output: StepContext | None + error: Exception | None + failed_at: str | None + cause: Exception | None = None +``` + +| Field | Type | Description | +|-------|------|-------------| +| `sample` | `Any` | The original input sample | +| `output` | `StepContext \| None` | Final context (`None` if any step failed) | +| `error` | `Exception \| None` | The exception (`None` if succeeded) | +| `failed_at` | `str \| None` | Class name of the step that raised (`None` if succeeded) | +| `cause` | `Exception \| None` | Inner exception for `BranchError` failures (default `None`) | + +**Notes:** + +- Mutable — background threads update it in-place when background steps complete +- For background steps, `output`/`error` may be `None` until `wait_for_background()` completes + +--- + +## `pipeline.pipeline` + +### `Pipeline` + +Ordered sequence of steps. Satisfies `StepProtocol` — can be nested inside other pipelines. + +#### Constructor + +```python +Pipeline(steps: list | None = None, hooks: list[PipelineHook] | None = None) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `steps` | `list \| None` | `None` | Optional initial list of steps | +| `hooks` | `list[PipelineHook] \| None` | `None` | Observation-only hooks fired around each foreground step | + +Validates step ordering and infers contracts at construction time. + +#### Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `requires` | `frozenset[str]` | Fields the pipeline needs from external context (auto-inferred) | +| `provides` | `frozenset[str]` | Fields the pipeline writes (auto-inferred, union of all steps) | + +#### Methods + +##### `then` + +```python +def then(self, step: object) -> Pipeline +``` + +Append a step and return `self` for chaining. Validates ordering immediately. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `step` | `object` | Any object satisfying `StepProtocol` | + +**Returns:** `self` (for method chaining) + +**Raises:** `PipelineOrderError` if the step requires a field produced by a later step + +--- + +##### `branch` + +```python +def branch( + self, + *pipelines: object, + merge: MergeStrategy | Callable = MergeStrategy.RAISE_ON_CONFLICT, +) -> Pipeline +``` + +Append a `Branch` step and return `self` for chaining. Shorthand for `.then(Branch(*pipelines, merge=merge))`. + +**Returns:** `self` (for method chaining) + +--- + +##### `run` + +```python +def run( + self, + contexts: Iterable[StepContext], + workers: int = 1, + on_sample_done: Callable[[SampleResult], None] | None = None, + cancel_token: CancellationToken | None = None, +) -> list[SampleResult] +``` + +Process contexts through the pipeline (sync entry point). + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `contexts` | `Iterable[StepContext]` | — | Input contexts to process | +| `workers` | `int` | `1` | Max concurrent samples in foreground steps | +| `on_sample_done` | `Callable \| None` | `None` | Callback after each sample's foreground steps complete (or fail). Must not block. | +| `cancel_token` | `CancellationToken \| None` | `None` | Cancellation signal. Checked before each step and each new sample. Pass a fresh token per invocation. | + +**Returns:** `list[SampleResult]` — one result per input context + +**Notes:** Calls `asyncio.run(self.run_async(...))` internally. For background steps, call `wait_for_background()` after this returns. When `cancel_token` is provided, also sets `cancel_token_var` so code inside steps (e.g. LLM clients) can read it. + +--- + +##### `run_async` + +```python +async def run_async( + self, + contexts: Iterable[StepContext], + workers: int = 1, + on_sample_done: Callable[[SampleResult], None] | None = None, + cancel_token: CancellationToken | None = None, +) -> list[SampleResult] +``` + +Async entry point. Use `await pipe.run_async(contexts)` from coroutine contexts. + +Same parameters and return type as `run()`. + +--- + +##### `__call__` + +```python +def __call__(self, ctx: StepContext) -> StepContext +``` + +Run all steps sequentially on a single context. Used when the pipeline is nested as a step inside another pipeline. + +**Notes:** `async_boundary` markers are ignored in this mode — all steps run to completion. + +--- + +##### `wait_for_background` + +```python +def wait_for_background(self, timeout: float | None = None) -> None +``` + +Block until all background tasks complete. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `timeout` | `float \| None` | `None` | Max seconds to wait. `None` = wait indefinitely. | + +**Raises:** `TimeoutError` if timeout elapses before completion + +--- + +##### `background_stats` + +```python +def background_stats(self) -> dict[str, int] +``` + +Return a snapshot of background task progress. Thread-safe. + +**Returns:** `{"active": int, "completed": int}` + +--- + +## `pipeline.branch` + +### `MergeStrategy` + +Enum of built-in merge strategies for `Branch` outputs. + +```python +class MergeStrategy(Enum): + RAISE_ON_CONFLICT = "raise_on_conflict" + LAST_WRITE_WINS = "last_write_wins" + NAMESPACED = "namespaced" +``` + +| Value | Behavior | +|-------|----------| +| `RAISE_ON_CONFLICT` | Raises `ValueError` if two branches write different values to the same named field. Metadata merges with last-writer-wins. | +| `LAST_WRITE_WINS` | Last branch's value wins for every conflicting field. | +| `NAMESPACED` | Each branch's output stored at `metadata["branch_N"]`. No conflict possible. | + +--- + +### `Branch` + +Runs multiple pipelines in parallel, then merges their outputs. Satisfies `StepProtocol`. + +#### Constructor + +```python +Branch( + *pipelines: object, + merge: MergeStrategy | Callable = MergeStrategy.RAISE_ON_CONFLICT, +) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `*pipelines` | `object` | — | Child pipelines to run in parallel (at least one required) | +| `merge` | `MergeStrategy \| Callable` | `RAISE_ON_CONFLICT` | Merge strategy or custom `fn(list[StepContext]) -> StepContext` | + +**Raises:** `ValueError` if no pipelines are provided + +#### Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `requires` | `frozenset[str]` | Union of all children's requires | +| `provides` | `frozenset[str]` | Union of all children's provides | +| `pipelines` | `list` | The child pipelines | + +#### Methods + +##### `__call__` + +```python +def __call__(self, ctx: StepContext) -> StepContext +``` + +Sync fan-out via `ThreadPoolExecutor`. All branches run to completion before any failure is raised. + +**Raises:** `BranchError` if any branch fails + +--- + +##### `__call_async__` + +```python +async def __call_async__(self, ctx: StepContext) -> StepContext +``` + +Async fan-out via `asyncio.gather`. Sync children are wrapped with `asyncio.to_thread`. + +**Raises:** `BranchError` if any branch fails + +--- + +## `pipeline.protocol` — Hooks + +### `PipelineHook` + +Observation-only protocol fired around each foreground step. Hooks cannot modify context — both methods return `None`. + +```python +@runtime_checkable +class PipelineHook(Protocol): + def before_step(self, step_name: str, ctx: StepContext) -> None: ... + def after_step(self, step_name: str, ctx: StepContext) -> None: ... +``` + +| Method | Parameters | Description | +|--------|-----------|-------------| +| `before_step` | `step_name: str, ctx: StepContext` | Called before each foreground step executes | +| `after_step` | `step_name: str, ctx: StepContext` | Called after each foreground step completes | + +**Notes:** + +- `step_name` is `type(step).__name__` — hooks know what ran but cannot inspect or mutate the step instance +- Hooks fire for foreground steps only — background steps (after `async_boundary`) do not trigger hooks +- If a hook raises, the pipeline logs the error and continues — a broken hook never kills the pipeline +- For `Branch` steps, hooks fire once for `"Branch"` as a whole, not for inner steps + +--- + +## `pipeline.errors` + +### `CancellationToken` + +Thread-safe cancellation signal. Create a fresh token per `run()` invocation. + +```python +class CancellationToken: + def cancel(self) -> None: ... + + @property + def is_cancelled(self) -> bool: ... +``` + +| Method / Property | Description | +|-------------------|-------------| +| `cancel()` | Signal cancellation. Thread-safe, idempotent. | +| `is_cancelled` | `True` after `cancel()` has been called. | + +--- + +### `cancel_token_var` + +`ContextVar` set by `Pipeline.run_async()` so code inside steps (e.g. LLM clients) can read the current cancel token without parameter changes. + +```python +cancel_token_var: ContextVar[CancellationToken | None] # default: None +``` + +**Notes:** + +- Set before steps run, reset after `run_async()` completes +- `asyncio.to_thread()` copies contextvars automatically — visible in sync steps too +- Read with `cancel_token_var.get(None)` — returns `None` when no pipeline is running + +--- + +### `PipelineCancelled` + +```python +class PipelineCancelled(Exception): ... +``` + +A `cancel_token` was triggered. Surfaces in `SampleResult.error` — never propagated to the caller of `run()`. Callers check `isinstance(result.error, PipelineCancelled)` to distinguish cancellation from step failures. + +--- + +### `PipelineOrderError` + +```python +class PipelineOrderError(Exception): ... +``` + +A step requires a field that no earlier step provides (but a later step does). Raised at **construction time**. + +--- + +### `PipelineConfigError` + +```python +class PipelineConfigError(Exception): ... +``` + +Invalid pipeline wiring. Raised at **construction time**. Examples: + +- More than one `async_boundary = True` step in the same pipeline +- An `async_boundary = True` step inside a `Branch` child + +--- + +### `BranchError` + +```python +class BranchError(Exception): + failures: list[BaseException] +``` + +One or more branch pipelines failed. All branches always run to completion before this is raised. Raised at **runtime**. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `failures` | `list[BaseException]` | One exception per failed branch | + +--- + +## Step class attributes + +Optional attributes a step class can declare to control pipeline behavior: + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `requires` | `set[str] \| frozenset[str]` | *(required)* | Metadata keys the step reads | +| `provides` | `set[str] \| frozenset[str]` | *(required)* | Metadata keys the step writes | +| `async_boundary` | `bool` | `False` | Marks the foreground/background split point | +| `max_workers` | `int` | `1` | Max concurrent background threads for this step class | diff --git a/docs/pipeline/branching.md b/docs/pipeline/branching.md new file mode 100644 index 0000000000000000000000000000000000000000..22af8d676dcf5aad2bcda8dece0aed1c3980f192 --- /dev/null +++ b/docs/pipeline/branching.md @@ -0,0 +1,218 @@ +# Branching & Parallelism + +A `Branch` runs multiple pipelines in parallel on the same input, then merges their outputs before the next step. It is itself a step — it satisfies `StepProtocol` and can be used anywhere a step is expected. + +--- + +## What is a Branch? + +```mermaid +graph LR + T[Tokenize] --> U[Uppercase] + T --> R[Reverse] + U --> S[Summarize] + R --> S +``` + +The branch forks the context to both child pipelines, runs them in parallel, and joins (merges) the results before `Summarize` runs. The join is implicit — any step after a `Branch` waits for all branches to complete. + +--- + +## Creating branches + +Two equivalent APIs: + +=== "Fluent shorthand" + + ```python + pipe = ( + Pipeline() + .then(Tokenize()) + .branch( + Pipeline().then(Uppercase()), + Pipeline().then(Reverse()), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + .then(Summarize()) + ) + ``` + +=== "Explicit Branch" + + ```python + from pipeline import Branch, MergeStrategy + + branch_step = Branch( + Pipeline().then(Uppercase()), + Pipeline().then(Reverse()), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + + pipe = ( + Pipeline() + .then(Tokenize()) + .then(branch_step) + .then(Summarize()) + ) + ``` + +--- + +## Merge strategies + +When all branches complete, their output contexts must be merged back into one. The `merge` parameter controls how conflicts are resolved. + +### `RAISE_ON_CONFLICT` (default) + +Raises `ValueError` if two branches write different values to the same named field. Disjoint fields pass through without conflict. + +```python +pipe = ( + Pipeline() + .then(Tokenize()) + .branch( + Pipeline().then(Uppercase()), # provides: {"upper_tokens"} + Pipeline().then(Reverse()), # provides: {"reversed_tokens"} + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) +) + +# Works — branches write to different fields +results = pipe.run([StepContext(sample="hello world")]) +``` + +!!! tip + In practice, branches that write disjoint fields (which is the common case) never conflict and the merge is a no-op. + +### `LAST_WRITE_WINS` + +The last branch's value wins for every conflicting field. Simple but lossy. + +```python +pipe = Pipeline().then(Tokenize()).branch( + Pipeline().then(Uppercase()), + Pipeline().then(Reverse()), + merge=MergeStrategy.LAST_WRITE_WINS, +) +``` + +### `NAMESPACED` + +Each branch's output is stored at `metadata["branch_0"]`, `metadata["branch_1"]`, etc. No conflict is possible. + +```python +pipe = Pipeline().then(Tokenize()).branch( + Pipeline().then(Uppercase()), + Pipeline().then(Reverse()), + merge=MergeStrategy.NAMESPACED, +) + +results = pipe.run([StepContext(sample="hello world")]) +meta = results[0].output.metadata +print(meta["branch_0"]) # context from Uppercase branch +print(meta["branch_1"]) # context from Reverse branch +``` + +### Custom merge function + +For full control, pass a callable: + +```python +def priority_merge(ctxs: list[StepContext]) -> StepContext: + """First branch wins for all fields.""" + base = ctxs[0] + merged_meta = dict(base.metadata) + for ctx in ctxs[1:]: + for k, v in ctx.metadata.items(): + merged_meta.setdefault(k, v) # first writer wins + return base.replace(metadata=MappingProxyType(merged_meta)) + +pipe = Pipeline().then(Tokenize()).branch( + Pipeline().then(Uppercase()), + Pipeline().then(Reverse()), + merge=priority_merge, +) +``` + +--- + +## How context flows through branches + +1. All branches receive the **same frozen context** — no copy needed since `StepContext` is immutable +2. Each branch runs its pipeline and returns a **new** context +3. The merge function receives the list of output contexts and returns a single merged context +4. The merged context is passed to the next step in the outer pipeline + +Immutability is what makes this safe. No branch can corrupt another branch's input. + +--- + +## Execution model + +=== "Sync" + + Branches run in a `ThreadPoolExecutor` with `max_workers=len(pipelines)`: + + ```python + # All branches get their own thread + with ThreadPoolExecutor(max_workers=len(self.pipelines)) as executor: + futures = [executor.submit(p, ctx) for p in self.pipelines] + results = [f.result() for f in futures] + return self._merge_fn(results) + ``` + +=== "Async" + + Branches run via `asyncio.gather`. Sync child pipelines are wrapped with `asyncio.to_thread`: + + ```python + results = await asyncio.gather( + *[run_child(p) for p in self.pipelines], + return_exceptions=True, + ) + return self._merge_fn(results) + ``` + +In both cases, all branches run to completion even if one fails. + +--- + +## Error handling in branches + +When one or more branches fail, a `BranchError` is raised — but only after **all** branches have completed: + +```python +from pipeline.errors import BranchError + +try: + results = pipe.run(contexts) +except BranchError as e: + print(f"{len(e.failures)} branch(es) failed") + for failure in e.failures: + print(f" - {type(failure).__name__}: {failure}") +``` + +- `BranchError.failures` contains one exception per failed branch +- Successful branches are not lost — their contexts are still available +- `SampleResult.failed_at` is set to `"Branch"` and `SampleResult.cause` carries the inner exception + +See [Error Handling](error-handling.md) for the full error model. + +--- + +## Contract inference + +A `Branch` computes its own `requires` and `provides` from the union of its children: + +```python +branch = Branch( + Pipeline().then(Uppercase()), # requires: {"tokens"}, provides: {"upper_tokens"} + Pipeline().then(Reverse()), # requires: {"tokens"}, provides: {"reversed_tokens"} +) + +# Inferred: +# branch.requires = {"tokens"} +# branch.provides = {"upper_tokens", "reversed_tokens"} +``` + +The outer pipeline validates against these aggregated contracts at construction time, so nesting branches inside pipelines works seamlessly. diff --git a/docs/pipeline/core-concepts.md b/docs/pipeline/core-concepts.md new file mode 100644 index 0000000000000000000000000000000000000000..5cc37c747f600529c3110695ab89dba7a4a65d2d --- /dev/null +++ b/docs/pipeline/core-concepts.md @@ -0,0 +1,272 @@ +# Core Concepts + +The pipeline engine is built on four foundational concepts: the **Step protocol**, the **StepContext**, the **contract system**, and the **Pipeline** compositor. Understanding these gives you the mental model for everything else. + +--- + +## StepProtocol + +A Step is any Python object that satisfies the `StepProtocol` — a structural (duck-typed) interface. No base class is required. + +```python +from collections.abc import Set as AbstractSet +from typing import Protocol, runtime_checkable + +@runtime_checkable +class StepProtocol(Protocol): + requires: AbstractSet[str] # metadata keys this step reads + provides: AbstractSet[str] # metadata keys this step writes + + def __call__(self, ctx: StepContext) -> StepContext: ... +``` + +Any object with `requires`, `provides`, and a `__call__` method is a valid step: + +```python +class Tokenize: + requires = frozenset() # no dependencies + provides = frozenset({"tokens", "word_count"}) + + def __call__(self, ctx: StepContext) -> StepContext: + tokens = str(ctx.sample).split() + return ctx.replace( + metadata=MappingProxyType({ + **ctx.metadata, + "tokens": tokens, + "word_count": len(tokens), + }) + ) +``` + +Key details: + +- **`AbstractSet[str]`** accepts both `set` and `frozenset`. Steps can use plain set literals — the pipeline normalizes them to `frozenset` at construction time. +- **`@runtime_checkable`** lets the pipeline use `isinstance(step, StepProtocol)` at construction time to catch missing attributes early, rather than failing at call time. +- **`Pipeline` and `Branch`** both satisfy this protocol, so they can be nested wherever a step is expected. + +--- + +## StepContext + +`StepContext` is the data carrier passed from step to step. It is a **frozen dataclass** — steps never mutate the incoming context. + +```python +from dataclasses import dataclass, field +from types import MappingProxyType + +@dataclass(frozen=True) +class StepContext: + sample: Any = None + metadata: MappingProxyType = field( + default_factory=lambda: MappingProxyType({}) + ) + + def replace(self, **changes) -> "StepContext": + return dataclasses.replace(self, **changes) +``` + +The engine only reads `sample` and `metadata`. All domain-specific fields are added by subclassing. + +### The `.replace()` pattern + +Steps create new contexts — they never mutate the incoming one: + +```python +def __call__(self, ctx: StepContext) -> StepContext: + result = process(ctx.sample) + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "result": result}) + ) +``` + +This is the only way to "modify" a context. `frozen=True` makes mutation a hard error at runtime rather than a subtle bug. + +### Metadata auto-coercion + +If a caller passes a plain `dict` as metadata, `StepContext.__post_init__` automatically wraps it in `MappingProxyType`, ensuring mutation is always a runtime error: + +```python +# Both of these produce identical immutable metadata: +ctx = StepContext(sample="hello", metadata={"key": "value"}) +ctx = StepContext(sample="hello", metadata=MappingProxyType({"key": "value"})) +``` + +### Subclassing for domain fields + +Applications subclass `StepContext` to add named fields for concepts shared across their pipelines: + +```python +@dataclass(frozen=True) +class MLContext(StepContext): + # Shared configuration + model_config: dict | None = None + + # Produced by steps (None until the providing step runs) + predictions: list | None = None + scores: dict | None = None + report: str | None = None +``` + +Use **named fields** for data shared across multiple steps in the pipeline. Use **`metadata`** for integration-specific or step-specific transient data that doesn't warrant a dedicated field. + +!!! tip "When to use which" + - Named field: `predictions`, `scores` — shared by multiple steps, type-checkable + - Metadata: `metadata["debug_log"]`, `metadata["cache_key"]` — step-specific, doesn't pollute the class + +### Why immutability? + +- **Branch safety** — All branches receive the same frozen context. No deep copy is needed since no branch can mutate what it receives. +- **Thread safety** — Steps running concurrently (via `workers` or `Branch`) can safely share context objects. +- **Debugging** — Each step returns a new context, creating a clear trace of data transformations. + +--- + +## Contracts: requires and provides + +Every step declares: + +- **`requires`** — the set of field names it reads from the context +- **`provides`** — the set of field names it writes to the context + +The pipeline validates these at construction time. + +### How validation works + +When you build a pipeline with `.then()`, the engine checks step ordering immediately: + +```python +pipe = ( + Pipeline() + .then(Tokenize()) # provides: {"tokens", "word_count"} + .then(Uppercase()) # requires: {"tokens"} ✓ — Tokenize provides it +) +``` + +If a step requires a field that a **later** step provides, the pipeline raises `PipelineOrderError`: + +```python +# This raises PipelineOrderError at construction time: +pipe = Pipeline().then(Uppercase()).then(Tokenize()) +# ↑ Uppercase requires "tokens", but Tokenize (which provides it) comes after +``` + +### External inputs vs internal dependencies + +Fields not produced by any step in the pipeline are treated as **external inputs** — they must be present in the initial `StepContext` passed to `run()`. These do not trigger ordering errors: + +```python +class ScoreStep: + requires = frozenset({"predictions"}) # external input + provides = frozenset({"scores"}) + +# No error — "predictions" is expected to come from the initial context +pipe = Pipeline().then(ScoreStep()) +``` + +### Contract inference for nested pipelines + +When a `Pipeline` is used as a step inside another pipeline, its `requires` and `provides` are computed automatically from its inner steps: + +```python +inner = Pipeline().then(Tokenize()).then(Uppercase()) + +# Inferred automatically: +# inner.requires = frozenset() — Tokenize needs nothing external +# inner.provides = frozenset({"tokens", "word_count", "upper_tokens"}) + +outer = Pipeline().then(inner).then(Summarize()) +# Summarize's requirements validated against inner.provides +``` + +The inference algorithm: + +1. Walk steps in order, tracking what has been provided so far +2. `requires` = fields needed by steps that no earlier step provides (external dependencies) +3. `provides` = union of all fields any step writes + +!!! warning "All Branch children always run" + The contract system assumes all `Branch` children execute. There is no concept of conditional branches where only some children run — all branches always run. If a branch provides a field that a later step requires, validation passes; if that branch were to not run, the pipeline would fail at runtime. + +--- + +## Pipeline + +A `Pipeline` is an ordered list of steps that runs sequentially for a single input. It satisfies the `StepProtocol`, so it can be nested inside other pipelines. + +### Building a pipeline + +Two equivalent forms: + +=== "Fluent builder (preferred)" + + ```python + pipe = ( + Pipeline() + .then(Tokenize()) + .then(Uppercase()) + .then(Summarize()) + ) + ``` + +=== "Constructor list" + + ```python + pipe = Pipeline([ + Tokenize(), + Uppercase(), + Summarize(), + ]) + ``` + +Both validate step ordering at construction time. The fluent builder validates **after each `.then()` call**, giving precise error messages about which step caused the violation. + +### The `.branch()` shorthand + +Instead of manually creating a `Branch`, use the fluent shorthand: + +```python +pipe = ( + Pipeline() + .then(Tokenize()) + .branch( + Pipeline().then(Uppercase()), + Pipeline().then(Reverse()), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + .then(Summarize()) +) +``` + +This is equivalent to `.then(Branch(...))`. + +### Nesting + +A pipeline used as a step is a black box — the outer pipeline sees only its aggregated `requires` and `provides`: + +```python +preprocessing = Pipeline().then(Tokenize()).then(Uppercase()) +postprocessing = Pipeline().then(Summarize()).then(FormatStep()) + +full = Pipeline().then(preprocessing).then(postprocessing) +``` + +!!! note "Inner pipeline as a fan-out step" + A step receives one context and must return one context — but nothing prevents it from internally expanding to multiple sub-inputs: + + ```python + class MultiSearchStep: + requires = frozenset() + provides = frozenset({"search_results"}) + + def __call__(self, ctx: StepContext) -> StepContext: + queries = generate_queries(ctx.sample) + sub_ctxs = [StepContext(sample=q) for q in queries] + sub_pipe = Pipeline().then(FetchStep()) + results = sub_pipe.run(sub_ctxs, workers=len(queries)) + merged = merge_results(results) + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "search_results": merged}) + ) + ``` + + From the outer pipeline's perspective, `MultiSearchStep` is a single step. The fan-out is an internal implementation detail. diff --git a/docs/pipeline/custom-steps.md b/docs/pipeline/custom-steps.md new file mode 100644 index 0000000000000000000000000000000000000000..48e0a3fa4ddb50dc4f755a79a2d5ac33530e51dc --- /dev/null +++ b/docs/pipeline/custom-steps.md @@ -0,0 +1,311 @@ +# Building Custom Steps + +This guide covers everything you need to create your own pipeline steps — from the minimal contract to advanced patterns like dependency injection, async execution, and testing. + +--- + +## The step contract + +Any Python object with `requires`, `provides`, and `__call__` is a valid step. No base class needed. + +```python +from types import MappingProxyType +from pipeline import StepContext + + +class MyStep: + requires = frozenset({"input_field"}) + provides = frozenset({"output_field"}) + + def __call__(self, ctx: StepContext) -> StepContext: + result = process(ctx.metadata["input_field"]) + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "output_field": result}) + ) +``` + +Rules: + +- `requires` and `provides` can be `set` or `frozenset` — the pipeline normalizes to `frozenset` +- `__call__` receives a `StepContext` and must return a `StepContext` +- Never mutate the incoming context — always use `.replace()` + +--- + +## Sync vs async steps + +=== "Sync" + + ```python + class ComputeStep: + requires = frozenset({"data"}) + provides = frozenset({"result"}) + + def __call__(self, ctx: StepContext) -> StepContext: + result = expensive_computation(ctx.metadata["data"]) + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "result": result}) + ) + ``` + +=== "Async" + + ```python + class FetchStep: + requires = frozenset({"url"}) + provides = frozenset({"response"}) + + async def __call__(self, ctx: StepContext) -> StepContext: + async with aiohttp.ClientSession() as session: + resp = await session.get(ctx.metadata["url"]) + data = await resp.json() + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "response": data}) + ) + ``` + +Use async steps for I/O-bound work (HTTP requests, API calls, file I/O). The pipeline detects and handles both transparently. + +--- + +## Dependency injection + +Steps that need external collaborators receive them via `__init__`. The `__call__` method stays stateless — it only uses `self.*` for injected dependencies and `ctx` for data. + +```python +class ScoringStep: + requires = frozenset({"predictions"}) + provides = frozenset({"scores"}) + + def __init__(self, scorer, threshold: float = 0.5): + self.scorer = scorer + self.threshold = threshold + + def __call__(self, ctx: StepContext) -> StepContext: + raw_scores = self.scorer.evaluate(ctx.metadata["predictions"]) + filtered = {k: v for k, v in raw_scores.items() if v >= self.threshold} + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "scores": filtered}) + ) +``` + +This makes testing easy — inject mocks: + +```python +pipe = Pipeline().then(ScoringStep(scorer=mock_scorer, threshold=0.8)) +``` + +--- + +## Declaring concurrency + +Two optional class attributes control how a step participates in concurrent execution: + +### `async_boundary` + +Marks the foreground/background split point. Everything from this step onward runs in a background thread: + +```python +class AnalyzeStep: + requires = frozenset({"data"}) + provides = frozenset({"analysis"}) + async_boundary = True # background from here + + def __call__(self, ctx: StepContext) -> StepContext: ... +``` + +See [Execution Model — Async Boundary](execution.md#async-boundary-fire-and-forget-background) for details. + +### `max_workers` + +Controls the per-step-class thread pool size for background execution: + +```python +class ParallelAnalyzeStep: + requires = frozenset({"data"}) + provides = frozenset({"analysis"}) + async_boundary = True + max_workers = 4 # up to 4 concurrent analyses + + def __call__(self, ctx: StepContext) -> StepContext: ... +``` + +Default is `max_workers = 1` (serialized). + +!!! warning + Steps that write shared state (e.g. updating an external database or accumulating results into a shared object) must use `max_workers = 1` to avoid race conditions. + +--- + +## Subclassing StepContext + +When `metadata` becomes unwieldy, subclass `StepContext` to add named fields: + +```python +from dataclasses import dataclass + +@dataclass(frozen=True) +class MLContext(StepContext): + predictions: list | None = None + scores: dict | None = None + report: str | None = None +``` + +Steps write to named fields using `.replace()`: + +```python +class PredictStep: + requires = frozenset() + provides = frozenset({"predictions"}) + + def __init__(self, model): + self.model = model + + def __call__(self, ctx: MLContext) -> MLContext: + preds = self.model.predict(ctx.sample) + return ctx.replace(predictions=preds) +``` + +!!! tip "When to subclass" + - **Named fields**: Data shared across multiple steps that benefits from type checking + - **Metadata**: Step-specific or integration-specific transient data (e.g. `metadata["cache_key"]`) + + The `requires`/`provides` validation works on attribute names, so it's subclass-agnostic. A step declaring `requires = {"predictions"}` works with any context subclass that has a `predictions` attribute. + +--- + +## Testing steps + +### Unit test — step in isolation + +```python +from types import MappingProxyType +from pipeline import StepContext + + +def test_tokenize_splits_words(): + step = Tokenize() + ctx = StepContext(sample="hello world") + + result = step(ctx) + + assert result.metadata["tokens"] == ["hello", "world"] + assert result.metadata["word_count"] == 2 + + +def test_uppercase_transforms_tokens(): + step = Uppercase() + ctx = StepContext( + metadata=MappingProxyType({"tokens": ["hello", "world"]}) + ) + + result = step(ctx) + + assert result.metadata["upper_tokens"] == ["HELLO", "WORLD"] +``` + +### Protocol compliance + +```python +from pipeline import StepProtocol + + +def test_step_satisfies_protocol(): + step = Tokenize() + assert isinstance(step, StepProtocol) + assert hasattr(step, "requires") + assert hasattr(step, "provides") + assert callable(step) +``` + +### Pipeline integration test + +```python +from pipeline import Pipeline, StepContext + + +def test_full_pipeline(): + pipe = Pipeline().then(Tokenize()).then(Uppercase()) + results = pipe.run([StepContext(sample="hello world")]) + + assert len(results) == 1 + assert results[0].error is None + assert results[0].output.metadata["upper_tokens"] == ["HELLO", "WORLD"] +``` + +--- + +## Common patterns + +### Map-reduce step + +A step that internally fans out to multiple sub-inputs: + +```python +class MultiSearchStep: + requires = frozenset() + provides = frozenset({"search_results"}) + + def __call__(self, ctx: StepContext) -> StepContext: + queries = generate_queries(ctx.sample) # 1 → N + sub_ctxs = [StepContext(sample=q) for q in queries] + sub_pipe = Pipeline().then(FetchStep()) + results = sub_pipe.run(sub_ctxs, workers=len(queries)) # parallel + merged = merge_results(results) # N → 1 + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "search_results": merged}) + ) +``` + +From the outer pipeline's perspective, this is a black box that takes one context and returns one. + +### Logging / observability step + +A pass-through step that logs without modifying data: + +```python +class LogStep: + requires = frozenset() + provides = frozenset() + + def __init__(self, logger): + self.logger = logger + + def __call__(self, ctx: StepContext) -> StepContext: + self.logger.info(f"Processing sample: {ctx.sample}") + self.logger.debug(f"Metadata keys: {list(ctx.metadata.keys())}") + return ctx # pass through unchanged +``` + +### Retry wrapper + +A step that wraps another step with retry logic: + +```python +import time + + +class RetryStep: + def __init__(self, inner, max_retries: int = 3, delay: float = 1.0): + self.inner = inner + self.max_retries = max_retries + self.delay = delay + self.requires = inner.requires + self.provides = inner.provides + + def __call__(self, ctx: StepContext) -> StepContext: + for attempt in range(self.max_retries): + try: + return self.inner(ctx) + except Exception: + if attempt == self.max_retries - 1: + raise + time.sleep(self.delay * (attempt + 1)) +``` + +Usage: + +```python +pipe = Pipeline().then(RetryStep(FlakyAPIStep(), max_retries=3)) +``` + diff --git a/docs/pipeline/error-handling.md b/docs/pipeline/error-handling.md new file mode 100644 index 0000000000000000000000000000000000000000..8308e58a6ba35f58542eb487102efcdbbc8187a6 --- /dev/null +++ b/docs/pipeline/error-handling.md @@ -0,0 +1,248 @@ +# Error Handling + +The pipeline engine guarantees that every sample produces a `SampleResult` — nothing is dropped silently. One failing sample never blocks others. Retry logic is the responsibility of individual steps, not the pipeline. + +--- + +## SampleResult + +Every sample that enters `run()` produces exactly one `SampleResult`: + +```python +@dataclass +class SampleResult: + sample: Any # the original input + output: StepContext | None # final context (None if failed) + error: Exception | None # the exception (None if succeeded) + failed_at: str | None # step class name where error occurred + cause: Exception | None = None # inner exception for BranchError +``` + +| Field | On success | On failure | +|-------|-----------|------------| +| `sample` | original input | original input | +| `output` | final `StepContext` | `None` | +| `error` | `None` | the exception | +| `failed_at` | `None` | class name of the failing step (e.g. `"Tokenize"`) | +| `cause` | `None` | inner exception when `failed_at == "Branch"` | + +!!! note "Background steps" + For steps after an `async_boundary`, `output` and `error` may still be `None` when `run()` returns. Call `pipe.wait_for_background()` to block until all background work completes and results are finalized. + +--- + +## Construction-time errors + +These are caught **before any data flows** — they surface immediately when you build the pipeline. + +### PipelineOrderError + +Raised when a step requires a field that is produced by a **later** step in the pipeline: + +```python +from pipeline import Pipeline +from pipeline.errors import PipelineOrderError + +class Uppercase: + requires = frozenset({"tokens"}) + provides = frozenset({"upper_tokens"}) + def __call__(self, ctx): ... + +class Tokenize: + requires = frozenset() + provides = frozenset({"tokens"}) + def __call__(self, ctx): ... + +try: + Pipeline().then(Uppercase()).then(Tokenize()) +except PipelineOrderError as e: + print(e) + # Uppercase requires {"tokens"} but it is provided by a later step +``` + +!!! tip + `PipelineOrderError` is always a bug — reorder your steps. Fields not produced by **any** step in the pipeline are treated as external inputs and do not trigger this error. + +### PipelineConfigError + +Raised for invalid pipeline wiring: + +**Multiple async boundaries:** + +```python +from pipeline.errors import PipelineConfigError + +class StepA: + requires = frozenset() + provides = frozenset({"a"}) + async_boundary = True + def __call__(self, ctx): ... + +class StepB: + requires = frozenset({"a"}) + provides = frozenset({"b"}) + async_boundary = True + def __call__(self, ctx): ... + +try: + Pipeline().then(StepA()).then(StepB()) +except PipelineConfigError: + print("Only one async_boundary per pipeline is allowed") +``` + +**Async boundary inside a Branch child:** + +```python +try: + Pipeline().branch( + Pipeline().then(StepA()), # async_boundary = True inside branch + ) +except PipelineConfigError: + print("async_boundary inside Branch children is not allowed") +``` + +--- + +## Runtime errors + +### Foreground failures + +When a step before the `async_boundary` (or in a pipeline with no boundary) raises an exception, the pipeline catches it per-sample and records it in the `SampleResult`: + +```python +class Boom: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx): + raise RuntimeError(f"Failed on {ctx.sample!r}") + +pipe = Pipeline().then(Tokenize()).then(Boom()) + +results = pipe.run([ + StepContext(sample="good"), + StepContext(sample="also good"), +]) + +for r in results: + if r.error: + print(f"Sample '{r.sample}' failed at {r.failed_at}: {r.error}") + else: + print(f"Sample '{r.sample}' succeeded") +``` + +``` +Sample 'good' failed at Boom: Failed on 'good' +Sample 'also good' failed at Boom: Failed on 'also good' +``` + +Each sample is processed independently — one failure does not prevent others from running. + +### Background failures + +When a step **after** the `async_boundary` raises, the caller has already moved on. The exception is captured and attached to the `SampleResult` in-place: + +```python +pipe = Pipeline().then(Tokenize()).then(BrokenBackgroundStep()) + +results = pipe.run(samples) +# results returned immediately — background still running + +pipe.wait_for_background(timeout=10.0) + +# Now check for background failures +for r in results: + if r.error: + print(f"Background failure at {r.failed_at}: {r.error}") +``` + +### PipelineCancelled + +When a `cancel_token` is triggered, remaining steps and samples are cancelled with `PipelineCancelled`: + +```python +from pipeline import CancellationToken, PipelineCancelled + +token = CancellationToken() +# ... later, from another thread or endpoint: +token.cancel() + +results = pipe.run(contexts, cancel_token=token) + +for r in results: + if isinstance(r.error, PipelineCancelled): + print(f"Sample '{r.sample}' was cancelled before {r.failed_at}") + elif r.error: + print(f"Sample '{r.sample}' failed at {r.failed_at}: {r.error}") +``` + +Cancellation is checked **between** steps — a running step always completes. `PipelineCancelled` follows the same `SampleResult` pattern as step errors: it is caught per-sample, not propagated. + +### BranchError + +When one or more branch pipelines fail, a `BranchError` is raised with the full list of failures: + +```python +from pipeline.errors import BranchError + +results = pipe.run(contexts) + +for r in results: + if isinstance(r.error, BranchError): + print(f"{len(r.error.failures)} branch(es) failed:") + for f in r.error.failures: + print(f" {type(f).__name__}: {f}") + elif r.error: + print(f"Step failure at {r.failed_at}: {r.error}") +``` + +All branches run to completion before `BranchError` is raised — no branch is cancelled when another fails. The `SampleResult.cause` field carries the inner exception from the failing branch. + +--- + +## Inspecting results + +The standard pattern after `run()`: + +```python +results = pipe.run(contexts) +pipe.wait_for_background() # if using async_boundary + +succeeded = [r for r in results if r.error is None] +failed = [r for r in results if r.error is not None] + +print(f"{len(succeeded)} succeeded, {len(failed)} failed") + +for r in failed: + print(f" Sample: {r.sample}") + print(f" Failed at: {r.failed_at}") + print(f" Error: {r.error}") +``` + +--- + +## Background monitoring + +### `wait_for_background()` + +Blocks until all background tasks complete: + +```python +# Wait indefinitely +pipe.wait_for_background() + +# Wait with timeout — raises TimeoutError if not done +pipe.wait_for_background(timeout=30.0) +``` + +Completed threads are removed from the tracking list after this call. + +### `background_stats()` + +Returns a snapshot of background task progress. Thread-safe — can be called from any thread while the pipeline is running: + +```python +stats = pipe.background_stats() +print(stats) +# {'active': 2, 'completed': 8} +``` diff --git a/docs/pipeline/execution.md b/docs/pipeline/execution.md new file mode 100644 index 0000000000000000000000000000000000000000..ea0156066bfd2fd3997e09e2c619d6c079a43c3d --- /dev/null +++ b/docs/pipeline/execution.md @@ -0,0 +1,219 @@ +# Execution Model + +"Async" means three different things in this framework. They operate at different levels and solve different problems. Keeping them separate is key to understanding the concurrency model. + +--- + +## Three types of concurrency + +| Type | Level | Problem it solves | +|------|-------|-------------------| +| **Async steps** | single step | Don't block the thread during I/O | +| **`async_boundary`** | across samples | Start the next sample before the current one finishes | +| **Branch parallelism** | within one sample | Run independent work simultaneously on the same data | + +Each mechanism is independent. They compose freely — you can have async steps inside branches, behind an `async_boundary`, run with multiple workers. + +--- + +## Entry points: `run()` and `run_async()` + +=== "Sync" + + ```python + # For regular (non-async) callers + results = pipe.run(contexts, workers=4) + ``` + +=== "Async" + + ```python + # For async callers (e.g. inside an async framework) + results = await pipe.run_async(contexts, workers=4) + ``` + +=== "With cancellation" + + ```python + from pipeline import CancellationToken + + token = CancellationToken() + results = pipe.run(contexts, workers=4, cancel_token=token) + # Call token.cancel() from another thread to stop processing + ``` + +`run()` is a thin wrapper that calls `asyncio.run(self.run_async(...))`. Both accept the same parameters and return `list[SampleResult]`. + +Optional parameters: `on_sample_done` (callback after each sample), `cancel_token` (stop between steps). See [API Reference](api-reference.md) for full signatures. + +--- + +## Workers: sample-level parallelism + +The `workers` parameter on `run()` / `run_async()` controls how many samples are processed through foreground steps simultaneously: + +```python +import time +from pipeline import Pipeline, StepContext + + +class SlowStep: + requires = frozenset() + provides = frozenset({"result"}) + + def __call__(self, ctx: StepContext) -> StepContext: + time.sleep(0.1) # Simulate expensive work + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "result": "done"}) + ) + + +pipe = Pipeline().then(SlowStep()) +samples = [StepContext(sample=f"s{i}") for i in range(6)] + +# Sequential: 6 × 0.1s ≈ 0.6s +results = pipe.run(samples, workers=1) + +# Parallel: 0.1s (all 6 run at once) +results = pipe.run(samples, workers=6) +``` + +Under the hood, `workers` creates an `asyncio.Semaphore` — at most N samples flow through the foreground steps at any given time. + +--- + +## Async steps — non-blocking I/O + +A step that makes network calls (HTTP requests, API calls, subprocess) can be defined as a coroutine to avoid blocking the thread: + +=== "Sync step" + + ```python + class FetchStep: + requires = frozenset() + provides = frozenset({"response"}) + + def __call__(self, ctx: StepContext) -> StepContext: + response = requests.get(ctx.sample) # blocks the thread + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "response": response}) + ) + ``` + +=== "Async step" + + ```python + class FetchStep: + requires = frozenset() + provides = frozenset({"response"}) + + async def __call__(self, ctx: StepContext) -> StepContext: + async with aiohttp.ClientSession() as session: + response = await session.get(ctx.sample) # yields the thread + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "response": response}) + ) + ``` + +The pipeline detects async steps automatically via `asyncio.iscoroutinefunction` and awaits them. Sync steps are wrapped with `asyncio.to_thread()` so they're safe in an async context too. + +!!! note + Async steps are about **not blocking the thread**, not about parallelism. The pipeline is still sequential — it just yields the thread during I/O waits. + +--- + +## Async boundary — fire-and-forget background + +**Problem:** Some steps are slow (e.g. LLM calls for analysis). Waiting for them before starting the next sample hurts throughput. + +**Solution:** A step declares `async_boundary = True`. Everything from that step onward runs in a background thread. The pipeline loop moves to the next sample immediately. + +```python +class SlowScoreStep: + requires = frozenset({"tokens"}) + provides = frozenset({"score"}) + async_boundary = True # hand off to background from here + max_workers = 3 # up to 3 scoring threads in parallel + + def __call__(self, ctx: StepContext) -> StepContext: + time.sleep(0.5) # Expensive scoring + score = len(ctx.metadata["tokens"]) * 10 + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "score": score}) + ) +``` + +```mermaid +graph LR + A1[Tokenize] --> B1[Uppercase] -->|async_boundary| C1[SlowScore] + + style A1 fill:#6366f1,stroke:#4f46e5,color:#fff + style B1 fill:#6366f1,stroke:#4f46e5,color:#fff + style C1 fill:#3b82f6,stroke:#2563eb,color:#fff +``` + +> **Indigo** = foreground (returns immediately) · **Blue** = background (fire-and-forget) + +Multiple samples flow through this simultaneously — sample 2 starts its foreground steps while sample 1's background steps are still running. + +### Using the boundary + +```python +pipe = Pipeline().then(Tokenize()).then(Uppercase()).then(SlowScoreStep()) + +# run() returns immediately after foreground steps (Tokenize + Uppercase) +results = pipe.run(samples, workers=4) + +# Background scoring continues — results not yet populated +print(pipe.background_stats()) +# {'active': 3, 'completed': 1} + +# Block until all background work finishes +pipe.wait_for_background(timeout=30.0) + +# Now all SampleResult.output fields are fully populated +for r in results: + print(r.output.metadata["score"]) +``` + +### Background pool model + +Each step **class** has a single shared `ThreadPoolExecutor`: + +- `SlowScoreStep.max_workers = 3` means one pool of 3 threads for all `SlowScoreStep` instances, regardless of how many pipelines are running +- The pool is created lazily at first use and persists for the process lifetime +- If two users need different concurrency limits for the same step type, they should subclass + +!!! warning "Boundary rules" + - **One boundary per pipeline.** If multiple steps declare `async_boundary = True`, the pipeline raises `PipelineConfigError` at construction time. + - **No boundary inside Branch children.** A boundary inside a branch child raises `PipelineConfigError`. Branch children always block until joined — detaching mid-branch is incoherent. + - **Nested pipeline boundary is ignored.** When a pipeline is used as a step inside another pipeline, `async_boundary` is warned and ignored — there is no "next sample" to move to from the outer pipeline's perspective. + +--- + +## `workers` vs `max_workers` — independent pools + +These two knobs control different thread pools and do not interact: + +| Knob | Pool | Controls | +|------|------|----------| +| `pipe.run(contexts, workers=N)` | foreground pool | How many samples run through pre-boundary steps simultaneously | +| `step.max_workers = K` | background pool (per step class) | How many instances of that step run in the background simultaneously | + +A sample leaves the foreground pool when it crosses the `async_boundary` and enters the background step's pool. + +**Mental model:** `workers` controls throughput *into* the pipeline; `max_workers` controls throughput *through* each background step. + +!!! warning "Rate limits" + `workers` and `max_workers` are independent pools, but total concurrent outbound calls = foreground calls + background calls. With `workers=4` and `max_workers=3`, up to 7 requests may be in-flight simultaneously. Account for this when configuring per-provider rate limits. + +--- + +## Rule of thumb + +| Question | Answer | +|----------|--------| +| Does the step wait on I/O? | `async def __call__` | +| Do I want to process more samples while previous ones are still in background steps? | `async_boundary = True` on the handoff step | +| Can two steps on the same sample run simultaneously? | [`Branch`](branching.md) | +| Do I want N samples going through the pipeline at the same time? | `workers=N` on `run()` | diff --git a/docs/pipeline/index.md b/docs/pipeline/index.md new file mode 100644 index 0000000000000000000000000000000000000000..fc6945c6d4e7600c12e2e189711afe816c745ae1 --- /dev/null +++ b/docs/pipeline/index.md @@ -0,0 +1,172 @@ +# Pipeline Engine + +A generic, composable step runner for ordered and parallel data processing. + +--- + +## What is the Pipeline Engine? + +The Pipeline Engine is a lightweight, domain-agnostic framework for composing processing steps into pipelines. It provides contract validation, immutable context passing, and built-in concurrency control — all in ~300 lines of pure Python with no external dependencies beyond the standard library. + +Everything composes from three primitives: + +**Sequential** — steps run one after another: + +```mermaid +graph LR + A1[Step A] --> B1[Step B] --> C1[Step C] +``` + +**Branch** — fork, run in parallel, join: + +```mermaid +graph LR + A2[Step A] --> B2[Step B] & C2[Step C] --> D2[Step D] +``` + +**Nesting** — a pipeline used as a step: + +```mermaid +graph LR + A3[Step A] --> P3[[Inner Pipeline]] --> D3[Step D] +``` + +Steps declare what data they read and write. The pipeline validates ordering at construction time — before any data flows — so wiring errors surface immediately, not at runtime. + +--- + +## Core Principles + +- **Three primitives** — Sequential steps, parallel branches, and nested pipelines cover every composition pattern +- **Contracts** — Steps declare `requires` and `provides` fields; the pipeline validates ordering at construction time +- **Immutable context** — Steps receive a frozen context and return a new one via `.replace()`, making concurrent execution safe by default +- **Declared concurrency** — Parallelism is configured on the step (`max_workers`, `async_boundary`), not the pipeline +- **Per-sample error isolation** — One failing sample never blocks others; every sample produces a result +- **Observation hooks** — `PipelineHook` lets external code observe step transitions without modifying data flow (progress streaming, metrics, logging) +- **Cancellation** — `CancellationToken` stops a running pipeline between steps; `cancel_token_var` makes the token readable inside steps for intra-step cancellation + +--- + +## Architecture at a Glance + +```mermaid +classDiagram + class StepProtocol { + <<protocol>> + +requires: set[str] + +provides: set[str] + +__call__(ctx: StepContext) StepContext + } + + class Pipeline { + +then(step) Pipeline + +branch(*pipelines) Pipeline + +run(samples) list~SampleResult~ + +run_async(samples) list~SampleResult~ + } + + class Branch { + +merge: MergeStrategy + } + + class YourStep { + +requires: set[str] + +provides: set[str] + +__call__(ctx) StepContext + } + + class StepContext { + <<frozen dataclass>> + +sample: str + +metadata: MappingProxyType + +replace(**kw) StepContext + } + + class SampleResult { + <<dataclass>> + +context: StepContext + +error: Exception? + +ok: bool + } + + StepProtocol <|.. Pipeline : satisfies + StepProtocol <|.. Branch : satisfies + StepProtocol <|.. YourStep : satisfies + Pipeline *-- "1..*" StepProtocol : contains steps + Branch *-- "2..*" Pipeline : contains pipelines + StepProtocol ..> StepContext : receives & returns + Pipeline ..> SampleResult : produces +``` + +`Pipeline` and `Branch` both satisfy `StepProtocol` through structural typing — no inheritance required. This means a `Pipeline` can be used as a step inside another pipeline, and a `Branch` slots into any step position. + +| Concept | What it is | Threading | Data flow | +|---------|-----------|-----------|-----------| +| **Step** | Single unit of work | Sync internally | Receives and returns `StepContext` | +| **Pipeline** | Ordered chain of steps | `workers=N` across samples | Passes `StepContext` step-to-step | +| **Branch** | Parallel fork/join | One thread per branch | Copies context in, merges outputs | +| **Nested Pipeline** | Pipeline used as a step | Inherits parent threading | Same `StepContext` flow | + +--- + +## Async Boundary — Background Processing + +One of the engine's key features is the **async boundary**: a way to split a pipeline into foreground (fast return) and background (fire-and-forget) stages. + +```mermaid +graph LR + S1["Step A"] --> S2["Step B"] --> AB{{"async_boundary"}} --> S3["Step C<br/><small>background</small>"] --> S4["Step D<br/><small>background</small>"] + + style S1 fill:#6366f1,stroke:#4f46e5,color:#fff + style S2 fill:#6366f1,stroke:#4f46e5,color:#fff + style AB fill:#f59e0b,stroke:#d97706,color:#000 + style S3 fill:#3b82f6,stroke:#2563eb,color:#fff + style S4 fill:#3b82f6,stroke:#2563eb,color:#fff +``` + +Mark any step with `async_boundary = True` — the pipeline returns results immediately after the foreground steps, while everything from the boundary onward continues in background threads. Use `pipe.wait_for_background()` when you need the final results. + +This is critical for pipelines where early steps produce user-facing output quickly but later steps (analysis, logging, scoring) are slow and don't need to block the caller. See [Execution Model](execution.md) for full details. + +--- + +## When to Use + +!!! tip "Good fit" + - Ordered multi-step processing with explicit data dependencies + - Parallel fork/join patterns (multiple independent operations on the same data) + - Fire-and-forget background processing with `async_boundary` + - Any pipeline where you want construction-time contract validation + +!!! note "Not designed for" + - DAG scheduling with complex dependency graphs + - Distributed computing across multiple machines + - Stream processing with backpressure + - ETL pipelines requiring a data catalog + +--- + +## Installation + +The pipeline engine is included in the project with no extra dependencies: + +```python +from pipeline import Pipeline, Branch, StepContext, MergeStrategy, PipelineHook, CancellationToken +``` + +!!! tip "Using the Pipeline Engine with ACE" + If you're building ACE pipelines, see [Composing Pipelines](../guides/composing-pipelines.md) + for ACE-specific steps and patterns. All pipeline classes are also importable + from `ace` directly: `from ace import Pipeline, Branch, ...` + +--- + +## What's Next + +- [**Quick Start**](quick-start.md) — Build and run your first pipeline in under 30 lines +- [**Core Concepts**](core-concepts.md) — Understand Step, Context, and the contract system +- [**Execution Model**](execution.md) — Three types of async, workers, and background processing +- [**Branching & Parallelism**](branching.md) — Parallel fork/join with merge strategies +- [**Error Handling**](error-handling.md) — Per-sample isolation, SampleResult, and error types +- [**Building Custom Steps**](custom-steps.md) — Create your own steps with dependency injection +- [**API Reference**](api-reference.md) — Complete signatures for all public classes diff --git a/docs/pipeline/quick-start.md b/docs/pipeline/quick-start.md new file mode 100644 index 0000000000000000000000000000000000000000..74d868838c2716a43ec2a5ea63243150e52c46db --- /dev/null +++ b/docs/pipeline/quick-start.md @@ -0,0 +1,194 @@ +# Quick Start + +Build and run your first pipeline in under 30 lines. + +--- + +## Define two steps + +Every step needs three things: `requires`, `provides`, and a `__call__` method. + +```python +from types import MappingProxyType +from pipeline import Pipeline, StepContext + + +class Tokenize: + """Split text into tokens and count words.""" + requires = frozenset() + provides = frozenset({"tokens", "word_count"}) + + def __call__(self, ctx: StepContext) -> StepContext: + tokens = str(ctx.sample).split() + return ctx.replace( + metadata=MappingProxyType({ + **ctx.metadata, + "tokens": tokens, + "word_count": len(tokens), + }) + ) + + +class Uppercase: + """Convert tokens to uppercase.""" + requires = frozenset({"tokens"}) + provides = frozenset({"upper_tokens"}) + + def __call__(self, ctx: StepContext) -> StepContext: + upper = [t.upper() for t in ctx.metadata["tokens"]] + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "upper_tokens": upper}) + ) +``` + +--- + +## Build and run + +Chain steps with `.then()` and run with a list of contexts: + +```python +pipe = Pipeline().then(Tokenize()).then(Uppercase()) + +results = pipe.run([ + StepContext(sample="hello world"), + StepContext(sample="pipeline engine demo"), +]) +``` + +The pipeline validates ordering at construction time — if `Uppercase` came before `Tokenize`, you'd get a `PipelineOrderError` immediately, not at runtime. + +--- + +## Inspect results + +Every sample produces exactly one `SampleResult`: + +```python +for r in results: + if r.error: + print(f"Failed at {r.failed_at}: {r.error}") + else: + print(f"Sample: {r.sample}") + print(f"Tokens: {r.output.metadata['upper_tokens']}") + print(f"Count: {r.output.metadata['word_count']}") +``` + +``` +Sample: hello world +Tokens: ['HELLO', 'WORLD'] +Count: 2 + +Sample: pipeline engine demo +Tokens: ['PIPELINE', 'ENGINE', 'DEMO'] +Count: 3 +``` + +--- + +## Add parallelism with Branch + +Run independent steps simultaneously with `Branch`: + +```python +from pipeline import MergeStrategy + + +class Reverse: + """Reverse each token.""" + requires = frozenset({"tokens"}) + provides = frozenset({"reversed_tokens"}) + + def __call__(self, ctx: StepContext) -> StepContext: + rev = [t[::-1] for t in ctx.metadata["tokens"]] + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "reversed_tokens": rev}) + ) + + +pipe = ( + Pipeline() + .then(Tokenize()) + .branch( + Pipeline().then(Uppercase()), # runs in parallel + Pipeline().then(Reverse()), # runs in parallel + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) +) + +results = pipe.run([StepContext(sample="fork join")]) + +meta = results[0].output.metadata +print(meta["upper_tokens"]) # ['FORK', 'JOIN'] +print(meta["reversed_tokens"]) # ['krof', 'nioj'] +``` + +Both branches write to different fields (`upper_tokens` vs `reversed_tokens`), so `RAISE_ON_CONFLICT` passes through without raising. + +--- + +## Fire-and-forget with async_boundary + +Some steps are slow and don't need to block the caller. Mark a step with `async_boundary = True` to hand everything from that point onward to a background thread — `run()` returns immediately after the foreground steps. + +```python +import time + + +class SlowScore: + """Expensive scoring that runs in the background.""" + requires = frozenset({"tokens"}) + provides = frozenset({"score"}) + async_boundary = True # everything from here runs in background + max_workers = 3 # up to 3 background threads + + def __call__(self, ctx: StepContext) -> StepContext: + time.sleep(0.5) # simulate slow work + score = ctx.metadata["word_count"] * 10 + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "score": score}) + ) + + +pipe = Pipeline().then(Tokenize()).then(SlowScore()) + +# Returns immediately — only Tokenize runs in the foreground +results = pipe.run([ + StepContext(sample="hello world"), + StepContext(sample="background processing demo"), +]) + +# Background scoring still running... +print(pipe.background_stats()) # {'active': 2, 'completed': 0} + +# Block until background work finishes +pipe.wait_for_background(timeout=10.0) + +# Now results are fully populated +for r in results: + print(f"{r.sample}: score={r.output.metadata['score']}") +``` + +``` +hello world: score=20 +background processing demo: score=30 +``` + +See [Execution Model](execution.md) for the full concurrency model — `workers` vs `max_workers`, async steps, and boundary rules. + +--- + +## Try it interactively + +All the examples on this page (and more) are available as a runnable Jupyter notebook: + +[:material-notebook: Open the Pipeline Demo Notebook](https://github.com/kayba-ai/agentic-context-engine/blob/main/examples/pipeline_ex/pipeline_demo.ipynb){ .md-button } + +--- + +## Next steps + +- [**Core Concepts**](core-concepts.md) — Understand the contract system and how validation works +- [**Execution Model**](execution.md) — Learn about async steps, `async_boundary`, and workers +- [**Branching & Parallelism**](branching.md) — Deep dive into merge strategies and error handling +- [**Building Custom Steps**](custom-steps.md) — Dependency injection, testing, and common patterns diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000000000000000000000000000000000..99707cc361c3146829c0f53388dcf04ab1d36f44 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,115 @@ +# ACE Framework Examples + +Navigation guide for all ACE examples. Each directory has its own detailed README. + +## 🎯 Getting Started + +**New to ACE?** Start with these: + +- **[simple_ace_example.py](simple_ace_example.py)** - Minimal ACE usage (5 minutes) +- **[seahorse_emoji_ace.py](seahorse_emoji_ace.py)** - Self-reflection demo +- **[Quick Start Guide](../docs/getting-started/quick-start.md)** - Step-by-step tutorial + +## 🧩 Integrations + +Add ACE learning to existing systems: + +### Browser Automation (browser-use) +**[browser-use/](browser-use/)** - Self-improving browser agents + +- [simple_ace_agent.py](browser-use/simple_ace_agent.py) - Basic ACEAgent usage +- [domain-checker/](browser-use/domain-checker/) - Domain availability automation +- [form-filler/](browser-use/form-filler/) - Form filling automation +- [online-shopping/](browser-use/online-shopping/) - E-commerce automation + +📖 See [browser-use/README.md](browser-use/README.md) for full guide + +### LangChain Integration +**[langchain/](langchain/)** - Wrap LangChain chains/agents with learning + +- [simple_chain_example.py](langchain/simple_chain_example.py) - Basic chain + ACE +- [agent_with_tools_example.py](langchain/agent_with_tools_example.py) - Agent with tools + +📖 See [langchain/README.md](langchain/README.md) for patterns + +### Custom Integration +**[custom_integration_example.py](custom_integration_example.py)** - Pattern for any agent + +Shows the three-step integration: Inject → Execute → Learn + +### Pipeline Composition +**[pipeline_composition/](pipeline_composition/)** - Build custom ACE pipelines + +- [compose_custom_pipeline.py](pipeline_composition/compose_custom_pipeline.py) - Mix and match steps, add custom steps, use `build_steps()` + +## 📊 Advanced Topics + +### Production Learning +**[helicone/](helicone/)** - Learn from Helicone observability logs + +- Parse production LLM traces +- Replay-based learning (cost-effective) +- Tool selection analysis + +📖 See [helicone/README.md](helicone/README.md) + +### Prompt Engineering +**[prompts/](prompts/)** - Compare ACE prompt versions + +- [compare_v1_v2_prompts.py](prompts/compare_v1_v2_prompts.py) - v1.0 vs v2.0 +- [advanced_prompts_v2.py](prompts/advanced_prompts_v2.py) - Advanced techniques + +### Skillbook Management +- **[skillbook_persistence.py](skillbook_persistence.py)** - Save and load learned strategies + +## 🗂️ Examples by Use Case + +| Use Case | Example | +|----------|---------| +| Q&A systems | [simple_ace_example.py](simple_ace_example.py) | +| Browser automation | [browser-use/](browser-use/) | +| LangChain workflows | [langchain/](langchain/) | +| Custom agents | [custom_integration_example.py](custom_integration_example.py) | +| Custom pipelines | [pipeline_composition/](pipeline_composition/) | +| Production learning | [helicone/](helicone/) | +| Prompt optimization | [prompts/](prompts/) | + +## 🚀 Quick Start + +```bash +# 1. Install +pip install ace-framework + +# 2. Set API key +export OPENAI_API_KEY="your-api-key" + +# 3. Run example +python examples/simple_ace_example.py + +# Browser examples (contributors: uv sync --group demos) +uv run python examples/browser-use/simple_ace_agent.py +``` + +## 📚 Documentation + +> **Note:** Documentation has been reorganized. Previous top-level docs (`QUICK_START.md`, `INTEGRATION_GUIDE.md`, etc.) have moved to `docs/getting-started/`, `docs/guides/`, and `docs/api/`. Old versions are archived in `docs/old_docs/`. + +- **[Quick Start Guide](../docs/getting-started/quick-start.md)** - 5-minute tutorial +- **[Integration Guide](../docs/guides/integration.md)** - Add ACE to existing agents +- **[API Reference](../docs/api/index.md)** - Complete API +- **[Complete ACE Guide](../docs/guides/complete-guide.md)** - Deep dive + +## 🔧 Adapting Examples + +Each example is documented for easy adaptation: + +1. **Browser automation**: Copy [browser-use/TEMPLATE.py](browser-use/TEMPLATE.py) +2. **LangChain**: See [langchain/README.md](langchain/README.md) patterns +3. **Custom agent**: Follow [custom_integration_example.py](custom_integration_example.py) +4. **Other**: Check subdirectory READMEs for guidance + +## ❓ Need Help? + +- [GitHub Issues](https://github.com/kayba-ai/agentic-context-engine/issues) +- [Discord Community](https://discord.gg/mqCqH7sTyK) +- Check subdirectory READMEs for specific guidance diff --git a/examples/ace/ace_demo.ipynb b/examples/ace/ace_demo.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..3e707a23ffacb35c9329c7f573d026b526d7f1f6 --- /dev/null +++ b/examples/ace/ace_demo.ipynb @@ -0,0 +1,3275 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " # ACE Next — Interactive Demo\n", + "\n", + "\n", + "\n", + " This notebook walks through the refactored `ace_next` pipeline.\n", + "\n", + " It covers:\n", + "\n", + "\n", + "\n", + " 1. **Runners** — `ACE` (full pipeline) and `TraceAnalyser` (learning-only)\n", + "\n", + " 2. **Steps** — individual pipeline steps and `learning_tail()`\n", + "\n", + " 3. **Manual pipeline construction** — composing steps by hand\n", + "\n", + " 4. **Custom environments** — writing your own evaluator\n", + "\n", + " 5. **Checkpointing & deduplication** — production features\n", + "\n", + " 6. **Observability with Opik** — pipeline traces and LLM cost tracking\n", + "\n", + " 7. **Skillbook persistence** — save / reload\n", + "\n", + " 8. **TraceAnalyser** — learning from pre-recorded traces\n", + "\n", + "\n", + "\n", + " **Requirements:** `uv sync` from the repo root.\n", + "\n", + " Set your LLM API key before running:\n", + "\n", + " ```bash\n", + "\n", + " export OPENAI_API_KEY=\"sk-...\"\n", + "\n", + " ```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ## 1. Setup & Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Project root: /home/david/Desktop/projects/Kayba/agentic-context-engine\n", + "Setup OK\n" + ] + } + ], + "source": [ + "import os\n", + "import sys\n", + "import logging\n", + "import tempfile\n", + "from pathlib import Path\n", + "\n", + "import nest_asyncio\n", + "\n", + "nest_asyncio.apply()\n", + "\n", + "# Silence LiteLLM's verbose logging so notebook output stays clean\n", + "logging.getLogger(\"LiteLLM\").setLevel(logging.WARNING)\n", + "logging.getLogger(\"LiteLLM Router\").setLevel(logging.WARNING)\n", + "logging.getLogger(\"LiteLLM Proxy\").setLevel(logging.WARNING)\n", + "\n", + "# Also suppress litellm's own set_verbose flag\n", + "try:\n", + " import litellm\n", + " litellm.set_verbose = False\n", + "except ImportError:\n", + " pass\n", + "\n", + "# Ensure the project root is on sys.path so `ace`, `ace_next`, and `pipeline`\n", + "# are importable regardless of where the notebook kernel starts.\n", + "_here = Path(__file__).resolve().parent if \"__file__\" in dir() else Path.cwd()\n", + "_root = _here\n", + "for _p in [_here] + list(_here.parents):\n", + " if (_p / \"pipeline\" / \"__init__.py\").exists():\n", + " _root = _p\n", + " break\n", + "sys.path.insert(0, str(_root))\n", + "\n", + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv(_root / \".env\")\n", + "\n", + "print(f\"Project root: {_root}\")\n", + "print(\"Setup OK\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ## 2. Core Imports\n", + "\n", + "\n", + "\n", + " Everything lives in `ace_next` — fully self-contained, zero cross-imports." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "All imports OK\n" + ] + } + ], + "source": [ + "from ace_next import (\n", + " # Runners\n", + " ACE,\n", + " TraceAnalyser,\n", + " # Role implementations\n", + " Agent,\n", + " Reflector,\n", + " SkillManager,\n", + " # LLM providers\n", + " LiteLLMClient,\n", + " # Core types\n", + " Sample,\n", + " Skillbook,\n", + " SimpleEnvironment,\n", + " TaskEnvironment,\n", + " EnvironmentResult,\n", + ")\n", + "from ace_next.core import AgentOutput, ACEStepContext, SkillbookView\n", + "\n", + "print(\"All imports OK\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ## 3. Configure the LLM Client\n", + "\n", + "\n", + "\n", + " We use LiteLLM which supports 100+ providers. Swap the model string\n", + "\n", + " for any provider: `gpt-4o-mini`, `claude-sonnet-4-5-20250929`,\n", + "\n", + " `bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0`, etc." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LLM client ready: us.anthropic.claude-haiku-4-5-20251001-v1:0\n" + ] + } + ], + "source": [ + "MODEL = os.getenv(\"ACE_MODEL\", \"us.anthropic.claude-haiku-4-5-20251001-v1:0\")\n", + "client = LiteLLMClient(model=MODEL)\n", + "\n", + "print(f\"LLM client ready: {MODEL}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ## 4. Build Roles\n", + "\n", + "\n", + "\n", + " The three ACE roles share the same LLM client. Each is independently\n", + "\n", + " customisable (prompt templates, retries, etc.)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Roles created: Agent, Reflector, SkillManager\n" + ] + } + ], + "source": [ + "agent = Agent(client)\n", + "reflector = Reflector(client)\n", + "skill_manager = SkillManager(client)\n", + "\n", + "print(\"Roles created: Agent, Reflector, SkillManager\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ## 5. Define Training Samples" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Prepared 5 training samples\n" + ] + } + ], + "source": [ + "samples = [\n", + " Sample(question=\"What is the capital of France?\", ground_truth=\"Paris\"),\n", + " Sample(question=\"What is the capital of Japan?\", ground_truth=\"Tokyo\"),\n", + " Sample(question=\"What is the capital of Brazil?\", ground_truth=\"Brasilia\"),\n", + " Sample(question=\"What is the capital of Australia?\", ground_truth=\"Canberra\"),\n", + " Sample(question=\"What is the capital of Nigeria?\", ground_truth=\"Abuja\"),\n", + "]\n", + "\n", + "print(f\"Prepared {len(samples)} training samples\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ---\n", + "\n", + " ## 6. ACE Runner — Full Adaptive Pipeline\n", + "\n", + "\n", + "\n", + " The `ACE` runner is the full closed-loop pipeline:\n", + "\n", + " ```\n", + "\n", + " Agent → Evaluate → Reflect → Tag → Update → Apply\n", + "\n", + " ```\n", + "\n", + "\n", + "\n", + " It takes `Sample` objects and an optional `TaskEnvironment`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### 6a. With SimpleEnvironment\n", + "\n", + "\n", + "\n", + " `SimpleEnvironment` checks if the ground truth appears in the agent's\n", + "\n", + " answer (case-insensitive substring match)." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Processed 3 samples\n", + "\n", + " Q: What is the capital of France?\n", + " A: The capital of France is Paris.\n", + " Q: What is the capital of Japan?\n", + " A: Tokyo is the capital of Japan.\n", + " Q: What is the capital of Brazil?\n", + " A: The capital of Brazil is Brasília.\n" + ] + } + ], + "source": [ + "skillbook = Skillbook()\n", + "\n", + "ace = ACE.from_roles(\n", + " agent=agent,\n", + " reflector=reflector,\n", + " skill_manager=skill_manager,\n", + " environment=SimpleEnvironment(),\n", + " skillbook=skillbook,\n", + ")\n", + "\n", + "results = ace.run(samples[:3], epochs=1)\n", + "\n", + "print(f\"Processed {len(results)} samples\\n\")\n", + "for r in results:\n", + " if r.error:\n", + " print(f\" ERROR at {r.failed_at}: {r.error}\")\n", + " elif r.output:\n", + " ctx: ACEStepContext = r.output\n", + " answer = ctx.agent_output.final_answer if ctx.agent_output else \"N/A\"\n", + " print(f\" Q: {r.sample.question}\")\n", + " print(f\" A: {answer}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Skillbook after 1 epoch:\n", + " Stats: {'sections': 6, 'skills': 8, 'tags': {'helpful': 8, 'harmful': 0, 'neutral': 0}}\n", + " - [problem_classification-00001] Classify factual queries as direct recall, not strategic problems\n", + " - [knowledge_retrieval-00002] Retrieve geographic facts directly; verify output format matches evaluation ground truth\n", + " - [strategy_selection-00003] Avoid complex strategies for straightforward factual queries\n", + " - [question_analysis-00004] Recognize factual retrieval questions requiring direct knowledge access\n", + " - [verification_patterns-00005] Support factual answers with historical context or authoritative evidence\n" + ] + } + ], + "source": [ + "print(f\"\\nSkillbook after 1 epoch:\")\n", + "print(f\" Stats: {skillbook.stats()}\")\n", + "for skill in skillbook.skills()[:5]:\n", + " print(f\" - [{skill.id}] {skill.content}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### 6b. Custom Environment\n", + "\n", + "\n", + "\n", + " Create your own evaluator by subclassing `TaskEnvironment`." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ExactMatchEnvironment defined\n" + ] + } + ], + "source": [ + "class ExactMatchEnvironment(TaskEnvironment):\n", + " \"\"\"Strict evaluation: answer must exactly match ground truth.\"\"\"\n", + "\n", + " def evaluate(self, sample: Sample, agent_output: AgentOutput) -> EnvironmentResult:\n", + " expected = (sample.ground_truth or \"\").strip().lower()\n", + " predicted = agent_output.final_answer.strip().lower()\n", + " correct = expected == predicted\n", + "\n", + " return EnvironmentResult(\n", + " feedback=\"Correct!\" if correct else f\"Wrong. Expected: {sample.ground_truth}\",\n", + " ground_truth=sample.ground_truth,\n", + " metrics={\"accuracy\": 1.0 if correct else 0.0},\n", + " )\n", + "\n", + "\n", + "print(\"ExactMatchEnvironment defined\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "skillbook2 = Skillbook()\n", + "\n", + "ace2 = ACE.from_roles(\n", + " agent=Agent(client),\n", + " reflector=Reflector(client),\n", + " skill_manager=SkillManager(client),\n", + " environment=ExactMatchEnvironment(),\n", + " skillbook=skillbook2,\n", + ")\n", + "\n", + "results2 = ace2.run(samples[:2], epochs=1)\n", + "\n", + "for r in results2:\n", + " if r.output:\n", + " ctx = r.output\n", + " print(f\" Q: {r.sample.question}\")\n", + " print(f\" A: {ctx.agent_output.final_answer if ctx.agent_output else 'N/A'}\")\n", + " if ctx.reflections:\n", + " print(f\" Insight: {ctx.reflections[0].key_insight}\")\n", + " print()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Skillbook after 1 epoch:\n", + " Stats: {'sections': 0, 'skills': 0, 'tags': {'helpful': 0, 'harmful': 0, 'neutral': 0}}\n" + ] + } + ], + "source": [ + "print(f\"\\nSkillbook after 1 epoch:\")\n", + "print(f\" Stats: {skillbook2.stats()}\")\n", + "for skill in skillbook2.skills()[:5]:\n", + " print(f\" - [{skill.id}] {skill.content}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### 6c. Without Environment\n", + "\n", + "\n", + "\n", + " When no environment is provided, `EvaluateStep` is a no-op. The Reflector\n", + "\n", + " still learns from ground-truth comparison in the trace." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:07 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:13 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:13 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:13 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:19 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:19 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:23 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:23 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:29 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:32 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:32 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:42 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n", + "Processed 2 samples (no environment)\n", + "Skills learned: {'sections': 3, 'skills': 6, 'tags': {'helpful': 6, 'harmful': 0, 'neutral': 0}}\n" + ] + } + ], + "source": [ + "skillbook3 = Skillbook()\n", + "\n", + "ace3 = ACE.from_roles(\n", + " agent=Agent(client),\n", + " reflector=Reflector(client),\n", + " skill_manager=SkillManager(client),\n", + " skillbook=skillbook3,\n", + " # No environment — EvaluateStep passes through\n", + ")\n", + "\n", + "results3 = ace3.run(samples[:2], epochs=1)\n", + "print(f\"Processed {len(results3)} samples (no environment)\")\n", + "print(f\"Skills learned: {skillbook3.stats()}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### 6d. Multi-Epoch Training\n", + "\n", + "\n", + "\n", + " Multiple epochs let the agent revisit samples with an evolving skillbook.\n", + "\n", + " Skills accumulate and refine across passes." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:43:57 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:04 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:04 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:04 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:09 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:09 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:09 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:14 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:14 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:15 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:15 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:15 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:19 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:20 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:20 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:20 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:22 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:22 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:26 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:26 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:26 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n", + "\u001b[92m16:44:26 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n", + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:29 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:29 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:29 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:32 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:32 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:32 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:37 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:37 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:37 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:39 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:39 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:39 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:41 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:44 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:44 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:44 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:46 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:46 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:48 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:50 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:50 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n", + "\u001b[92m16:44:50 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n", + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:55 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:56 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:56 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:58 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:44:58 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:45:00 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:45:07 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:45:10 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:45:10 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:45:19 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:45:19 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:45:32 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:45:32 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:45:41 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:45:41 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:45:52 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n", + "Total results across 2 epochs: 10\n", + "Skills learned: {'sections': 1, 'skills': 8, 'tags': {'helpful': 38, 'harmful': 1, 'neutral': 0}}\n", + " Epoch 1: 4/5 correct\n", + " Epoch 2: 4/5 correct\n" + ] + } + ], + "source": [ + "skillbook4 = Skillbook()\n", + "\n", + "ace4 = ACE.from_roles(\n", + " agent=Agent(client),\n", + " reflector=Reflector(client),\n", + " skill_manager=SkillManager(client),\n", + " environment=SimpleEnvironment(),\n", + " skillbook=skillbook4,\n", + ")\n", + "\n", + "results4 = ace4.run(samples, epochs=2)\n", + "\n", + "print(f\"Total results across 2 epochs: {len(results4)}\")\n", + "print(f\"Skills learned: {skillbook4.stats()}\")\n", + "\n", + "# Print per-epoch accuracy\n", + "for epoch in range(1, 3):\n", + " epoch_results = [r for r in results4 if r.output and r.output.epoch == epoch]\n", + " correct = sum(\n", + " 1 for r in epoch_results\n", + " if r.output and r.output.agent_output\n", + " and (r.sample.ground_truth or \"\").lower() in r.output.agent_output.final_answer.lower()\n", + " )\n", + " print(f\" Epoch {epoch}: {correct}/{len(epoch_results)} correct\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ---\n", + "\n", + " ## 7. Manual Step-by-Step Pipeline\n", + "\n", + "\n", + "\n", + " Under the hood, runners compose `Pipeline` objects from individual steps.\n", + "\n", + " Here we build one by hand to see exactly what each step does." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Pipeline steps: 6\n", + " requires: frozenset({'sample', 'skillbook'})\n", + " provides: frozenset({'agent_output', 'skill_manager_output', 'reflection', 'trace'})\n" + ] + } + ], + "source": [ + "from pipeline import Pipeline\n", + "from ace_next.steps import (\n", + " AgentStep,\n", + " EvaluateStep,\n", + " ReflectStep,\n", + " TagStep,\n", + " UpdateStep,\n", + " ApplyStep,\n", + " learning_tail,\n", + ")\n", + "\n", + "skillbook5 = Skillbook()\n", + "env = SimpleEnvironment()\n", + "\n", + "# Build the full pipeline manually\n", + "pipe = Pipeline(\n", + " [\n", + " AgentStep(Agent(client)),\n", + " EvaluateStep(env),\n", + " *learning_tail(Reflector(client), SkillManager(client), skillbook5),\n", + " ]\n", + ")\n", + "\n", + "print(f\"Pipeline steps: {len(pipe._steps)}\")\n", + "print(f\" requires: {pipe.requires}\")\n", + "print(f\" provides: {pipe.provides}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### Run a single sample through the manual pipeline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sample = samples[0]\n", + "\n", + "# Build the context the same way ACE._build_context() does\n", + "ctx = ACEStepContext(\n", + " sample=sample,\n", + " skillbook=SkillbookView(skillbook5),\n", + " epoch=1,\n", + " total_epochs=1,\n", + " step_index=0,\n", + " total_steps=1,\n", + " global_sample_index=0,\n", + ")\n", + "\n", + "print(f\"Before pipeline:\")\n", + "print(f\" Skills: {skillbook5.stats()}\")\n", + "print(f\" agent_output: {ctx.agent_output}\")\n", + "\n", + "# Run the full pipeline on a single context\n", + "from pipeline.protocol import SampleResult\n", + "\n", + "results_manual = pipe.run([ctx])\n", + "\n", + "print(f\"\\nAfter pipeline:\")\n", + "for r in results_manual:\n", + " if r.error:\n", + " print(f\" ERROR: {r.error}\")\n", + " elif r.output:\n", + " out: ACEStepContext = r.output\n", + " print(f\" Agent answer: {out.agent_output.final_answer if out.agent_output else 'N/A'}\")\n", + " print(f\" Reflector insight: {out.reflections[0].key_insight if out.reflections else 'N/A'}\")\n", + " print(f\" Skills now: {skillbook5.stats()}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### Using `learning_tail()` as a building block\n", + "\n", + "\n", + "\n", + " `learning_tail()` returns the standard learning steps:\n", + "\n", + " `[ReflectStep, TagStep, UpdateStep, ApplyStep]` with optional\n", + "\n", + " deduplication and checkpoint steps appended." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "learning_tail() returns 4 steps:\n", + " - ReflectStep\n", + " - TagStep\n", + " - UpdateStep\n", + " - ApplyStep\n" + ] + } + ], + "source": [ + "skillbook6 = Skillbook()\n", + "\n", + "tail = learning_tail(\n", + " Reflector(client),\n", + " SkillManager(client),\n", + " skillbook6,\n", + ")\n", + "\n", + "print(f\"learning_tail() returns {len(tail)} steps:\")\n", + "for step in tail:\n", + " print(f\" - {type(step).__name__}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ---\n", + "\n", + " ## 8. Checkpointing\n", + "\n", + "\n", + "\n", + " Save the skillbook every N successful samples so you can resume after\n", + "\n", + " interruption or compare skillbook evolution over time." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:25 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:31 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:31 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:31 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:36 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:36 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:36 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:41 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:41 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:42 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:42 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:42 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:46 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:48 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:48 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:48 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:50 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:50 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:52 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:54 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:54 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:57 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:58 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m16:59:58 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n", + "INFO [ace_next.steps.checkpoint] CheckpointStep: saved checkpoint at sample 2 → /tmp/tmpqnq54gga/checkpoint_2.json\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:00:04 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:00:05 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:00:05 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:00:17 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:00:17 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n", + "INFO [ace_next.steps.checkpoint] CheckpointStep: saved checkpoint at sample 4 → /tmp/tmpqnq54gga/checkpoint_4.json\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:00:28 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n", + "Checkpoint files:\n", + " checkpoint_2.json (2464 bytes)\n", + " checkpoint_4.json (4971 bytes)\n", + " latest.json (4971 bytes)\n" + ] + } + ], + "source": [ + "skillbook7 = Skillbook()\n", + "\n", + "with tempfile.TemporaryDirectory() as tmpdir:\n", + " ace7 = ACE.from_roles(\n", + " agent=Agent(client),\n", + " reflector=Reflector(client),\n", + " skill_manager=SkillManager(client),\n", + " environment=SimpleEnvironment(),\n", + " skillbook=skillbook7,\n", + " checkpoint_dir=tmpdir,\n", + " checkpoint_interval=2, # save every 2 successful samples\n", + " )\n", + "\n", + " results7 = ace7.run(samples, epochs=1)\n", + "\n", + " saved = sorted(Path(tmpdir).glob(\"*.json\"))\n", + " print(\"Checkpoint files:\")\n", + " for f in saved:\n", + " print(f\" {f.name} ({f.stat().st_size} bytes)\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ---\n", + "\n", + " ## 9. Deduplication\n", + "\n", + "\n", + "\n", + " Merge near-duplicate skills to keep the skillbook compact. The\n", + "\n", + " `DeduplicationManager` runs periodically during training." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:08:53 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:08:59 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:08:59 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:08:59 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:04 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:04 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:04 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:08 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:08 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:09 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:09 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:09 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:14 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:15 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:15 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n", + "\u001b[92m17:09:15 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n", + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:17 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:17 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:20 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:21 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:21 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:24 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:24 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:25 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:31 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:33 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:33 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n", + "\n", + "\u001b[1;31mGive Feedback / Get Help: https://github.com/BerriAI/litellm/issues/new\u001b[0m\n", + "LiteLLM.Info: If you need to debug this error, use `litellm._turn_on_debug()'.\n", + "\n", + "WARNING [ace_next.deduplication.detector] Failed to compute batch embeddings via LiteLLM: litellm.AuthenticationError: AuthenticationError: OpenAIException - Error code: 401 - {'error': {'message': 'Incorrect API key provided: sk-proj-********************************************************************************************************************************************************T6EA. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'code': 'invalid_api_key', 'param': None}, 'status': 401}\n", + "INFO [ace_next.deduplication.detector] Computed 0 embeddings for skills\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:42 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:42 - LiteLLM:INFO\u001b[0m: utils.py:3889 - \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] \n", + "LiteLLM completion() model= us.anthropic.claude-haiku-4-5-20251001-v1:0; provider = bedrock\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m17:09:54 - LiteLLM:INFO\u001b[0m: utils.py:1629 - Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO [LiteLLM] Wrapper: Completed Call, calling success_handler\n", + "Skills after training with dedup: {'sections': 3, 'skills': 7, 'tags': {'helpful': 8, 'harmful': 0, 'neutral': 0}}\n" + ] + } + ], + "source": [ + "from ace_next import DeduplicationManager, SimilarityDetector\n", + "from ace_next.protocols import DeduplicationConfig\n", + "\n", + "skillbook8 = Skillbook()\n", + "\n", + "dedup = DeduplicationManager(\n", + " DeduplicationConfig(similarity_threshold=0.85)\n", + ")\n", + "\n", + "ace8 = ACE.from_roles(\n", + " agent=Agent(client),\n", + " reflector=Reflector(client),\n", + " skill_manager=SkillManager(client),\n", + " environment=SimpleEnvironment(),\n", + " skillbook=skillbook8,\n", + " dedup_manager=dedup,\n", + " dedup_interval=3, # run dedup every 3 samples\n", + ")\n", + "\n", + "results8 = ace8.run(samples, epochs=1)\n", + "\n", + "print(f\"Skills after training with dedup: {skillbook8.stats()}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ---\n", + "\n", + " ## 10. Observability with Opik\n", + "\n", + "\n", + "\n", + " `OpikStep` is an explicit, opt-in pipeline step that logs traces to Opik.\n", + "\n", + " It is **not** wired into `learning_tail()` — you append it yourself.\n", + "\n", + "\n", + "\n", + " Three usage patterns:\n", + "\n", + " 1. **Pipeline traces + LLM cost tracking** — append `OpikStep()` (default)\n", + "\n", + " 2. **Pipeline traces only** — `OpikStep(register_litellm_callback=False)`\n", + "\n", + " 3. **LLM cost tracking only** — `register_opik_litellm_callback()` (no step)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opik available: True\n" + ] + } + ], + "source": [ + "from ace_next import OpikStep, OPIK_AVAILABLE, register_opik_litellm_callback\n", + "\n", + "print(f\"Opik available: {OPIK_AVAILABLE}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### 10a. Append OpikStep to a custom pipeline\n", + "\n", + "\n", + "\n", + " Place it at the end — after the learning tail." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Pipeline steps (with Opik): 7\n", + " - AgentStep\n", + " - EvaluateStep\n", + " - ReflectStep\n", + " - TagStep\n", + " - UpdateStep\n", + " - ApplyStep\n", + " - OpikStep\n" + ] + } + ], + "source": [ + "if OPIK_AVAILABLE:\n", + " skillbook_opik = Skillbook()\n", + "\n", + " pipe_with_opik = Pipeline(\n", + " [\n", + " AgentStep(Agent(client)),\n", + " EvaluateStep(SimpleEnvironment()),\n", + " *learning_tail(Reflector(client), SkillManager(client), skillbook_opik),\n", + " OpikStep(project_name=\"ace-demo\"),\n", + " ]\n", + " )\n", + " print(f\"Pipeline steps (with Opik): {len(pipe_with_opik._steps)}\")\n", + " for step in pipe_with_opik._steps:\n", + " print(f\" - {type(step).__name__}\")\n", + "else:\n", + " print(\"Opik not installed — skipping pipeline example\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### 10b. LLM-level cost tracking only\n", + "\n", + "\n", + "\n", + " If you only want per-LLM-call token/cost logging without pipeline traces,\n", + "\n", + " use the standalone helper. This registers an `OpikLogger` callback on\n", + "\n", + " `litellm.callbacks`." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LiteLLM Opik callback registered: True\n" + ] + } + ], + "source": [ + "if OPIK_AVAILABLE:\n", + " registered = register_opik_litellm_callback(project_name=\"ace-demo\")\n", + " print(f\"LiteLLM Opik callback registered: {registered}\")\n", + "else:\n", + " print(\"Opik not installed — skipping callback example\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ---\n", + "\n", + " ## 11. Skillbook Persistence — Save & Reload\n", + "\n", + "\n", + "\n", + " Save the learned skillbook to disk and reload it in a future session." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saved to learned_skillbook.json (5561 bytes)\n", + "Reloaded: {'sections': 5, 'skills': 7, 'tags': {'helpful': 7, 'harmful': 0, 'neutral': 0}}\n", + "Stats match: True\n" + ] + } + ], + "source": [ + "with tempfile.TemporaryDirectory() as tmpdir:\n", + " path = Path(tmpdir) / \"learned_skillbook.json\"\n", + "\n", + " # Save\n", + " skillbook.save_to_file(str(path))\n", + " print(f\"Saved to {path.name} ({path.stat().st_size} bytes)\")\n", + "\n", + " # Reload\n", + " reloaded = Skillbook.load_from_file(str(path))\n", + " print(f\"Reloaded: {reloaded.stats()}\")\n", + " print(f\"Stats match: {reloaded.stats() == skillbook.stats()}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ---\n", + "\n", + " ## 12. TraceAnalyser — Learning from Pre-Recorded Traces\n", + "\n", + "\n", + "\n", + " `TraceAnalyser` runs the learning tail only — no Agent, no Evaluate.\n", + "\n", + " Feed it raw trace dicts (the same shape ReflectStep expects) and it\n", + "\n", + " builds a skillbook from historical data." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Analysed 3 traces\n", + "Skills learned: {'sections': 2, 'skills': 6, 'tags': {'helpful': 6, 'harmful': 0, 'neutral': 0}}\n", + " - [information_retrieval] Use weather.com for direct weather information queries\n", + " - [information_retrieval] Navigate tool → search location → extract data for geographic queries\n", + " - [information_retrieval] Extract quantified data with specific values and conditions\n", + " - [web_ui_patterns] Identify and dismiss cookie consent popups before accessing page content\n", + " - [web_ui_patterns] Handle unexpected UI overlays appearing mid-interaction during web tasks\n" + ] + } + ], + "source": [ + "# Simulate some pre-recorded traces (e.g., from browser-use history logs)\n", + "traces = [\n", + " {\n", + " \"question\": \"Book a flight from NYC to London\",\n", + " \"reasoning\": \"Step 1: Opened booking site. Step 2: Searched flights. Step 3: Selected cheapest option.\",\n", + " \"answer\": \"Booked flight AA100 for $450\",\n", + " \"skill_ids\": [],\n", + " \"feedback\": \"Task succeeded in 3 steps\",\n", + " \"ground_truth\": None,\n", + " },\n", + " {\n", + " \"question\": \"Find the cheapest hotel in Paris\",\n", + " \"reasoning\": \"Step 1: Opened hotel site. Step 2: Set filters. Step 3: Sorted by price. Step 4: Cookie popup blocked view.\",\n", + " \"answer\": \"Failed: could not dismiss cookie popup\",\n", + " \"skill_ids\": [],\n", + " \"feedback\": \"Task failed — cookie popup blocked interaction after step 3\",\n", + " \"ground_truth\": None,\n", + " },\n", + " {\n", + " \"question\": \"Check weather in Tokyo\",\n", + " \"reasoning\": \"Step 1: Navigated to weather.com. Step 2: Searched Tokyo. Step 3: Read forecast.\",\n", + " \"answer\": \"Tokyo: 22C, partly cloudy\",\n", + " \"skill_ids\": [],\n", + " \"feedback\": \"Task succeeded in 3 steps — fast and accurate\",\n", + " \"ground_truth\": None,\n", + " },\n", + "]\n", + "\n", + "skillbook9 = Skillbook()\n", + "\n", + "analyser = TraceAnalyser.from_roles(\n", + " reflector=Reflector(client),\n", + " skill_manager=SkillManager(client),\n", + " skillbook=skillbook9,\n", + ")\n", + "\n", + "results9 = analyser.run(traces, epochs=1)\n", + "\n", + "print(f\"Analysed {len(results9)} traces\")\n", + "print(f\"Skills learned: {skillbook9.stats()}\")\n", + "for skill in skillbook9.skills()[:5]:\n", + " print(f\" - [{skill.section}] {skill.content}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### Multi-epoch trace analysis\n", + "\n", + "\n", + "\n", + " Each epoch re-processes all traces with the evolving skillbook.\n", + "\n", + " Early epochs extract obvious patterns; later epochs refine." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total results across 2 epochs: 6\n", + "Skills after 2 epochs: {'sections': 2, 'skills': 6, 'tags': {'helpful': 9, 'harmful': 0, 'neutral': 0}}\n" + ] + } + ], + "source": [ + "skillbook10 = Skillbook()\n", + "\n", + "analyser2 = TraceAnalyser.from_roles(\n", + " reflector=Reflector(client),\n", + " skill_manager=SkillManager(client),\n", + " skillbook=skillbook10,\n", + ")\n", + "\n", + "results10 = analyser2.run(traces, epochs=2)\n", + "\n", + "print(f\"Total results across 2 epochs: {len(results10)}\")\n", + "print(f\"Skills after 2 epochs: {skillbook10.stats()}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ---\n", + "\n", + " ## 13. Mixed Workflow — TraceAnalyser then ACE\n", + "\n", + "\n", + "\n", + " A common pattern: build an initial skillbook from historical traces,\n", + "\n", + " then deploy with live learning." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Phase 1 — TraceAnalyser:\n", + " Skills from traces: {'sections': 3, 'skills': 9, 'tags': {'helpful': 9, 'harmful': 0, 'neutral': 0}}\n", + "\n", + "Phase 2 — ACE live learning:\n", + " Processed 3 samples\n", + " Skills after live learning: {'sections': 3, 'skills': 12, 'tags': {'helpful': 17, 'harmful': 0, 'neutral': 7}}\n" + ] + } + ], + "source": [ + "# Phase 1: Build skillbook from historical data\n", + "shared_skillbook = Skillbook()\n", + "\n", + "analyser_phase1 = TraceAnalyser.from_roles(\n", + " reflector=Reflector(client),\n", + " skill_manager=SkillManager(client),\n", + " skillbook=shared_skillbook,\n", + ")\n", + "analyser_phase1.run(traces, epochs=1)\n", + "\n", + "print(f\"Phase 1 — TraceAnalyser:\")\n", + "print(f\" Skills from traces: {shared_skillbook.stats()}\")\n", + "\n", + "# Phase 2: Deploy with live ACE learning (reuse the evolved skillbook)\n", + "ace_phase2 = ACE.from_roles(\n", + " agent=Agent(client),\n", + " reflector=Reflector(client),\n", + " skill_manager=SkillManager(client),\n", + " environment=SimpleEnvironment(),\n", + " skillbook=shared_skillbook,\n", + ")\n", + "\n", + "results_phase2 = ace_phase2.run(samples[:3], epochs=1)\n", + "\n", + "print(f\"\\nPhase 2 — ACE live learning:\")\n", + "print(f\" Processed {len(results_phase2)} samples\")\n", + "print(f\" Skills after live learning: {shared_skillbook.stats()}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ---\n", + "\n", + " ## 14. Error Handling\n", + "\n", + "\n", + "\n", + " Failed samples are captured in `SampleResult.error` — the pipeline\n", + "\n", + " never drops a sample silently. Other samples continue processing." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " [1] OK answer=The capital of France is Paris.\n", + " [2] OK answer=No problem to solve. This prompt provides the operational framework and instructions for ACE Agent v2.1, but does not contain a specific question or problem in the 'Question' field. To proceed, please provide: (1) A specific question or problem to solve, (2) Relevant skillbook entries with strategy IDs and content, and (3) Any additional context needed. Once a question is provided, I will apply the skillbook protocol with complete step-by-step reasoning and specific skill citations.\n", + " [3] OK answer=Tokyo is the capital of Japan.\n" + ] + } + ], + "source": [ + "bad_samples = [\n", + " samples[0],\n", + " Sample(question=\"\", ground_truth=\"\"), # edge case: empty question\n", + " samples[1],\n", + "]\n", + "\n", + "skillbook11 = Skillbook()\n", + "ace11 = ACE.from_roles(\n", + " agent=Agent(client),\n", + " reflector=Reflector(client),\n", + " skill_manager=SkillManager(client),\n", + " environment=SimpleEnvironment(),\n", + " skillbook=skillbook11,\n", + ")\n", + "\n", + "results11 = ace11.run(bad_samples, epochs=1)\n", + "\n", + "for i, r in enumerate(results11, 1):\n", + " status = \"OK\" if r.error is None else f\"FAIL ({r.failed_at})\"\n", + " if r.output and r.output.agent_output:\n", + " answer = r.output.agent_output.final_answer\n", + " else:\n", + " answer = \"N/A\"\n", + " print(f\" [{i}] {status:20s} answer={answer}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ---\n", + "\n", + " ## 15. Inspecting the SkillbookView\n", + "\n", + "\n", + "\n", + " Steps receive a read-only `SkillbookView` on the context.\n", + "\n", + " This prevents accidental mutations from within pipeline steps." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SkillbookView: SkillbookView(0 skills)\n", + " len: 0\n", + " stats: {'sections': 0, 'skills': 0, 'tags': {'helpful': 0, 'harmful': 0, 'neutral': 0}}\n", + " prompt: skills[0\t]:...\n" + ] + } + ], + "source": [ + "sb = Skillbook()\n", + "view = SkillbookView(sb)\n", + "\n", + "print(f\"SkillbookView: {view}\")\n", + "print(f\" len: {len(view)}\")\n", + "print(f\" stats: {view.stats()}\")\n", + "print(f\" prompt: {view.as_prompt()[:200]}...\")\n", + "\n", + "# Iterate over skills in the view\n", + "for skill in view:\n", + " print(f\" - {skill.id}: {skill.content}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ---\n", + "\n", + " ## Summary\n", + "\n", + "\n", + "\n", + " | What | How |\n", + "\n", + " |------|-----|\n", + "\n", + " | Full pipeline | `ACE.from_roles(agent=..., reflector=..., skill_manager=...)` |\n", + "\n", + " | With environment | `ACE.from_roles(..., environment=SimpleEnvironment())` |\n", + "\n", + " | Without environment | `ACE.from_roles(...)` — EvaluateStep is a no-op |\n", + "\n", + " | Multi-epoch | `ace.run(samples, epochs=3)` |\n", + "\n", + " | Checkpointing | `ACE.from_roles(..., checkpoint_dir=\"./ckpts\", checkpoint_interval=10)` |\n", + "\n", + " | Deduplication | `ACE.from_roles(..., dedup_manager=dedup, dedup_interval=5)` |\n", + "\n", + " | Opik tracing | `Pipeline([...steps..., OpikStep(project_name=\"my-project\")])` |\n", + "\n", + " | LLM cost tracking | `register_opik_litellm_callback()` |\n", + "\n", + " | Trace analysis | `TraceAnalyser.from_roles(reflector=..., skill_manager=...)` |\n", + "\n", + " | Save skillbook | `ace.save(\"path.json\")` or `skillbook.save_to_file(\"path.json\")` |\n", + "\n", + " | Load skillbook | `Skillbook.from_file(\"path.json\")` |\n", + "\n", + " | Manual steps | `Pipeline([AgentStep(a), EvaluateStep(e), *learning_tail(r, sm, sb)])` |\n", + "\n", + " | Learning tail | `learning_tail(reflector, skill_manager, skillbook)` |\n", + "\n", + "\n", + "\n", + " **Pipeline:**\n", + "\n", + " ```\n", + "\n", + " ACE: Agent → Evaluate → Reflect → Tag → Update → Apply → [Dedup] → [Checkpoint] → [Opik]\n", + "\n", + " TraceAnalyser: Reflect → Tag → Update → Apply → [Dedup] → [Checkpoint] → [Opik]\n", + "\n", + " ```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ace-framework (3.12.0)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/ace/ace_demo.py b/examples/ace/ace_demo.py new file mode 100644 index 0000000000000000000000000000000000000000..e47dd44cbcdd7674dfab025ee9530ddfddbeac84 --- /dev/null +++ b/examples/ace/ace_demo.py @@ -0,0 +1,619 @@ +#!/usr/bin/env python3 +# %% [markdown] +# # ACE Next — Interactive Demo +# +# This notebook walks through the refactored `ace` pipeline. +# It covers: +# +# 1. **Runners** — `ACE` (full pipeline) and `TraceAnalyser` (learning-only) +# 2. **Steps** — individual pipeline steps and `learning_tail()` +# 3. **Manual pipeline construction** — composing steps by hand +# 4. **Custom environments** — writing your own evaluator +# 5. **Checkpointing & deduplication** — production features +# 6. **Observability with Opik** — pipeline traces and LLM cost tracking +# 7. **Skillbook persistence** — save / reload +# 8. **TraceAnalyser** — learning from pre-recorded traces +# +# **Requirements:** `uv sync` from the repo root. +# Set your LLM API key before running: +# ```bash +# export OPENAI_API_KEY="sk-..." +# ``` + +# %% [markdown] +# ## 1. Setup & Imports + +# %% +import os +import sys +import tempfile +from pathlib import Path + +import nest_asyncio + +nest_asyncio.apply() + +# Ensure the project root is on sys.path so `ace`, `ace`, and `pipeline` +# are importable regardless of where the notebook kernel starts. +_here = Path(__file__).resolve().parent if "__file__" in dir() else Path.cwd() +_root = _here +for _p in [_here] + list(_here.parents): + if (_p / "pipeline" / "__init__.py").exists(): + _root = _p + break +sys.path.insert(0, str(_root)) + +from dotenv import load_dotenv + +load_dotenv(_root / ".env") + +print(f"Project root: {_root}") +print("Setup OK") + +# %% [markdown] +# ## 2. Core Imports +# +# Everything lives in `ace` — fully self-contained, zero cross-imports. + +# %% +from ace import ( + # Runners + ACE, + TraceAnalyser, + # Role implementations + Agent, + Reflector, + SkillManager, + # Core types + Sample, + Skillbook, + SimpleEnvironment, + TaskEnvironment, + EnvironmentResult, +) +from ace.core import AgentOutput, ACEStepContext, SkillbookView + +print("All imports OK") + +# %% [markdown] +# ## 3. Configure the LLM Client +# +# We use LiteLLM which supports 100+ providers. Swap the model string +# for any provider: `gpt-4o-mini`, `claude-sonnet-4-5-20250929`, +# `bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0`, etc. + +# %% +MODEL = os.getenv("ACE_MODEL", "us.anthropic.claude-haiku-4-5-20251001-v1:0") + +print(f"Model: {MODEL}") + +# %% [markdown] +# ## 4. Build Roles +# +# The three ACE roles share the same LLM client. Each is independently +# customisable (prompt templates, retries, etc.). + +# %% +agent = Agent(MODEL) +reflector = Reflector(MODEL) +skill_manager = SkillManager(MODEL) + +print("Roles created: Agent, Reflector, SkillManager") + +# %% [markdown] +# ## 5. Define Training Samples + +# %% +samples = [ + Sample(question="What is the capital of France?", ground_truth="Paris"), + Sample(question="What is the capital of Japan?", ground_truth="Tokyo"), + Sample(question="What is the capital of Brazil?", ground_truth="Brasilia"), + Sample(question="What is the capital of Australia?", ground_truth="Canberra"), + Sample(question="What is the capital of Nigeria?", ground_truth="Abuja"), +] + +print(f"Prepared {len(samples)} training samples") + +# %% [markdown] +# --- +# ## 6. ACE Runner — Full Adaptive Pipeline +# +# The `ACE` runner is the full closed-loop pipeline: +# ``` +# Agent → Evaluate → Reflect → Tag → Update → Apply +# ``` +# +# It takes `Sample` objects and an optional `TaskEnvironment`. + +# %% [markdown] +# ### 6a. With SimpleEnvironment +# +# `SimpleEnvironment` checks if the ground truth appears in the agent's +# answer (case-insensitive substring match). + +# %% +skillbook = Skillbook() + +ace = ACE.from_roles( + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + environment=SimpleEnvironment(), + skillbook=skillbook, +) + +results = ace.run(samples[:3], epochs=1) + +print(f"Processed {len(results)} samples\n") +for r in results: + if r.error: + print(f" ERROR at {r.failed_at}: {r.error}") + elif r.output: + ctx: ACEStepContext = r.output + answer = ctx.agent_output.final_answer if ctx.agent_output else "N/A" + print(f" Q: {r.sample.question}") + print(f" A: {answer}") + +# %% +print(f"\nSkillbook after 1 epoch:") +print(f" Stats: {skillbook.stats()}") +for skill in skillbook.skills()[:5]: + print(f" - [{skill.id}] {skill.content}") + +# %% [markdown] +# ### 6b. Custom Environment +# +# Create your own evaluator by subclassing `TaskEnvironment`. + + +# %% +class ExactMatchEnvironment(TaskEnvironment): + """Strict evaluation: answer must exactly match ground truth.""" + + def evaluate(self, sample: Sample, agent_output: AgentOutput) -> EnvironmentResult: + expected = (sample.ground_truth or "").strip().lower() + predicted = agent_output.final_answer.strip().lower() + correct = expected in predicted + + return EnvironmentResult( + feedback=( + "Correct!" if correct else f"Wrong. Expected: {sample.ground_truth}" + ), + ground_truth=sample.ground_truth, + metrics={"accuracy": 1.0 if correct else 0.0}, + ) + + +print("ExactMatchEnvironment defined") + +# %% +skillbook2 = Skillbook() + +ace2 = ACE.from_roles( + agent=Agent(MODEL), + reflector=Reflector(MODEL), + skill_manager=SkillManager(MODEL), + environment=ExactMatchEnvironment(), + skillbook=skillbook2, +) + +results2 = ace2.run(samples[:2], epochs=1) + +for r in results2: + if r.output: + ctx = r.output + print(f" Q: {r.sample.question}") + print(f" A: {ctx.agent_output.final_answer if ctx.agent_output else 'N/A'}") + if ctx.reflections: + print(f" Insight: {ctx.reflections[0].key_insight}") + print() + +# %% [markdown] +# ### 6c. Without Environment +# +# When no environment is provided, `EvaluateStep` is a no-op. The Reflector +# still learns from ground-truth comparison in the trace. + +# %% +skillbook3 = Skillbook() + +ace3 = ACE.from_roles( + agent=Agent(MODEL), + reflector=Reflector(MODEL), + skill_manager=SkillManager(MODEL), + skillbook=skillbook3, + # No environment — EvaluateStep passes through +) + +results3 = ace3.run(samples[:2], epochs=1) +print(f"Processed {len(results3)} samples (no environment)") +print(f"Skills learned: {skillbook3.stats()}") + +# %% [markdown] +# ### 6d. Multi-Epoch Training +# +# Multiple epochs let the agent revisit samples with an evolving skillbook. +# Skills accumulate and refine across passes. + +# %% +skillbook4 = Skillbook() + +ace4 = ACE.from_roles( + agent=Agent(MODEL), + reflector=Reflector(MODEL), + skill_manager=SkillManager(MODEL), + environment=SimpleEnvironment(), + skillbook=skillbook4, +) + +results4 = ace4.run(samples, epochs=2) + +print(f"Total results across 2 epochs: {len(results4)}") +print(f"Skills learned: {skillbook4.stats()}") + +# Print per-epoch accuracy +for epoch in range(1, 3): + epoch_results = [r for r in results4 if r.output and r.output.epoch == epoch] + correct = sum( + 1 + for r in epoch_results + if r.output + and r.output.agent_output + and (r.sample.ground_truth or "").lower() + in r.output.agent_output.final_answer.lower() + ) + print(f" Epoch {epoch}: {correct}/{len(epoch_results)} correct") + +# %% [markdown] +# --- +# ## 7. Manual Step-by-Step Pipeline +# +# Under the hood, runners compose `Pipeline` objects from individual steps. +# Here we build one by hand to see exactly what each step does. +# All pipeline classes and steps are importable directly from `ace`. + +# %% +from ace import ( + Pipeline, + AgentStep, + EvaluateStep, + learning_tail, +) + +skillbook5 = Skillbook() +env = SimpleEnvironment() + +# Build the full pipeline manually +pipe = Pipeline( + [ + AgentStep(Agent(MODEL), skillbook5), + EvaluateStep(env), + *learning_tail(Reflector(MODEL), SkillManager(MODEL), skillbook5), + ] +) + +print(f"Pipeline steps: {len(pipe._steps)}") +print(f" requires: {pipe.requires}") +print(f" provides: {pipe.provides}") + +# %% [markdown] +# ### Run a single sample through the manual pipeline + +# %% +sample = samples[0] + +# Build the context the same way ACE._build_context() does +ctx = ACEStepContext( + sample=sample, + skillbook=SkillbookView(skillbook5), + epoch=1, + total_epochs=1, + step_index=0, + total_steps=1, + global_sample_index=0, +) + +print(f"Before pipeline:") +print(f" Skills: {skillbook5.stats()}") +print(f" agent_output: {ctx.agent_output}") + +# Run the full pipeline on a single context +from pipeline.protocol import SampleResult + +results_manual = pipe.run([ctx]) + +print(f"\nAfter pipeline:") +for r in results_manual: + if r.error: + print(f" ERROR: {r.error}") + elif r.output: + out: ACEStepContext = r.output + print( + f" Agent answer: {out.agent_output.final_answer if out.agent_output else 'N/A'}" + ) + print( + f" Reflector insight: {out.reflections[0].key_insight if out.reflections else 'N/A'}" + ) + print(f" Skills now: {skillbook5.stats()}") + +# %% [markdown] +# ### Using `learning_tail()` as a building block +# +# `learning_tail()` returns the standard learning steps: +# `[ReflectStep, UpdateStep]` (the agentic SkillManager mutates the +# skillbook directly via its tools). Optional deduplication and +# checkpoint steps are appended. + +# %% +skillbook6 = Skillbook() + +tail = learning_tail( + Reflector(MODEL), + SkillManager(MODEL), + skillbook6, +) + +print(f"learning_tail() returns {len(tail)} steps:") +for step in tail: + print(f" - {type(step).__name__}") + +# %% [markdown] +# --- +# ## 8. Checkpointing +# +# Save the skillbook every N successful samples so you can resume after +# interruption or compare skillbook evolution over time. + +# %% +skillbook7 = Skillbook() + +with tempfile.TemporaryDirectory() as tmpdir: + ace7 = ACE.from_roles( + agent=Agent(MODEL), + reflector=Reflector(MODEL), + skill_manager=SkillManager(MODEL), + environment=SimpleEnvironment(), + skillbook=skillbook7, + checkpoint_dir=tmpdir, + checkpoint_interval=2, # save every 2 successful samples + ) + + results7 = ace7.run(samples, epochs=1) + + saved = sorted(Path(tmpdir).glob("*.json")) + print("Checkpoint files:") + for f in saved: + print(f" {f.name} ({f.stat().st_size} bytes)") + +# %% [markdown] +# --- +# ## 9. Deduplication +# +# Merge near-duplicate skills to keep the skillbook compact. The +# `DeduplicationManager` runs periodically during training. + +# %% +from ace import DeduplicationManager, SimilarityDetector +from ace.protocols import DeduplicationConfig + +skillbook8 = Skillbook() + +dedup = DeduplicationManager(DeduplicationConfig(similarity_threshold=0.85)) + +ace8 = ACE.from_roles( + agent=Agent(MODEL), + reflector=Reflector(MODEL), + skill_manager=SkillManager(MODEL), + environment=SimpleEnvironment(), + skillbook=skillbook8, + dedup_manager=dedup, + dedup_interval=3, # run dedup every 3 samples +) + +results8 = ace8.run(samples, epochs=1) + +print(f"Skills after training with dedup: {skillbook8.stats()}") + +# %% [markdown] +# --- +# ## 10. Skillbook Persistence — Save & Reload +# +# Save the learned skillbook to disk and reload it in a future session. + +# %% +with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir) / "learned_skillbook.json" + + # Save + skillbook.save_to_file(str(path)) + print(f"Saved to {path.name} ({path.stat().st_size} bytes)") + + # Reload + reloaded = Skillbook.load_from_file(str(path)) + print(f"Reloaded: {reloaded.stats()}") + print(f"Stats match: {reloaded.stats() == skillbook.stats()}") + +# %% [markdown] +# --- +# ## 11. TraceAnalyser — Learning from Pre-Recorded Traces +# +# `TraceAnalyser` runs the learning tail only — no Agent, no Evaluate. +# Feed it raw trace dicts (the same shape ReflectStep expects) and it +# builds a skillbook from historical data. + +# %% +# Simulate some pre-recorded traces (e.g., from browser-use history logs) +traces = [ + { + "question": "Book a flight from NYC to London", + "reasoning": "Step 1: Opened booking site. Step 2: Searched flights. Step 3: Selected cheapest option.", + "answer": "Booked flight AA100 for $450", + "skill_ids": [], + "feedback": "Task succeeded in 3 steps", + "ground_truth": None, + }, + { + "question": "Find the cheapest hotel in Paris", + "reasoning": "Step 1: Opened hotel site. Step 2: Set filters. Step 3: Sorted by price. Step 4: Cookie popup blocked view.", + "answer": "Failed: could not dismiss cookie popup", + "skill_ids": [], + "feedback": "Task failed — cookie popup blocked interaction after step 3", + "ground_truth": None, + }, + { + "question": "Check weather in Tokyo", + "reasoning": "Step 1: Navigated to weather.com. Step 2: Searched Tokyo. Step 3: Read forecast.", + "answer": "Tokyo: 22C, partly cloudy", + "skill_ids": [], + "feedback": "Task succeeded in 3 steps — fast and accurate", + "ground_truth": None, + }, +] + +skillbook9 = Skillbook() + +analyser = TraceAnalyser.from_roles( + reflector=Reflector(MODEL), + skill_manager=SkillManager(MODEL), + skillbook=skillbook9, +) + +results9 = analyser.run(traces, epochs=1) + +print(f"Analysed {len(results9)} traces") +print(f"Skills learned: {skillbook9.stats()}") +for skill in skillbook9.skills()[:5]: + print(f" - [{skill.section}] {skill.content}") + +# %% [markdown] +# ### Multi-epoch trace analysis +# +# Each epoch re-processes all traces with the evolving skillbook. +# Early epochs extract obvious patterns; later epochs refine. + +# %% +skillbook10 = Skillbook() + +analyser2 = TraceAnalyser.from_roles( + reflector=Reflector(MODEL), + skill_manager=SkillManager(MODEL), + skillbook=skillbook10, +) + +results10 = analyser2.run(traces, epochs=2) + +print(f"Total results across 2 epochs: {len(results10)}") +print(f"Skills after 2 epochs: {skillbook10.stats()}") + +# %% [markdown] +# --- +# ## 12. Mixed Workflow — TraceAnalyser then ACE +# +# A common pattern: build an initial skillbook from historical traces, +# then deploy with live learning. + +# %% +# Phase 1: Build skillbook from historical data +shared_skillbook = Skillbook() + +analyser_phase1 = TraceAnalyser.from_roles( + reflector=Reflector(MODEL), + skill_manager=SkillManager(MODEL), + skillbook=shared_skillbook, +) +analyser_phase1.run(traces, epochs=1) + +print(f"Phase 1 — TraceAnalyser:") +print(f" Skills from traces: {shared_skillbook.stats()}") + +# Phase 2: Deploy with live ACE learning (reuse the evolved skillbook) +ace_phase2 = ACE.from_roles( + agent=Agent(MODEL), + reflector=Reflector(MODEL), + skill_manager=SkillManager(MODEL), + environment=SimpleEnvironment(), + skillbook=shared_skillbook, +) + +results_phase2 = ace_phase2.run(samples[:3], epochs=1) + +print(f"\nPhase 2 — ACE live learning:") +print(f" Processed {len(results_phase2)} samples") +print(f" Skills after live learning: {shared_skillbook.stats()}") + +# %% [markdown] +# --- +# ## 13. Error Handling +# +# Failed samples are captured in `SampleResult.error` — the pipeline +# never drops a sample silently. Other samples continue processing. + +# %% +bad_samples = [ + samples[0], + Sample(question="", ground_truth=""), # edge case: empty question + samples[1], +] + +skillbook11 = Skillbook() +ace11 = ACE.from_roles( + agent=Agent(MODEL), + reflector=Reflector(MODEL), + skill_manager=SkillManager(MODEL), + environment=SimpleEnvironment(), + skillbook=skillbook11, +) + +results11 = ace11.run(bad_samples, epochs=1) + +for i, r in enumerate(results11, 1): + status = "OK" if r.error is None else f"FAIL ({r.failed_at})" + if r.output and r.output.agent_output: + answer = r.output.agent_output.final_answer + else: + answer = "N/A" + print(f" [{i}] {status:20s} answer={answer}") + +# %% [markdown] +# --- +# ## 14. Inspecting the SkillbookView +# +# Steps receive a read-only `SkillbookView` on the context. +# This prevents accidental mutations from within pipeline steps. + +# %% +sb = Skillbook() +view = SkillbookView(sb) + +print(f"SkillbookView: {view}") +print(f" len: {len(view)}") +print(f" stats: {view.stats()}") +print(f" prompt: {view.as_prompt()[:200]}...") + +# Iterate over skills in the view +for skill in view: + print(f" - {skill.id}: {skill.content}") + +# %% [markdown] +# --- +# ## Summary +# +# | What | How | +# |------|-----| +# | Full pipeline | `ACE.from_roles(agent=..., reflector=..., skill_manager=...)` | +# | With environment | `ACE.from_roles(..., environment=SimpleEnvironment())` | +# | Without environment | `ACE.from_roles(...)` — EvaluateStep is a no-op | +# | Multi-epoch | `ace.run(samples, epochs=3)` | +# | Checkpointing | `ACE.from_roles(..., checkpoint_dir="./ckpts", checkpoint_interval=10)` | +# | Deduplication | `ACE.from_roles(..., dedup_manager=dedup, dedup_interval=5)` | +# | Trace analysis | `TraceAnalyser.from_roles(reflector=..., skill_manager=...)` | +# | Save skillbook | `ace.save("path.json")` or `skillbook.save_to_file("path.json")` | +# | Load skillbook | `Skillbook.load_from_file("path.json")` | +# | Manual steps | `Pipeline([AgentStep(a), EvaluateStep(e), *learning_tail(r, sm, sb)])` | +# | Learning tail | `learning_tail(reflector, skill_manager, skillbook)` | +# +# **Pipeline:** +# ``` +# ACE: Agent → Evaluate → Reflect → Tag → Update → Apply → [Dedup] → [Checkpoint] → [Opik] +# TraceAnalyser: Reflect → Tag → Update → Apply → [Dedup] → [Checkpoint] → [Opik] +# ``` diff --git a/examples/ace/mcp_client_demo.py b/examples/ace/mcp_client_demo.py new file mode 100644 index 0000000000000000000000000000000000000000..52785bd0f93ef5251ea441dd600a52b2e37a9194 --- /dev/null +++ b/examples/ace/mcp_client_demo.py @@ -0,0 +1,74 @@ +import asyncio +import os +import sys + +try: + from mcp.client.session import ClientSession + from mcp.client.stdio import get_default_environment + from mcp.client.stdio import stdio_client +except ModuleNotFoundError as exc: # pragma: no cover - optional dependency guard + if (exc.name or "").split(".")[0] == "mcp": + print( + "This example requires the optional MCP extra. " + 'Install it with `pip install "ace-framework[mcp]"` or ' + '`uv add "ace-framework[mcp]"`.', + file=sys.stderr, + ) + raise SystemExit(1) from exc + raise + +try: + from mcp.client.stdio import StdioServerParameters +except ImportError: # pragma: no cover - fallback for SDK layout variants + import mcp.client.stdio as mcp_stdio + + StdioServerParameters = mcp_stdio.StdioServerParameters + + +async def main(): + if len(sys.argv) < 2: + print( + "Usage: uv run python examples/ace/mcp_client_demo.py <path_to_ace_mcp_cli>" + ) + sys.exit(1) + + server_path = sys.argv[1] + + server_params = StdioServerParameters( + command=server_path, # usually "ace-mcp" or "uv" + args=["run", "ace-mcp"] if "uv" in server_path else [], + env={ + **get_default_environment(), + "OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", ""), + "GOOGLE_API_KEY": os.environ.get("GOOGLE_API_KEY", ""), + "GEMINI_API_KEY": os.environ.get("GEMINI_API_KEY", ""), + "MISTRAL_API_KEY": os.environ.get("MISTRAL_API_KEY", ""), + "ACE_MCP_DEFAULT_MODEL": os.environ.get("ACE_MCP_DEFAULT_MODEL", ""), + }, + ) + + async with stdio_client(server_params) as (read_stream, write_stream): + async with ClientSession(read_stream, write_stream) as session: + await session.initialize() + + print("Connected to ACE MCP Server.") + + # List available tools + tools = await session.list_tools() + print("\\nAvailable Tools:") + for tool in tools.tools: + print(f" - {tool.name}") + + print("\\nTesting ace.ask...") + ask_args = {"session_id": "demo-session-1", "question": "What is 2 + 2?"} + + result = await session.call_tool("ace.ask", ask_args) + if result.isError: + print(f"Error calling tool: {result.content}") + else: + for content in result.content: + print(f"Response: {content.text}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/ace/rr_benchmark.py b/examples/ace/rr_benchmark.py new file mode 100644 index 0000000000000000000000000000000000000000..ca3ddb3c2c58c0f4997f8f33fb146ab92bcf8508 --- /dev/null +++ b/examples/ace/rr_benchmark.py @@ -0,0 +1,525 @@ +#!/usr/bin/env python3 +"""E2E benchmark: RR (PydanticAI) over 30 traces. + +Generates 30 synthetic agent traces with known errors, runs the full +ACE learning pipeline (RRStep -> Tag -> Update -> Apply), and reports: +- Success rate (RR produced valid learnings) +- Skills extracted +- Timing + +Usage: + uv run python examples/ace/rr_benchmark.py + ACE_MODEL=bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 uv run python examples/ace/rr_benchmark.py +""" + +import json +import logging +import os +import sys +import time +from pathlib import Path + +from dotenv import load_dotenv + +_root = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(_root)) +load_dotenv(_root / ".env") + +from ace.core.skillbook import Skillbook +from ace.implementations import SkillManager +from ace.steps.rr_step import RRConfig, RRStep +from ace.runners.trace_analyser import TraceAnalyser + +MODEL = os.getenv("ACE_MODEL", "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0") + +logging.basicConfig(level=logging.WARNING, format="%(name)s | %(message)s") +logging.getLogger("ace.steps.rr").setLevel(logging.INFO) + + +# --------------------------------------------------------------------------- +# Synthetic traces — 30 traces with realistic agent errors +# --------------------------------------------------------------------------- + +TRACES = [ + # Math errors (5) + { + "question": "What is 17 x 23?", + "ground_truth": "391", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "17x23 = 17x20 + 17x3 = 340 + 51 = 381", + "answer": "381", + "skill_ids": [], + } + ], + }, + { + "question": "What is 144 / 12?", + "ground_truth": "12", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "144/12 = 14", + "answer": "14", + "skill_ids": [], + } + ], + }, + { + "question": "What is 25% of 80?", + "ground_truth": "20", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "25% of 80 = 80/25 = 3.2", + "answer": "3.2", + "skill_ids": [], + } + ], + }, + { + "question": "What is sqrt(169)?", + "ground_truth": "13", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "sqrt(169) is about 14", + "answer": "14", + "skill_ids": [], + } + ], + }, + { + "question": "What is 2^10?", + "ground_truth": "1024", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "2^10 = 2x10 = 20", + "answer": "20", + "skill_ids": [], + } + ], + }, + # Geography errors (5) + { + "question": "What is the capital of Australia?", + "ground_truth": "Canberra", + "feedback": "Incorrect. The capital is Canberra, not Sydney.", + "steps": [ + { + "role": "agent", + "reasoning": "Sydney is the largest city, so it must be the capital.", + "answer": "Sydney", + "skill_ids": [], + } + ], + }, + { + "question": "What is the capital of Brazil?", + "ground_truth": "Brasilia", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "Sao Paulo is the biggest city.", + "answer": "Sao Paulo", + "skill_ids": [], + } + ], + }, + { + "question": "What is the capital of Turkey?", + "ground_truth": "Ankara", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "Istanbul is the most famous city.", + "answer": "Istanbul", + "skill_ids": [], + } + ], + }, + { + "question": "What is the capital of Myanmar?", + "ground_truth": "Naypyidaw", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "Yangon is the largest city.", + "answer": "Yangon", + "skill_ids": [], + } + ], + }, + { + "question": "What is the capital of Nigeria?", + "ground_truth": "Abuja", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "Lagos is the most well-known city.", + "answer": "Lagos", + "skill_ids": [], + } + ], + }, + # Science errors (5) + { + "question": "What is the boiling point of water in Fahrenheit?", + "ground_truth": "212", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "Water boils at 100 degrees.", + "answer": "100", + "skill_ids": [], + } + ], + }, + { + "question": "How many chromosomes do humans have?", + "ground_truth": "46", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "Humans have 23 chromosomes.", + "answer": "23", + "skill_ids": [], + } + ], + }, + { + "question": "What is the speed of light in km/s?", + "ground_truth": "299,792", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "Speed of light is about 300,000 miles per second.", + "answer": "300,000 miles/s", + "skill_ids": [], + } + ], + }, + { + "question": "What is the atomic number of gold?", + "ground_truth": "79", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "Gold is Au, atomic number around 80.", + "answer": "80", + "skill_ids": [], + } + ], + }, + { + "question": "What planet is closest to the sun?", + "ground_truth": "Mercury", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "Venus is very hot so it must be closest.", + "answer": "Venus", + "skill_ids": [], + } + ], + }, + # Correct answers (5) — RR should find nothing or minimal learnings + { + "question": "What is 2+2?", + "ground_truth": "4", + "feedback": "Correct.", + "steps": [ + {"role": "agent", "reasoning": "2+2=4.", "answer": "4", "skill_ids": []} + ], + }, + { + "question": "What is the capital of France?", + "ground_truth": "Paris", + "feedback": "Correct.", + "steps": [ + { + "role": "agent", + "reasoning": "The capital of France is Paris.", + "answer": "Paris", + "skill_ids": [], + } + ], + }, + { + "question": "What color is the sky?", + "ground_truth": "Blue", + "feedback": "Correct.", + "steps": [ + { + "role": "agent", + "reasoning": "The sky appears blue due to Rayleigh scattering.", + "answer": "Blue", + "skill_ids": [], + } + ], + }, + { + "question": "How many days in a week?", + "ground_truth": "7", + "feedback": "Correct.", + "steps": [ + { + "role": "agent", + "reasoning": "A week has 7 days.", + "answer": "7", + "skill_ids": [], + } + ], + }, + { + "question": "What is H2O?", + "ground_truth": "Water", + "feedback": "Correct.", + "steps": [ + { + "role": "agent", + "reasoning": "H2O is the chemical formula for water.", + "answer": "Water", + "skill_ids": [], + } + ], + }, + # Reasoning errors (5) + { + "question": "If all roses are flowers and some flowers fade quickly, do all roses fade quickly?", + "ground_truth": "No, not necessarily.", + "feedback": "Incorrect. 'Some flowers fade quickly' doesn't mean all roses do.", + "steps": [ + { + "role": "agent", + "reasoning": "Roses are flowers, flowers fade, so roses fade.", + "answer": "Yes", + "skill_ids": [], + } + ], + }, + { + "question": "A bat and ball cost $1.10 total. The bat costs $1 more than the ball. How much is the ball?", + "ground_truth": "$0.05", + "feedback": "Incorrect. If ball=$0.10, bat=$1.10, total=$1.20.", + "steps": [ + { + "role": "agent", + "reasoning": "$1.10 - $1.00 = $0.10.", + "answer": "$0.10", + "skill_ids": [], + } + ], + }, + { + "question": "Is a whale a fish?", + "ground_truth": "No, a whale is a mammal.", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "Whales live in water, so they are fish.", + "answer": "Yes", + "skill_ids": [], + } + ], + }, + { + "question": "Can you fold a piece of paper more than 7 times?", + "ground_truth": "Yes, the record is 12 folds.", + "feedback": "Incorrect.", + "steps": [ + { + "role": "agent", + "reasoning": "It's physically impossible to fold paper more than 7 times.", + "answer": "No", + "skill_ids": [], + } + ], + }, + { + "question": "Does hot water freeze faster than cold water?", + "ground_truth": "It can, under certain conditions (Mpemba effect).", + "feedback": "Incorrect — the Mpemba effect is real.", + "steps": [ + { + "role": "agent", + "reasoning": "Hot water has more energy so it takes longer to cool.", + "answer": "No, never.", + "skill_ids": [], + } + ], + }, + # Tool-use errors (5) + { + "question": "Search for 'Python 3.12 release date'", + "ground_truth": "October 2, 2023", + "feedback": "Agent searched for wrong query.", + "steps": [ + { + "role": "agent", + "reasoning": "I'll search for 'Python release'.", + "answer": "Python 3.11 was released Oct 2022", + "skill_ids": [], + }, + { + "role": "agent", + "reasoning": "Found a result about 3.11.", + "answer": "October 2022", + "skill_ids": [], + }, + ], + }, + { + "question": "Convert 100 USD to EUR", + "ground_truth": "~92 EUR", + "feedback": "Agent used wrong conversion direction.", + "steps": [ + { + "role": "agent", + "reasoning": "1 EUR = 1.08 USD, so 100*1.08=108.", + "answer": "108 EUR", + "skill_ids": [], + } + ], + }, + { + "question": "Get weather in Tokyo", + "ground_truth": "22C, partly cloudy", + "feedback": "Agent misspelled the city name.", + "steps": [ + { + "role": "agent", + "reasoning": "I'll call weather API for Tokio.", + "answer": "Error: 404 city not found", + "skill_ids": [], + }, + { + "role": "agent", + "reasoning": "API failed, I'll guess.", + "answer": "Probably warm and humid", + "skill_ids": [], + }, + ], + }, + { + "question": "Calculate the area of a circle with radius 5", + "ground_truth": "78.54", + "feedback": "Agent used diameter instead of radius.", + "steps": [ + { + "role": "agent", + "reasoning": "Area = pi * r^2 = pi * 10^2 = 314.16", + "answer": "314.16", + "skill_ids": [], + } + ], + }, + { + "question": "List files in /home/user/docs", + "ground_truth": "report.pdf, notes.txt, budget.xlsx", + "feedback": "Agent listed wrong directory.", + "steps": [ + { + "role": "agent", + "reasoning": "I'll list /home/user.", + "answer": "Desktop/, docs/, Downloads/", + "skill_ids": [], + } + ], + }, +] + +assert len(TRACES) == 30, f"Expected 30 traces, got {len(TRACES)}" + + +# --------------------------------------------------------------------------- +# Run benchmark +# --------------------------------------------------------------------------- + + +def main(): + print(f"Model: {MODEL}") + print(f"Traces: {len(TRACES)}") + print(f"{'=' * 60}") + + skillbook = Skillbook() + rr = RRStep( + MODEL, + config=RRConfig( + max_requests=20, + timeout=15.0, + ), + ) + sm = SkillManager(MODEL) + + analyser = TraceAnalyser.from_roles( + reflector=rr, + skill_manager=sm, + skillbook=skillbook, + ) + + t0 = time.time() + results = analyser.run(TRACES, epochs=1) + elapsed = time.time() - t0 + + # Report + print(f"\n{'=' * 60}") + print(f" BENCHMARK RESULTS") + print(f"{'=' * 60}") + + successes = sum(1 for r in results if r.error is None) + failures = sum(1 for r in results if r.error is not None) + print(f"\n Traces processed: {len(results)}/{len(TRACES)}") + print(f" Successes: {successes}") + print(f" Failures: {failures}") + print(f" Time: {elapsed:.1f}s ({elapsed/len(TRACES):.1f}s/trace)") + + if failures > 0: + print(f"\n Errors:") + for r in results: + if r.error is not None: + print(f" - {r.error}") + + skills = skillbook.skills() + print(f"\n Skills extracted: {len(skills)}") + for s in skills[:20]: + print(f" [{s.id}] {s.content[:80]}") + if len(skills) > 20: + print(f" ... and {len(skills) - 20} more") + + # Save results + out_dir = _root / "examples" / "ace" / "benchmark_output" + out_dir.mkdir(exist_ok=True) + skillbook.save_to_file(str(out_dir / "skillbook.json")) + print(f"\n Skillbook saved to: {out_dir / 'skillbook.json'}") + + # Summary + print(f"\n{'=' * 60}") + rate = successes / len(results) * 100 if results else 0 + print(f" Success rate: {rate:.0f}%") + print(f" Skills learned: {len(skills)}") + print(f" Total time: {elapsed:.1f}s") + print(f"{'=' * 60}") + + return 0 if rate >= 80 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/ace/rr_demo.py b/examples/ace/rr_demo.py new file mode 100644 index 0000000000000000000000000000000000000000..63ef08ddf8a134b547021af7771b148a0cbc6cbd --- /dev/null +++ b/examples/ace/rr_demo.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python3 +"""Demo of the Recursive Reflector (RR) pipeline with a real LLM. + +Shows the RR analyzing agent traces, iterating in its Python REPL sandbox, +and producing structured learnings. Requires an API key for LiteLLM. + +Usage: + # Default model (Bedrock Claude Haiku): + uv run python examples/ace/rr_demo.py + + # Custom model: + ACE_MODEL=bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0 uv run python examples/ace/rr_demo.py +""" + +import json +import logging +import os +import sys +from pathlib import Path + +from dotenv import load_dotenv + +# Ensure project root is importable +_root = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(_root)) +load_dotenv(_root / ".env") + +from ace.steps.rr_step import RRConfig, RRStep, TraceSandbox +from ace.core.context import ACEStepContext, SkillbookView +from ace.core.skillbook import Skillbook + +MODEL = os.getenv("ACE_MODEL", "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0") + +# Show what the RR is doing at each iteration +logging.basicConfig( + level=logging.INFO, + format=" %(name)s | %(message)s", +) +# Quiet the noisy libraries +for name in ("LiteLLM", "litellm", "httpx", "httpcore"): + logging.getLogger(name).setLevel(logging.WARNING) + + +def section(name: str) -> None: + print(f"\n{'=' * 60}\n {name}\n{'=' * 60}\n") + + +def print_result(result): + """Print a ReflectorOutput nicely.""" + print(f"\n --- Result ---") + print(f" Reasoning: {result.reasoning[:300]}") + print(f" Key insight: {result.key_insight}") + if result.error_identification: + print(f" Error: {result.error_identification}") + if result.root_cause_analysis: + print(f" Root cause: {result.root_cause_analysis}") + if result.correct_approach: + print(f" Correct approach: {result.correct_approach}") + raw = result.raw or {} + if "rr_trace" in raw: + rt = raw["rr_trace"] + print(f"\n RR trace: depth={rt.get('depth')}, " + f"iterations={rt.get('total_iterations')}, " + f"compactions={rt.get('compactions')}, " + f"timed_out={rt.get('timed_out')}") + if "usage" in raw: + u = raw["usage"] + print(f" Usage: {u.get('input_tokens')} in, " + f"{u.get('output_tokens')} out, " + f"{u.get('total_tokens')} total, " + f"{u.get('requests')} requests") + + +# --------------------------------------------------------------------------- +# Demo 1: RRStep — agent got the wrong answer (simple) +# --------------------------------------------------------------------------- + + +def demo_wrong_answer(): + """RR analyzes a trace where the agent answered incorrectly.""" + section("Demo 1: RRStep — wrong answer") + + rr = RRStep( + MODEL, + config=RRConfig(max_requests=15, max_depth=0), + ) + + ctx = ACEStepContext( + trace={ + "question": "What is the largest planet in our solar system by mass?", + "ground_truth": "Jupiter", + "feedback": "Incorrect. The correct answer is Jupiter, not Saturn.", + "steps": [ + { + "role": "agent", + "reasoning": ( + "The user is asking about the largest planet. " + "Saturn has those huge rings and is very large. " + "I'll go with Saturn." + ), + "answer": "Saturn", + "skill_ids": [], + } + ], + }, + skillbook=SkillbookView(Skillbook()), + ) + + result_ctx = rr(ctx) + print_result(result_ctx.reflections[0]) + + +# --------------------------------------------------------------------------- +# Demo 2: RRStep — multi-step tool-use failure +# --------------------------------------------------------------------------- + + +def demo_tool_failure(): + """RR analyzes a trace with tool-use errors.""" + section("Demo 2: RRStep — tool-use failure trace") + + rr = RRStep( + MODEL, + config=RRConfig(max_requests=15, max_depth=0), + ) + + ctx = ACEStepContext( + trace={ + "question": "What's the current weather in Tokyo?", + "ground_truth": '{"temp_c": 22, "condition": "partly cloudy", "humidity": 65}', + "feedback": ( + "Failed. Agent called the weather API with 'Tokio' (misspelled) " + "and got a 404 error, then guessed instead of retrying." + ), + "steps": [ + { + "role": "agent", + "reasoning": ( + "I need to call the weather API for Tokyo. " + "Let me use get_weather(city='Tokio')." + ), + "answer": "Error: 404 - City 'Tokio' not found", + "skill_ids": [], + }, + { + "role": "agent", + "reasoning": ( + "The API returned an error. I'll estimate based on " + "general knowledge — Tokyo is warm in summer." + ), + "answer": "It's probably around 28C and sunny in Tokyo.", + "skill_ids": [], + }, + ], + }, + skillbook=SkillbookView(Skillbook()), + ) + + result_ctx = rr(ctx) + print_result(result_ctx.reflections[0]) + + +# --------------------------------------------------------------------------- +# Demo 3: Real benchmark trace (if available) +# --------------------------------------------------------------------------- + + +def demo_real_trace(): + """RR analyzes a real benchmark trace.""" + section("Demo 3: Real benchmark trace") + + traces_path = _root / "ace-eval" / "results" / "benchmarks" / "bench_20260314_154608" / "benchmark" / "traces.json" + if not traces_path.exists(): + print(" Benchmark traces not found, skipping.") + return + + data = json.loads(traces_path.read_text()) + # Find a failed trace (reward=0) + trace_dict = None + for key, val in data.items(): + for trial in val.get("trials", []): + if trial.get("reward", 1.0) == 0.0 and trial.get("trace"): + trace_dict = trial["trace"] + print(f" Using trace: task {key}, question: {trace_dict.get('question', '')[:100]}...") + break + if trace_dict: + break + + if not trace_dict: + print(" No failed traces found, skipping.") + return + + rr = RRStep( + MODEL, + config=RRConfig(max_requests=20, max_depth=0), + ) + + ctx = ACEStepContext( + trace=trace_dict, + skillbook=SkillbookView(Skillbook()), + ) + + result_ctx = rr(ctx) + print_result(result_ctx.reflections[0]) + + +# --------------------------------------------------------------------------- +# Demo 4: Batch traces with recursion +# --------------------------------------------------------------------------- + + +def demo_batch_recursion(): + """RR analyzes multiple traces using recurse tool.""" + section("Demo 4: Batch traces with recursion (depth=1)") + + traces_path = _root / "ace-eval" / "results" / "benchmarks" / "bench_20260314_154608" / "benchmark" / "traces.json" + if not traces_path.exists(): + print(" Benchmark traces not found, skipping.") + return + + data = json.loads(traces_path.read_text()) + # Collect first 3 failed traces as batch items + batch_items = [] + for key, val in data.items(): + for trial in val.get("trials", []): + if trial.get("reward", 1.0) == 0.0 and trial.get("trace"): + t = trial["trace"] + batch_items.append({ + "task_id": f"task_{key}", + "question": t.get("question", ""), + "feedback": t.get("feedback", ""), + "trace": t, + }) + if len(batch_items) >= 3: + break + if len(batch_items) >= 3: + break + + if len(batch_items) < 2: + print(f" Only {len(batch_items)} failed traces found, need at least 2. Skipping.") + return + + print(f" Batch: {len(batch_items)} failed traces") + for bi in batch_items: + print(f" - {bi['task_id']}: {bi['question'][:80]}...") + + rr = RRStep( + MODEL, + config=RRConfig( + max_requests=30, + max_depth=1, # allow one level of recursion + ), + ) + + ctx = ACEStepContext( + trace={ + "question": "Analyze these failed agent traces and extract common patterns", + "batch_items": batch_items, + "item_ids": [bi["task_id"] for bi in batch_items], + }, + skillbook=SkillbookView(Skillbook()), + ) + + result_ctx = rr(ctx) + for i, ref in enumerate(result_ctx.reflections): + print(f"\n --- Reflection {i} ---") + print_result(ref) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="RR Demo") + parser.add_argument("--demo", type=int, default=0, + help="Run specific demo (1-4), 0=all") + args = parser.parse_args() + + print(f"Model: {MODEL}") + + demos = { + 1: demo_wrong_answer, + 2: demo_tool_failure, + 3: demo_real_trace, + 4: demo_batch_recursion, + } + + if args.demo: + demos[args.demo]() + else: + for d in demos.values(): + d() + + section("Done") diff --git a/examples/ace/rr_stress_test.py b/examples/ace/rr_stress_test.py new file mode 100644 index 0000000000000000000000000000000000000000..81a1bbab68a93257449912e6dab3c290dde022f3 --- /dev/null +++ b/examples/ace/rr_stress_test.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Stress test: RR analyzing 30 real benchmark traces at once. + +Usage: + uv run python examples/ace/rr_stress_test.py +""" + +import json +import logging +import os +import sys +import time +from pathlib import Path + +from dotenv import load_dotenv + +_root = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(_root)) +load_dotenv(_root / ".env") + +from ace.steps.rr_step import RRConfig, RRStep +from ace.core.context import ACEStepContext, SkillbookView +from ace.core.skillbook import Skillbook + +MODEL = os.getenv("ACE_MODEL", "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0") + +logging.basicConfig(level=logging.INFO, format=" %(name)s | %(message)s") +for name in ("LiteLLM", "litellm", "httpx", "httpcore"): + logging.getLogger(name).setLevel(logging.WARNING) + + +def load_traces(n: int = 30) -> list[dict]: + """Load n traces from benchmark results, deduplicating by task.""" + traces_path = _root / "ace-eval" / "results" / "benchmarks" / "bench_20260314_154608" / "benchmark" / "traces.json" + data = json.loads(traces_path.read_text()) + + batch = [] + for key, val in data.items(): + for trial in val.get("trials", []): + if trial.get("trace"): + t = trial["trace"] + batch.append({ + "task_id": f"task_{key}_r{trial.get('reward', '?')}", + "question": t.get("question", ""), + "feedback": t.get("feedback", ""), + "trace": t, + }) + if len(batch) >= n: + return batch + return batch + + +def main(): + traces = load_traces(30) + print(f"Model: {MODEL}") + print(f"Loaded {len(traces)} traces") + print(f"Total trace chars: {sum(len(t['trace'].get('reasoning', '')) for t in traces):,}") + print() + + rr = RRStep( + MODEL, + config=RRConfig( + max_requests=80, + max_depth=1, # allow recursion + max_tokens=1_500_000, + ), + ) + + ctx = ACEStepContext( + trace={ + "question": "Analyze these agent traces from a customer service benchmark. Identify common failure patterns, categorize them, and extract actionable learnings.", + "batch_items": traces, + "item_ids": [t["task_id"] for t in traces], + }, + skillbook=SkillbookView(Skillbook()), + ) + + print("Running RR...") + t0 = time.time() + result_ctx = rr(ctx) + elapsed = time.time() - t0 + + print(f"\n{'=' * 60}") + print(f" Completed in {elapsed:.1f}s") + print(f" Reflections: {len(result_ctx.reflections)}") + print(f"{'=' * 60}\n") + + for i, ref in enumerate(result_ctx.reflections): + print(f"--- Reflection {i} ---") + print(f" Reasoning: {ref.reasoning[:200]}...") + print(f" Key insight: {ref.key_insight[:200] if ref.key_insight else '(none)'}") + if ref.error_identification: + print(f" Error: {ref.error_identification[:200]}") + if ref.root_cause_analysis: + print(f" Root cause: {ref.root_cause_analysis[:200]}") + if ref.correct_approach: + print(f" Correct approach: {ref.correct_approach[:200]}") + raw = ref.raw or {} + if "rr_trace" in raw: + rt = raw["rr_trace"] + print(f" RR trace: depth={rt.get('depth')}, iters={rt.get('total_iterations')}, " + f"compactions={rt.get('compactions')}, timed_out={rt.get('timed_out')}") + if "usage" in raw: + u = raw["usage"] + print(f" Usage: {u.get('total_tokens'):,} tokens, {u.get('requests')} requests") + print() + + +if __name__ == "__main__": + main() diff --git a/examples/ace/smoke_test.py b/examples/ace/smoke_test.py new file mode 100644 index 0000000000000000000000000000000000000000..8807ff0b8bb209bea2270e6d697f85e5eaf7cb7a --- /dev/null +++ b/examples/ace/smoke_test.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""Focused e2e smoke test for ace — exercises core runners with a real LLM. + +Verifies that each runner actually generates insights (skills with non-empty +content), not just that the pipeline runs without errors. + +Usage: + ACE_MODEL=anthropic/claude-haiku-4-5-20251001 uv run python examples/ace/smoke_test.py +""" + +import os +import sys +import tempfile +from pathlib import Path + +import nest_asyncio + +nest_asyncio.apply() + +# Ensure project root is importable +_root = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(_root)) + +from dotenv import load_dotenv + +load_dotenv(_root / ".env") + +from ace import ( + ACE, + ACELiteLLM, + Agent, + Reflector, + Sample, + SimpleEnvironment, + Skillbook, + SkillManager, + TraceAnalyser, +) + +MODEL = os.getenv("ACE_MODEL", "anthropic/claude-haiku-4-5-20251001") +passed = 0 +total = 5 + + +def section(name: str) -> None: + print(f"\n{'='*60}\n {name}\n{'='*60}") + + +def assert_skills_have_content(skillbook: Skillbook, label: str) -> None: + """Verify every skill has a non-empty content field.""" + for skill in skillbook.skills(): + assert ( + skill.content and skill.content.strip() + ), f"{label}: skill {skill.id} has empty content" + + +# ── Shared setup ──────────────────────────────────────────── +agent = Agent(MODEL) +reflector = Reflector(MODEL) +skill_manager = SkillManager(MODEL) + +# ── 1. ACE runner (full pipeline) ─────────────────────────── +section("1. ACE runner — 3 samples, 1 epoch") +skillbook = Skillbook() +ace = ACE.from_roles( + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + environment=SimpleEnvironment(), + skillbook=skillbook, +) +results = ace.run( + [ + Sample(question="What is the capital of France?", ground_truth="Paris"), + Sample(question="What is the capital of Japan?", ground_truth="Tokyo"), + Sample(question="What is the capital of Brazil?", ground_truth="Brasilia"), + ], + epochs=1, +) +assert len(results) == 3, f"Expected 3 results, got {len(results)}" +errors = [r for r in results if r.error] +assert not errors, f"Pipeline errors: {errors}" + +# Verify agent produced answers +for r in results: + assert r.output is not None, f"No output for {r.sample.question}" + ao = getattr(r.output, "agent_output", None) + assert ao is not None, f"No agent_output for {r.sample.question}" + assert ao.final_answer.strip(), f"Empty answer for {r.sample.question}" + +# Verify insights were generated +ace_skill_count = len(skillbook.skills()) +assert ace_skill_count > 0, "ACE runner produced zero skills" +assert_skills_have_content(skillbook, "ACE runner") +print(f" OK — {len(results)} results, {ace_skill_count} skills learned") +for s in skillbook.skills()[:3]: + print(f" [{s.id}] {s.content[:70]}") +passed += 1 + +# ── 2. TraceAnalyser ──────────────────────────────────────── +section("2. TraceAnalyser — 2 pre-recorded traces") +skills_before = len(skillbook.skills()) +analyser = TraceAnalyser.from_roles( + reflector=reflector, + skill_manager=skill_manager, + skillbook=skillbook, # continues from ACE run +) +traces = [ + { + "question": "Translate 'hello' to Spanish", + "answer": "hola", + "feedback": "Correct! Simple and accurate.", + }, + { + "question": "What is 12 * 15?", + "answer": "170", + "feedback": "Incorrect. The correct answer is 180.", + }, +] +trace_results = analyser.run(traces, epochs=1) +assert len(trace_results) == 2, f"Expected 2 results, got {len(trace_results)}" +trace_errors = [r for r in trace_results if r.error] +assert not trace_errors, f"TraceAnalyser errors: {trace_errors}" + +# Verify new insights were added +skills_after = len(skillbook.skills()) +new_skills = skills_after - skills_before +assert ( + new_skills > 0 +), f"TraceAnalyser added zero new skills (before={skills_before}, after={skills_after})" +assert_skills_have_content(skillbook, "TraceAnalyser") +print( + f" OK — {len(trace_results)} traces, {new_skills} new skills, {skills_after} total" +) +passed += 1 + +# ── 3. ACELiteLLM ask + learn_from_feedback ───────────────── +section("3. ACELiteLLM — ask + learn_from_feedback") +llm_skillbook = Skillbook() +ace_llm = ACELiteLLM(MODEL, skillbook=llm_skillbook) +answer = ace_llm.ask("What colour is the sky on a clear day?") +assert isinstance(answer, str) and len(answer.strip()) > 0, f"Bad answer: {answer!r}" +print(f" ask() → {answer[:80]}") + +learned = ace_llm.learn_from_feedback( + feedback="Good answer but could mention why (Rayleigh scattering).", + ground_truth="Blue", +) +assert learned, "learn_from_feedback returned False" + +# Verify insights were generated +llm_skill_count = len(ace_llm.skillbook.skills()) +assert llm_skill_count > 0, "learn_from_feedback produced zero skills" +assert_skills_have_content(ace_llm.skillbook, "ACELiteLLM") + +# Verify as_prompt() returns something useful +prompt = ace_llm.skillbook.as_prompt() +assert ( + prompt and len(prompt.strip()) > 0 +), "Skillbook as_prompt() is empty after learning" +print(f" learn_from_feedback() → OK, {llm_skill_count} skills") +print(f" as_prompt() → {len(prompt)} chars") +passed += 1 + +# ── 4. Skillbook persistence ──────────────────────────────── +section("4. Skillbook persistence — save, reload, verify") +with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f: + tmp_path = f.name +try: + skillbook.save_to_file(tmp_path) + reloaded = Skillbook.load_from_file(tmp_path) + + # Verify counts match + orig_stats = skillbook.stats() + new_stats = reloaded.stats() + assert ( + orig_stats["skills"] == new_stats["skills"] + ), f"Skill count mismatch: {orig_stats} vs {new_stats}" + + # Verify content survives round-trip + orig_prompt = skillbook.as_prompt() + reloaded_prompt = reloaded.as_prompt() + assert ( + orig_prompt == reloaded_prompt + ), f"as_prompt() differs after reload:\n original: {orig_prompt[:100]}...\n reloaded: {reloaded_prompt[:100]}..." + + # Verify individual skill content preserved + orig_ids = {s.id for s in skillbook.skills()} + reloaded_ids = {s.id for s in reloaded.skills()} + assert orig_ids == reloaded_ids, f"Skill IDs differ: {orig_ids} vs {reloaded_ids}" + + print( + f" OK — saved/loaded {orig_stats['skills']} skills, content round-trip verified" + ) + passed += 1 +finally: + Path(tmp_path).unlink(missing_ok=True) + +# ── 5. max_retries wiring ─────────────────────────────────── +section("5. max_retries wiring") +a = Agent(MODEL, max_retries=5) +r = Reflector(MODEL, max_retries=7) +s = SkillManager(MODEL, max_retries=9) +assert a.max_retries == 5, f"Agent max_retries={a.max_retries}" +assert r.max_retries == 7, f"Reflector max_retries={r.max_retries}" +assert s.max_retries == 9, f"SkillManager max_retries={s.max_retries}" +# Verify defaults +a_default = Agent(MODEL) +r_default = Reflector(MODEL) +s_default = SkillManager(MODEL) +assert a_default.max_retries == 3, f"Agent default max_retries={a_default.max_retries}" +assert ( + r_default.max_retries == 3 +), f"Reflector default max_retries={r_default.max_retries}" +assert ( + s_default.max_retries == 3 +), f"SkillManager default max_retries={s_default.max_retries}" +print(" OK — custom: Agent=5, Reflector=7, SkillManager=9; defaults=3") +passed += 1 + +# ── Summary ───────────────────────────────────────────────── +section(f"RESULT: {passed}/{total} passed") +sys.exit(0 if passed == total else 1) diff --git a/examples/agentic-system-prompting/README.md b/examples/agentic-system-prompting/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2e2c3aa474dd46ca0e6f60b7cc80ea71d41458aa --- /dev/null +++ b/examples/agentic-system-prompting/README.md @@ -0,0 +1,243 @@ +<img src="https://framerusercontent.com/images/XBGa12hY8xKYI6KzagBxpbgY4.png" alt="Kayba Logo" width="1080"/> + +# Agent Prompt Optimizer + +![GitHub stars](https://img.shields.io/github/stars/kayba-ai/agentic-context-engine?style=social) +[![Discord](https://img.shields.io/discord/1429935408145236131?label=Discord&logo=discord&logoColor=white&color=5865F2)](https://discord.gg/mqCqH7sTyK) +[![Twitter Follow](https://img.shields.io/twitter/follow/kaybaai?style=social)](https://twitter.com/kaybaai) +[![PyPI version](https://badge.fury.io/py/ace-framework.svg)](https://badge.fury.io/py/ace-framework) +[![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/) +![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-yellow.svg) + +## The Problem with Manual System Prompting + +- Time-consuming iteration cycles of trial and error +- Prompt drift and regression as you patch edge cases +- No systematic learning from agent failures +- Knowledge stays in your head instead of in the prompt +- Hard to manage as prompts scale + +## The Solution + +ACE (Agentic Context Engine) automatically optimizes your agent's system prompt by learning from execution. It observes agent runs, analyzes what strategies worked and what failed, then generates actionable insights for the system prompt. + +`Traces / Conversations` **→** `ACE` **→** `Prompt Suggestions` + +You put in past traces or conversations. ACE handles agentic system prompting by learning from mistakes. You receive improved system prompt suggestions. + +**How it works:** + +0. **Prepare your data** - Export/convert your agent conversations to `.md` or `.toon` files and place them in a directory. To convert JSON to TOON, use the included `convert.py` script or the toon library directly. The more detailed your traces, the better the insights. + +1. **ReplayAgent** - Simulates an agent for offline learning from your trace/conversation +2. **Reflector** - Analyzes each conversation to identify what worked, what failed, and why +3. **SkillManager** - Transforms reflections into atomic, actionable prompt strategies/insights +4. **Deduplicator** - Consolidates similar strategies/insights using embeddings to keep the output clean +5. **Skillbook** - Output file stores all prompt strategies/insights in a human-readable format you can review and implement + +The output is a **human-readable skillbook** where each insight contains: +- **Prompt suggestion** - The recommended text to add to your system prompt +- **Justification** - Why this change would help based on the analysis +- **Evidence** - What actually happened in the trace that led to this insight +You review each suggestion and decide what to copy into your system prompt. ACE may even suggest strategies that contradict your current prompt when it identifies flaws in the original design. + +## Setup + +### Installation + +```bash +pip install ace-framework +``` + +Or for development: +```bash +git clone https://github.com/kayba-ai/agentic-context-engine +cd agentic-context-engine +uv sync +uv pip install -e . # Required to run examples +``` + +### API Keys + +**Requirements:** +- LLM API key for analysis (e.g., `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.) +- `OPENAI_API_KEY` for deduplication (uses OpenAI embeddings) + +Create a `.env` file in the project root: + +```bash +# Required for analysis (choose one) +OPENAI_API_KEY=your-openai-key +# OR +ANTHROPIC_API_KEY=your-anthropic-key + +# Required for deduplication (uses OpenAI embeddings) +OPENAI_API_KEY=your-openai-key +``` + +## Implementation + +### Agentic System Prompting = ACE Offline Adapter + +Process past traces and conversations in batch to generate insights **without the agent running**. + +**Use case:** Periodic automated system prompt revision. Feed historical data, let ACE analyze patterns, then have a human review and choose what to implement. + +#### Quick Start (CLI) + +```bash +# Basic usage +python agentic_system_prompting.py /path/to/traces + +# With options +python agentic_system_prompting.py /path/to/traces --model gpt-4o --epochs 2 +python agentic_system_prompting.py /path/to/traces --input-skillbook existing.json +python agentic_system_prompting.py /path/to/traces --output-dir ./results --threshold 0.8 +``` + +**CLI Options:** +- `traces_dir` - Required: path to directory containing `.md` or `.toon` trace files +- `-m, --model` - LLM model for analysis (default: `claude-haiku-4-5-20251001`) +- `-e, --epochs` - Number of training epochs (default: 1) +- `-t, --threshold` - Deduplication similarity threshold 0.0-1.0 (default: 0.7) +- `-i, --input-skillbook` - Continue learning from an existing skillbook +- `-o, --output-dir` - Output directory for results (default: script directory) + +**Outputs:** +- `skillbook_{timestamp}.json` - The learned skillbook +- `skills_{timestamp}.md` - Human-readable skills grouped by section +- `external_agent_injection_{timestamp}.txt` - Ready-to-inject prompt text for external agents + +#### Python API + +```python +from ace import ( + Skillbook, + Sample, + OfflineACE, + Reflector, + SkillManager, + ReplayAgent, + SimpleEnvironment, +) +from ace.llm_providers.litellm_client import LiteLLMClient, LiteLLMConfig +from ace.prompts_v3 import PromptManager + +# 1. Initialize LLM client +config = LiteLLMConfig( + model="claude-sonnet-4-5-20250929", + max_tokens=8192, + temperature=0.1, +) +llm = LiteLLMClient(config=config) +prompt_mgr = PromptManager() + +# 2. Create ACE components +skillbook = Skillbook() +agent = ReplayAgent() # Dummy agent that replays conversations +reflector = Reflector(llm=llm, prompt_template=prompt_mgr.get_reflector_prompt()) +skill_manager = SkillManager(llm=llm, prompt_template=prompt_mgr.get_skill_manager_prompt()) + +# 3. Load past conversations as samples +samples = [ + Sample( + question="Your task description here", + context="The full conversation/trace content", + ground_truth="", # Empty for analysis tasks + metadata={"source": "conversation_1"} + ), + # ... more historical data +] + +# 4. Create adapter and run +environment = SimpleEnvironment() +adapter = OfflineACE( + skillbook=skillbook, + agent=agent, + reflector=reflector, + skill_manager=skill_manager, +) +results = adapter.run(samples, environment, epochs=1) + +# 5. Review and save generated skills +print(adapter.skillbook.as_prompt()) +adapter.skillbook.save_to_file("offline_adapter_skillbook.json") +``` + +**Tip:** Enable deduplication to automatically consolidate similar skills during learning. This keeps the skillbook clean. + +```python +from ace import DeduplicationConfig + +dedup_config = DeduplicationConfig( + enabled=True, + similarity_threshold=0.85, +) + +adapter = OfflineACE( + skillbook=skillbook, + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + dedup_config=dedup_config, +) +``` + +#### Async Mode + +For large batches, enable async learning so the Reflector and SkillManager process in the background: + +```python +adapter = OfflineACE( + skillbook=skillbook, + agent=agent, + reflector=reflector, + skill_manager=skill_manager, + async_learning=True, + max_reflector_workers=3, +) + +results = adapter.run(samples, environment) +``` + +#### Checkpoints + +Save skillbook periodically during long training runs: + +```python +results = adapter.run( + samples=samples, + environment=environment, + epochs=3, + checkpoint_interval=10, # Save every 10 samples + checkpoint_dir="./checkpoints", +) +``` + +### Agentic Prompting at Runtime = Online Adapter + +Fully autonomous self-improving agents at runtime. The agent learns from every interaction, generates insights, and injects them into future contexts automatically - no manual intervention required. + +**Use case:** Continuous improvement in production where agents get better with every run. + +See the [Quick Start Guide](../../docs/QUICKSTART.md) for setup instructions. + +- **LiteLLM:** [`examples/litellm/`](../litellm/) - Make a new agent that self-learns +- **LangChain:** [`examples/langchain/`](../langchain/) - Wrap your existing agent with self-improving + +## FAQ + +**Can I combine manual prompts with ACE skills?** +Yes. ACE skills complement your base prompts. Start with manual prompts and let ACE build domain-specific expertise on top. + +**What if ACE suggests something that contradicts my system prompt?** +Review it. ACE may have identified a flaw in your original design. The skillbook is human-readable JSON - you decide what to keep. + +**Can I share skills between agents?** +Yes. Skillbooks are portable JSON files with human readable text. + +## Next Steps + +- Explore [examples](../) in this repository +- Read the [main documentation](https://github.com/kayba-ai/agentic-context-engine) +- Join our [Discord](https://discord.gg/mqCqH7sTyK) for tips and support diff --git a/examples/agentic-system-prompting/external_agent_injection_20260318_150822.txt b/examples/agentic-system-prompting/external_agent_injection_20260318_150822.txt new file mode 100644 index 0000000000000000000000000000000000000000..58e740311ddc291e54ca48dd216b210e5cbea337 --- /dev/null +++ b/examples/agentic-system-prompting/external_agent_injection_20260318_150822.txt @@ -0,0 +1,26 @@ + +## Available Strategic Knowledge (Learned from Experience) + +The following strategies have been learned from previous task executions. +Each skill shows its success rate based on helpful/harmful feedback: + +skills[6 ]{id section content helpful harmful neutral}: + customer_service_structure-00001 customer_service_structure "Structure every customer service interaction into 4 phases: (1) Acknowledge request in 1 message, (2) Collect all required information in 1-2 focused turns, (3) Execute action decisively in 1 message, (4) Confirm completion and offer further assistance in 1 message." 1 0 0 + customer_service_opening-00002 customer_service_opening Keep initial greeting under 250 characters—acknowledge the request, confirm understanding, and prepare to collect information. 1 0 0 + customer_service_efficiency-00003 customer_service_efficiency Complete customer service interactions within 12-15 total messages through structured, focused communication. 1 0 0 + customer_service_balance-00004 customer_service_balance Maintain conversational balance with agent speaking approximately 1-1.5 times per user message—never dominate with 2+ agent responses per user input. 1 0 0 + customer_service_context-00005 customer_service_context Reference previously provided customer information and proceed with action—do not ask users to repeat details already shared. 1 0 0 + customer_service_closure-00006 customer_service_closure Explicitly confirm task completion with a summary of what was done, then proactively ask if there is anything else needed. 1 0 0 + +**How to use these strategies:** +- Review skills relevant to your current task +- **When applying a strategy, cite its ID in your reasoning** (e.g., "Following [content_extraction-00001], I will extract the title...") + - Citations enable precise tracking of strategy effectiveness + - Makes reasoning transparent and auditable + - Improves learning quality through accurate attribution +- Prioritize strategies with high success rates (helpful > harmful) +- Apply strategies when they match your context +- Adapt general strategies to your specific situation +- Learn from both successful patterns and failure avoidance + +**Important:** These are learned patterns, not rigid rules. Use judgment. diff --git a/examples/agentic-system-prompting/recursive_agentic_system_prompting.py b/examples/agentic-system-prompting/recursive_agentic_system_prompting.py new file mode 100644 index 0000000000000000000000000000000000000000..663024c26666c867c4331b4e8b8be3f321b140cb --- /dev/null +++ b/examples/agentic-system-prompting/recursive_agentic_system_prompting.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +""" +Agentic System Prompting — Offline Trace Analysis + +Feeds pre-recorded agent traces through TraceAnalyser (offline mode) to +extract reusable strategies into a skillbook. + +Each trace file is loaded as a traces-format dict and passed directly +to RRStep via a thin adapter step, so the sandbox +receives the full conversation data. + +TraceAnalyser handles the rest of the learning-tail pipeline: + [RRTraceStep] → UpdateStep (the agentic SkillManager mutates directly) + +Usage: + python recursive_agentic_system_prompting.py /path/to/traces + python recursive_agentic_system_prompting.py /path/to/traces --model gpt-4o + python recursive_agentic_system_prompting.py /path/to/traces --input-skillbook existing.json + python recursive_agentic_system_prompting.py /path/to/traces --epochs 2 + +Options: + traces_dir Path to directory containing .json, .md, or .toon trace files + --model, -m LLM model for analysis (default: bedrock/us.anthropic.claude-sonnet-4-6) + --threshold, -t Deduplication similarity threshold 0.0-1.0 (default: 0.7) + --epochs, -e Number of passes over all traces (default: 1) + --input-skillbook, -i Path to existing skillbook to continue from + --output-dir, -o Output directory for results (default: script directory) +""" + +import argparse +import json +import logging +import os +from datetime import datetime +from itertools import groupby +from pathlib import Path +from typing import Any, Dict, List + +from dotenv import load_dotenv, find_dotenv + +load_dotenv(find_dotenv()) + +# Show RR iteration progress +_handler = logging.StreamHandler() +_handler.setFormatter( + logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S") +) +_logger = logging.getLogger("ace.steps.rr") +_logger.setLevel(logging.DEBUG) +_logger.addHandler(_handler) + +from pipeline import Pipeline + +from ace import TraceAnalyser, SkillManager, Skillbook +from ace.steps.rr_step import RRStep, RRConfig +from ace.core.context import ACEStepContext +from ace.deduplication import DeduplicationManager +from ace.protocols.deduplication import DeduplicationConfig +from ace.implementations.prompts import wrap_skillbook_for_external_agent +from ace.steps import UpdateStep, DeduplicateStep +from ace.implementations.rr.prompts import REFLECTOR_RECURSIVE_PROMPT + + +# --------------------------------------------------------------------------- +# Adapter step: normalises raw traces into the dict format RRStep expects. +# --------------------------------------------------------------------------- +class RRTraceStep: + """Bridge between TraceAnalyser's per-trace context and RRStep. + + TraceAnalyser places the raw trace on ``ctx.trace``. RRStep.__call__ + expects a traces-format dict with a ``steps`` key. This adapter + normalises the trace and delegates to ``RRStep.__call__``. + """ + + requires = frozenset({"trace", "skillbook"}) + provides = frozenset({"reflection"}) + + def __init__(self, rr: RRStep) -> None: + self.rr = rr + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + trace = ctx.trace + # If the trace is already a traces-format dict, pass it through. + # Otherwise wrap it so the sandbox can access it via traces["steps"]. + if isinstance(trace, dict) and "steps" in trace: + traces_dict = trace + else: + traces_dict = { + "question": str(trace.get("id", "")) if isinstance(trace, dict) else "", + "steps": [trace], + } + return self.rr(ctx.replace(trace=traces_dict)) + + +def load_traces(traces_dir: Path) -> Dict[str, Any]: + """Load all trace files into a single batch trace dict. + + All files are combined into one traces-format dict so the REPL agent + receives every conversation at once and can analyze cross-trace patterns. + """ + if not traces_dir.exists(): + print(f"Directory not found: {traces_dir}") + return {} + + steps: List[Dict[str, Any]] = [] + for ext in ("*.json", "*.md", "*.toon"): + for file_path in sorted(traces_dir.glob(ext)): + try: + raw = file_path.read_text(encoding="utf-8") + content = json.loads(raw) if file_path.suffix == ".json" else raw + steps.append( + { + "role": "conversation", + "id": file_path.name, + "content": content, + } + ) + except Exception as e: + print(f"Error reading {file_path.name}: {e}") + + print(f"Loaded {len(steps)} traces") + if not steps: + return {} + + return { + "question": f"Analyze {len(steps)} conversation traces", + "ground_truth": None, + "feedback": None, + "steps": steps, + } + + +def main(): + parser = argparse.ArgumentParser( + description="Offline trace analysis — extract strategies into a skillbook" + ) + parser.add_argument( + "traces_dir", type=Path, help="Directory containing trace files" + ) + parser.add_argument( + "-m", + "--model", + default="bedrock/eu.anthropic.claude-sonnet-4-6", + help="LLM model for analysis", + ) + parser.add_argument( + "-t", + "--threshold", + type=float, + default=0.7, + help="Deduplication similarity threshold (0.0-1.0)", + ) + parser.add_argument( + "-e", "--epochs", type=int, default=1, help="Number of passes over all traces" + ) + parser.add_argument( + "-i", "--input-skillbook", type=Path, default=None, help="Existing skillbook" + ) + parser.add_argument( + "-o", "--output-dir", type=Path, default=None, help="Output directory" + ) + args = parser.parse_args() + + if not os.getenv("OPENAI_API_KEY"): + print("WARNING: OPENAI_API_KEY required for deduplication embeddings!") + return + + # Load all traces into a single batch dict + batch_trace = load_traces(args.traces_dir) + if not batch_trace: + print(f"\nAdd .json, .md, or .toon trace files to {args.traces_dir}/") + return + n_traces = len(batch_trace["steps"]) + + # Skillbook (existing or empty) + skillbook = Skillbook() + if args.input_skillbook and args.input_skillbook.exists(): + skillbook = Skillbook.load_from_file(str(args.input_skillbook)) + print(f"Loaded skillbook: {len(skillbook.skills())} skills") + + # Build PydanticAI-backed roles directly from model strings + rr = RRStep( + args.model, + config=RRConfig( + max_requests=60, + ), + prompt_template=REFLECTOR_RECURSIVE_PROMPT, + ) + skill_manager = SkillManager(args.model) + dedup = DeduplicationManager( + DeduplicationConfig( + enabled=True, + similarity_threshold=args.threshold, + embedding_model="text-embedding-3-small", + ) + ) + + # Build pipeline: RRTraceStep → Update → Dedup + # (SkillManager mutates the skillbook directly via its tools, so no + # separate ApplyStep is needed.) + steps: list[Any] = [RRTraceStep(rr)] + steps.extend( + [ + UpdateStep(skill_manager, skillbook), + DeduplicateStep(dedup, skillbook), + ] + ) + analyser = TraceAnalyser(pipeline=Pipeline(steps), skillbook=skillbook) + + print( + f"\nStarting analysis: {n_traces} traces (single batch), " + f"epochs={args.epochs}, model={args.model}" + ) + start = datetime.now() + + # Run — single batch trace through the pipeline + results = analyser.run([batch_trace], epochs=args.epochs) + + # Surface any pipeline errors (the pipeline catches exceptions silently) + failed = [r for r in results if r.error is not None] + if failed: + print(f"\n{len(failed)}/{len(results)} traces FAILED:") + for r in failed: + print(f" - {r.failed_at}: {r.error}") + + duration = (datetime.now() - start).total_seconds() + + # Save results + output_dir = args.output_dir or Path(__file__).parent + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + output_skillbook = output_dir / f"skillbook_{timestamp}.json" + analyser.save(str(output_skillbook)) + + skills = analyser.skillbook.skills() + print(f"\nCompleted in {duration:.1f}s") + print(f"Analyzed: {n_traces} traces (single batch) × {args.epochs} epoch(s)") + print(f"Generated: {len(skills)} skills") + print(f"Saved to: {output_skillbook}") + + # Markdown export + output_md = output_dir / f"skills_{timestamp}.md" + with open(output_md, "w") as f: + for section, section_skills in groupby( + sorted(skills, key=lambda s: s.section), key=lambda s: s.section + ): + f.write(f"## {section}\n\n") + for skill in section_skills: + f.write(f"- {skill.content}\n") + if skill.justification: + f.write(f" Justification: {skill.justification}\n") + if skill.evidence: + f.write(f" Evidence: {skill.evidence}\n") + f.write("\n") + print(f"Skills: {output_md}") + + if skills: + print("\nTop skills:") + for i, skill in enumerate( + sorted(skills, key=lambda s: s.helpful, reverse=True)[:5], 1 + ): + print(f" {i}. [{skill.section}] {skill.content[:80]}...") + + # External agent injection + injection = wrap_skillbook_for_external_agent(analyser.skillbook) + if injection: + output_injection = output_dir / f"external_agent_injection_{timestamp}.txt" + with open(output_injection, "w") as f: + f.write(injection) + print(f"External agent injection: {output_injection}") + + +if __name__ == "__main__": + main() diff --git a/examples/agentic-system-prompting/skillbook_20260318_144601.json b/examples/agentic-system-prompting/skillbook_20260318_144601.json new file mode 100644 index 0000000000000000000000000000000000000000..8f37d7eb5ed698dcb04be2873fb0924a25eee575 --- /dev/null +++ b/examples/agentic-system-prompting/skillbook_20260318_144601.json @@ -0,0 +1,6 @@ +{ + "skills": {}, + "sections": {}, + "next_id": 0, + "similarity_decisions": {} +} \ No newline at end of file diff --git a/examples/agentic-system-prompting/skillbook_20260318_144659.json b/examples/agentic-system-prompting/skillbook_20260318_144659.json new file mode 100644 index 0000000000000000000000000000000000000000..8f37d7eb5ed698dcb04be2873fb0924a25eee575 --- /dev/null +++ b/examples/agentic-system-prompting/skillbook_20260318_144659.json @@ -0,0 +1,6 @@ +{ + "skills": {}, + "sections": {}, + "next_id": 0, + "similarity_decisions": {} +} \ No newline at end of file diff --git a/examples/agentic-system-prompting/skillbook_20260318_144917.json b/examples/agentic-system-prompting/skillbook_20260318_144917.json new file mode 100644 index 0000000000000000000000000000000000000000..8f37d7eb5ed698dcb04be2873fb0924a25eee575 --- /dev/null +++ b/examples/agentic-system-prompting/skillbook_20260318_144917.json @@ -0,0 +1,6 @@ +{ + "skills": {}, + "sections": {}, + "next_id": 0, + "similarity_decisions": {} +} \ No newline at end of file diff --git a/examples/agentic-system-prompting/skillbook_20260318_145205.json b/examples/agentic-system-prompting/skillbook_20260318_145205.json new file mode 100644 index 0000000000000000000000000000000000000000..8f37d7eb5ed698dcb04be2873fb0924a25eee575 --- /dev/null +++ b/examples/agentic-system-prompting/skillbook_20260318_145205.json @@ -0,0 +1,6 @@ +{ + "skills": {}, + "sections": {}, + "next_id": 0, + "similarity_decisions": {} +} \ No newline at end of file diff --git a/examples/agentic-system-prompting/skillbook_20260318_145249.json b/examples/agentic-system-prompting/skillbook_20260318_145249.json new file mode 100644 index 0000000000000000000000000000000000000000..8f37d7eb5ed698dcb04be2873fb0924a25eee575 --- /dev/null +++ b/examples/agentic-system-prompting/skillbook_20260318_145249.json @@ -0,0 +1,6 @@ +{ + "skills": {}, + "sections": {}, + "next_id": 0, + "similarity_decisions": {} +} \ No newline at end of file diff --git a/examples/agentic-system-prompting/skillbook_20260318_150822.json b/examples/agentic-system-prompting/skillbook_20260318_150822.json new file mode 100644 index 0000000000000000000000000000000000000000..3a81287936d8d7777e5361367664fde2b1fa3110 --- /dev/null +++ b/examples/agentic-system-prompting/skillbook_20260318_150822.json @@ -0,0 +1,116 @@ +{ + "skills": { + "customer_service_structure-00001": { + "id": "customer_service_structure-00001", + "section": "customer_service_structure", + "content": "Structure every customer service interaction into 4 phases: (1) Acknowledge request in 1 message, (2) Collect all required information in 1-2 focused turns, (3) Execute action decisively in 1 message, (4) Confirm completion and offer further assistance in 1 message.", + "justification": "Atomic, evidence-based structure directly enables 12-15 message resolution targets. Provides actionable framework for all interactions.", + "evidence": "Most efficient conversation (4 msgs) followed this pattern. Cancellation avg 11.8 msgs, booking 19.4 msgs.", + "helpful": 1, + "harmful": 0, + "neutral": 0, + "created_at": "2026-03-18T14:08:22.435257+00:00", + "updated_at": "2026-03-18T14:08:22.435269+00:00", + "embedding": null, + "status": "active", + "sources": [] + }, + "customer_service_opening-00002": { + "id": "customer_service_opening-00002", + "section": "customer_service_opening", + "content": "Keep initial greeting under 250 characters—acknowledge the request, confirm understanding, and prepare to collect information.", + "justification": "Measurable threshold with direct efficiency correlation. Prevents verbose opening messages that trigger longer conversations.", + "evidence": "Efficient conversations averaged 70 char opening messages vs inefficient at 99 chars", + "helpful": 1, + "harmful": 0, + "neutral": 0, + "created_at": "2026-03-18T14:08:22.435293+00:00", + "updated_at": "2026-03-18T14:08:22.435295+00:00", + "embedding": null, + "status": "active", + "sources": [] + }, + "customer_service_efficiency-00003": { + "id": "customer_service_efficiency-00003", + "section": "customer_service_efficiency", + "content": "Complete customer service interactions within 12-15 total messages through structured, focused communication.", + "justification": "Clear efficiency target derived from measured performance. 16 of 30 conversations achieved this threshold.", + "evidence": "16 conversations resolved in ≤14 messages. Range: 4-34 messages. Efficient conversations averaged 11.8-19.4 msgs by task type.", + "helpful": 1, + "harmful": 0, + "neutral": 0, + "created_at": "2026-03-18T14:08:22.435306+00:00", + "updated_at": "2026-03-18T14:08:22.435308+00:00", + "embedding": null, + "status": "active", + "sources": [] + }, + "customer_service_balance-00004": { + "id": "customer_service_balance-00004", + "section": "customer_service_balance", + "content": "Maintain conversational balance with agent speaking approximately 1-1.5 times per user message—never dominate with 2+ agent responses per user input.", + "justification": "Specific turn ratio metric with measured range. Prevents agent-dominated conversations that increase message count.", + "evidence": "Agent:User turn ratio averaged 1.50x across all 30 conversations (range: 0.67x to 2.40x)", + "helpful": 1, + "harmful": 0, + "neutral": 0, + "created_at": "2026-03-18T14:08:22.435314+00:00", + "updated_at": "2026-03-18T14:08:22.435315+00:00", + "embedding": null, + "status": "active", + "sources": [] + }, + "customer_service_context-00005": { + "id": "customer_service_context-00005", + "section": "customer_service_context", + "content": "Reference previously provided customer information and proceed with action—do not ask users to repeat details already shared.", + "justification": "Eliminates redundant message rounds. Directly impacts efficiency by reducing total conversation length.", + "evidence": "Inefficient conversations (n=14, avg 22.1 msgs) likely contained repeated information requests", + "helpful": 1, + "harmful": 0, + "neutral": 0, + "created_at": "2026-03-18T14:08:22.435321+00:00", + "updated_at": "2026-03-18T14:08:22.435323+00:00", + "embedding": null, + "status": "active", + "sources": [] + }, + "customer_service_closure-00006": { + "id": "customer_service_closure-00006", + "section": "customer_service_closure", + "content": "Explicitly confirm task completion with a summary of what was done, then proactively ask if there is anything else needed.", + "justification": "Provides clear closure framework and opens door for additional requests within same conversation thread.", + "evidence": "All 30 conversations terminated with user_stop. Clear task completion statements support higher satisfaction.", + "helpful": 1, + "harmful": 0, + "neutral": 0, + "created_at": "2026-03-18T14:08:22.435327+00:00", + "updated_at": "2026-03-18T14:08:22.435329+00:00", + "embedding": null, + "status": "active", + "sources": [] + } + }, + "sections": { + "customer_service_structure": [ + "customer_service_structure-00001" + ], + "customer_service_opening": [ + "customer_service_opening-00002" + ], + "customer_service_efficiency": [ + "customer_service_efficiency-00003" + ], + "customer_service_balance": [ + "customer_service_balance-00004" + ], + "customer_service_context": [ + "customer_service_context-00005" + ], + "customer_service_closure": [ + "customer_service_closure-00006" + ] + }, + "next_id": 6, + "similarity_decisions": {} +} \ No newline at end of file diff --git a/examples/agentic-system-prompting/skills_20260318_144601.md b/examples/agentic-system-prompting/skills_20260318_144601.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/examples/agentic-system-prompting/skills_20260318_144659.md b/examples/agentic-system-prompting/skills_20260318_144659.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/examples/agentic-system-prompting/skills_20260318_144917.md b/examples/agentic-system-prompting/skills_20260318_144917.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/examples/agentic-system-prompting/skills_20260318_145205.md b/examples/agentic-system-prompting/skills_20260318_145205.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/examples/agentic-system-prompting/skills_20260318_145249.md b/examples/agentic-system-prompting/skills_20260318_145249.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/examples/agentic-system-prompting/skills_20260318_150822.md b/examples/agentic-system-prompting/skills_20260318_150822.md new file mode 100644 index 0000000000000000000000000000000000000000..0a65b27a8134c1ac989cf8f67cf6c1b0816497fc --- /dev/null +++ b/examples/agentic-system-prompting/skills_20260318_150822.md @@ -0,0 +1,36 @@ +## customer_service_balance + +- Maintain conversational balance with agent speaking approximately 1-1.5 times per user message—never dominate with 2+ agent responses per user input. + Justification: Specific turn ratio metric with measured range. Prevents agent-dominated conversations that increase message count. + Evidence: Agent:User turn ratio averaged 1.50x across all 30 conversations (range: 0.67x to 2.40x) + +## customer_service_closure + +- Explicitly confirm task completion with a summary of what was done, then proactively ask if there is anything else needed. + Justification: Provides clear closure framework and opens door for additional requests within same conversation thread. + Evidence: All 30 conversations terminated with user_stop. Clear task completion statements support higher satisfaction. + +## customer_service_context + +- Reference previously provided customer information and proceed with action—do not ask users to repeat details already shared. + Justification: Eliminates redundant message rounds. Directly impacts efficiency by reducing total conversation length. + Evidence: Inefficient conversations (n=14, avg 22.1 msgs) likely contained repeated information requests + +## customer_service_efficiency + +- Complete customer service interactions within 12-15 total messages through structured, focused communication. + Justification: Clear efficiency target derived from measured performance. 16 of 30 conversations achieved this threshold. + Evidence: 16 conversations resolved in ≤14 messages. Range: 4-34 messages. Efficient conversations averaged 11.8-19.4 msgs by task type. + +## customer_service_opening + +- Keep initial greeting under 250 characters—acknowledge the request, confirm understanding, and prepare to collect information. + Justification: Measurable threshold with direct efficiency correlation. Prevents verbose opening messages that trigger longer conversations. + Evidence: Efficient conversations averaged 70 char opening messages vs inefficient at 99 chars + +## customer_service_structure + +- Structure every customer service interaction into 4 phases: (1) Acknowledge request in 1 message, (2) Collect all required information in 1-2 focused turns, (3) Execute action decisively in 1 message, (4) Confirm completion and offer further assistance in 1 message. + Justification: Atomic, evidence-based structure directly enables 12-15 message resolution targets. Provides actionable framework for all interactions. + Evidence: Most efficient conversation (4 msgs) followed this pattern. Cancellation avg 11.8 msgs, booking 19.4 msgs. + diff --git a/examples/openclaw/AGENTS.md.snippet b/examples/openclaw/AGENTS.md.snippet new file mode 100644 index 0000000000000000000000000000000000000000..8dd408ab1e237b8055c44efb16bc19073df91663 --- /dev/null +++ b/examples/openclaw/AGENTS.md.snippet @@ -0,0 +1,71 @@ +# ─── Add these sections to your OpenClaw AGENTS.md ─── +# +# Paste them at the end of ~/.openclaw/workspace/AGENTS.md (or wherever +# your AGENTS.md lives). The agent reads AGENTS.md at session start and +# will follow these instructions automatically. +# +# REQUIREMENTS: +# - Build the ACE-extended Docker image (see Dockerfile.ace) +# - Pass your LLM API key through docker-compose environment: +# +# environment: +# ACE_MODEL: "bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0" +# # Pick ONE of these depending on your provider: +# AWS_BEARER_TOKEN_BEDROCK: ${AWS_BEARER_TOKEN_BEDROCK} +# ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} +# OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} +# LITELLM_API_KEY: ${LITELLM_API_KEY} + +## Auto-Learning + +At the start of each session, before responding to the user's first message, +run: + + ace-learn + +After it completes, report the results to the user. Include: +- How many new sessions were processed +- How many new strategies were added (and their IDs) +- Total strategy count in the skillbook + +Example: + +> **ACE Learning Report** +> Processed 2 new sessions (6 total, 4 previously processed). +> Added 3 new strategies: `error_handling-00005`, `code_style-00008`, +> `debugging-00003`. Skillbook now contains 14 strategies. + +If there were no new sessions, report that briefly: + +> **ACE:** No new sessions to learn from. Skillbook has 14 strategies loaded. + +If the command fails, tell the user that auto-learning failed and suggest they +check their API key configuration, then continue normally. +If `ace-learn` is not found, tell the user ACE is not installed and continue. + +### On-demand learning + +If the user asks to "learn from this session" (or similar), process only the +current session's transcript: + + ace-learn <path-to-current-session.jsonl> + +Find the current session file in `~/.openclaw/agents/main/sessions/` (it will +be the most recently modified `.jsonl` file). Report what was learned using the +same format as above. + +Do **not** reprocess all sessions — only the current one. + +## Learned Strategies + +After running `ace-learn`, read the file `skills/kayba-ace/ace_skillbook.md` +into your context. This file contains strategies learned from past sessions. + +**Important:** You must explicitly read the file — markdown links are not +auto-inlined. Use your file-reading tools to load the full content. + +When a strategy is relevant to the current task: + +1. Apply it. +2. Cite the strategy ID (e.g. `conversation_style-00003`) so the user can + trace which learned behaviour influenced the response. diff --git a/examples/openclaw/Dockerfile.ace b/examples/openclaw/Dockerfile.ace new file mode 100644 index 0000000000000000000000000000000000000000..80153c94c3245e8edba4906655b9e4636ecf7435 --- /dev/null +++ b/examples/openclaw/Dockerfile.ace @@ -0,0 +1,73 @@ +# Dockerfile.ace — OpenClaw + ACE (self-improving agent) +# +# Extends the OpenClaw image with Python 3.12 and the ACE framework +# pre-installed, so the agent can learn from past sessions automatically. +# +# Build (two-step, from the openclaw repo root): +# +# # 1. Build the base OpenClaw image +# docker build -t openclaw:base . +# +# # 2. Extend with ACE +# docker build -t openclaw:local -f Dockerfile.ace . +# +# Then set OPENCLAW_IMAGE=openclaw:local in your .env file. +# +# IMPORTANT: You must pass your LLM API key through docker-compose so +# ace-learn can call the reflection model. Add to docker-compose.yml +# under the gateway service's environment section: +# +# ACE_MODEL: "bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0" +# # Plus one of: AWS_BEARER_TOKEN_BEDROCK, ANTHROPIC_API_KEY, +# # OPENROUTER_API_KEY, or LITELLM_API_KEY + +ARG OPENCLAW_IMAGE=openclaw:base +FROM ${OPENCLAW_IMAGE} + +USER root + +# --------------------------------------------------------------------------- +# Python 3.12 + uv +# --------------------------------------------------------------------------- + +# uv — fast Python package & version manager +RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh + +# Install Python 3.12 to a shared location +# (bookworm ships 3.11; uv downloads an official standalone build) +ENV UV_PYTHON_INSTALL_DIR=/opt/python +RUN uv python install 3.12 + +# --------------------------------------------------------------------------- +# ACE framework +# --------------------------------------------------------------------------- + +RUN rm -rf /opt/ace \ + && git clone --depth 1 https://github.com/Kayba-ai/agentic-context-engine.git /opt/ace \ + && cd /opt/ace \ + && uv sync --no-dev --extra claude-code --python 3.12 \ + && uv pip install boto3 --python .venv/bin/python \ + && chown -R node:node /opt/ace + +# Wrapper script: writes output to the persistent workspace volume +# so the skillbook survives container restarts. +RUN printf '#!/bin/bash\n\ +set -euo pipefail\n\ +export OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}"\n\ +OUTPUT_DIR="$OPENCLAW_HOME/workspace/skills/kayba-ace"\n\ +mkdir -p "$OUTPUT_DIR"\n\ +cd /opt/ace\n\ +exec .venv/bin/python examples/openclaw/kayba-ace/learn_from_traces.py \\\n\ + --output "$OUTPUT_DIR" \\\n\ + "$@"\n' > /usr/local/bin/ace-learn \ + && chmod +x /usr/local/bin/ace-learn + +# Note: Do NOT set OPENCLAW_HOME here — the base image derives it from +# $HOME (~/.openclaw). Setting it explicitly causes double-nesting. + +# --------------------------------------------------------------------------- +# Restore original user and working directory +# --------------------------------------------------------------------------- + +USER node +WORKDIR /app diff --git a/examples/openclaw/README.md b/examples/openclaw/README.md new file mode 100644 index 0000000000000000000000000000000000000000..21d68e1a6aaeeb75a5f2dfd41854a30c6eb2fbd7 --- /dev/null +++ b/examples/openclaw/README.md @@ -0,0 +1,151 @@ +# OpenClaw + ACE Integration + +Learn from [OpenClaw](https://docs.openclaw.ai) session transcripts and build a +self-improving skillbook of reusable strategies. + +For the full setup guide, see [docs/integrations/openclaw.md](../../docs/integrations/openclaw.md). + +## Quick Start (Docker — Recommended) + +Extend your OpenClaw Docker image with ACE pre-installed. The agent runs +`ace-learn` at session start automatically. + +```bash +# 1. Copy Dockerfile.ace into your OpenClaw directory +cp examples/openclaw/Dockerfile.ace /path/to/your/openclaw/ + +# 2. Build (from the OpenClaw directory) +docker build -t openclaw:base . +docker build -t openclaw:local --build-arg OPENCLAW_IMAGE=openclaw:base -f Dockerfile.ace . + +# 3. Point OpenClaw at the new image (in your .env file) +# OPENCLAW_IMAGE=openclaw:local + +# 4. Pass your LLM API key in docker-compose.yml (see docs for all providers) +# environment: +# AWS_BEARER_TOKEN_BEDROCK: ${AWS_BEARER_TOKEN_BEDROCK} + +# 5. Add auto-learning to AGENTS.md (see AGENTS.md.snippet) + +# 6. Restart the gateway +docker compose down && docker compose up -d openclaw-gateway +``` + +### Verify + +```bash +# Dry run — parses sessions without making LLM calls +docker run --rm -v ~/.openclaw:/home/node/.openclaw openclaw:local ace-learn --dry-run + +# Full run +docker run --rm \ + -v ~/.openclaw:/home/node/.openclaw \ + -e AWS_BEARER_TOKEN_BEDROCK="$AWS_BEARER_TOKEN_BEDROCK" \ + openclaw:local ace-learn +``` + +## Quick Start (Host) + +Run ACE on the host machine. Useful if you don't want to customize Docker. + +```bash +# 1. Install +git clone https://github.com/Kayba-ai/agentic-context-engine.git +cd agentic-context-engine +uv sync + +# 2. Set your LLM API key +export ANTHROPIC_API_KEY="your-key" + +# 3. Dry run (no LLM calls, just parse sessions) +uv run python examples/openclaw/kayba-ace/learn_from_traces.py --dry-run + +# 4. Learn from all new sessions +uv run python examples/openclaw/kayba-ace/learn_from_traces.py +``` + +## How It Works + +``` +OpenClaw sessions --> JSONL transcripts on disk + | + ace-learn / learn_from_traces.py + | + LoadTracesStep --> OpenClawToTraceStep + | + TraceAnalyser (Reflect -> Tag -> Update -> Apply) + | + +---------------+----------------+ + | | + ace_skillbook.json ace_skillbook.md + | + AGENTS.md tells agent to read skillbook + | + Agent loads strategies into context +``` + +1. OpenClaw writes session transcripts to `~/.openclaw/agents/<id>/sessions/*.jsonl` +2. `LoadTracesStep` reads JSONL files into raw event lists +3. `OpenClawToTraceStep` converts events to structured traces +4. `TraceAnalyser` runs the ACE learning pipeline (Reflect -> Tag -> Update -> Apply) +5. Updated skillbook is saved; the agent reads `ace_skillbook.md` into its context + +## CLI Usage + +```bash +# Learn from all new sessions (default agent: main) +ace-learn # Docker +uv run python examples/openclaw/kayba-ace/learn_from_traces.py # Host + +# Process specific trace files +ace-learn <trace.jsonl> [<trace2.jsonl> ...] + +# Reprocess all sessions (ignore already-processed log) +ace-learn --reprocess + +# Custom output directory +ace-learn --output ./out + +# Enable Opik observability logging +ace-learn --opik + +# Use a different agent ID +ace-learn --agent other-agent +``` + +## Files + +| File | Description | +|---|---| +| `kayba-ace/` | Skill folder: `learn_from_traces.py`, `SKILL.md` (copied to OpenClaw workspace by `setup.py`) | +| `Dockerfile.ace` | Extends OpenClaw image with Python 3.12 + ACE | +| `ace-learn.sh` | Wrapper script (reference copy; Dockerfile inlines it) | +| `AGENTS.md.snippet` | Paste into your AGENTS.md for auto-learning | +| `setup.py` | Automated setup: copies skill folder, patches AGENTS.md | + +## Configuration + +| Variable | Default | Description | +|---|---|---| +| `ACE_MODEL` | `bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0` | LLM for reflection and skill extraction | +| `OPENCLAW_AGENT_ID` | `main` | Agent ID for session discovery | +| `OPENCLAW_HOME` | `$HOME/.openclaw` | Used by `ace-learn` only; do not set as a gateway env var | +| `LITELLM_API_KEY` | - | API key (for non-Bedrock providers) | +| `SPH_LITELLM_KEY` | - | Alternative API key variable | +| `AWS_BEARER_TOKEN_BEDROCK` | - | AWS Bedrock bearer token | +| `ANTHROPIC_API_KEY` | - | Anthropic API key | +| `OPENROUTER_API_KEY` | - | OpenRouter API key | + +## Outputs + +| File | Format | Description | +|---|---|---| +| `ace_skillbook.json` | JSON | Full skillbook (machine-readable, persists across runs) | +| `ace_skillbook.md` | Markdown | Human-readable skillbook grouped by section | +| `ace_processed.txt` | Text | Tracks which sessions have already been processed | + +## Automate with Cron (Host Only) + +```bash +*/30 * * * * cd /path/to/agentic-context-engine && uv run python examples/openclaw/kayba-ace/learn_from_traces.py >> /tmp/ace-openclaw.log 2>&1 +``` diff --git a/examples/openclaw/ace-learn.sh b/examples/openclaw/ace-learn.sh new file mode 100644 index 0000000000000000000000000000000000000000..2050e7c342116c0831fa00a18a8537431a9e1bb3 --- /dev/null +++ b/examples/openclaw/ace-learn.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# ace-learn — wrapper for learn_from_traces.py +# +# Writes skillbook output to the persistent workspace volume so it +# survives container restarts. The agent calls this as: ace-learn [args] + +set -euo pipefail + +# Resolve OPENCLAW_HOME once — the Python script also reads this env var +# to find the sessions directory. Without it, the script falls back to a +# path relative to its own location which is wrong inside Docker. +export OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}" + +OUTPUT_DIR="$OPENCLAW_HOME/workspace/skills/kayba-ace" +mkdir -p "$OUTPUT_DIR" + +cd /opt/ace +exec .venv/bin/python examples/openclaw/kayba-ace/learn_from_traces.py \ + --output "$OUTPUT_DIR" \ + "$@" diff --git a/examples/openclaw/kayba-ace/SKILL.md b/examples/openclaw/kayba-ace/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..f8a94babd6af62fd9c3eb3c4e2275bd9fcca3fc3 --- /dev/null +++ b/examples/openclaw/kayba-ace/SKILL.md @@ -0,0 +1,159 @@ +# ACE — Learn from Traces + +This skill ships `learn_from_traces.py`, a script that reads OpenClaw session +transcripts, feeds them through the ACE learning pipeline, and writes an +updated skillbook to disk. + +## Usage + +```bash +python learn_from_traces.py [OPTIONS] [FILES...] +``` + +The script auto-discovers new sessions from `~/.openclaw/agents/<agent>/sessions/` +and only processes files that haven't been processed before. Processed filenames +are tracked in `ace_processed.txt`. + +## Options + +| Flag | Description | +|---|---| +| `--dry-run` | Parse sessions but skip the learning step (no LLM calls) | +| `--reprocess` | Ignore the processed log and reprocess all sessions | +| `--agent ID` | OpenClaw agent ID (default: `$OPENCLAW_AGENT_ID` or `main`) | +| `--output DIR` | Output directory for skillbook files (default: script directory) | +| `--opik` | Enable Opik observability logging | + +Pass one or more JSONL file paths as positional arguments to process specific +files instead of auto-discovering sessions. + +## Examples + +### Learn from all new sessions + +```bash +python learn_from_traces.py +``` + +Discovers unprocessed sessions under `~/.openclaw/agents/main/sessions/`, +runs the learning pipeline, and writes the updated skillbook. + +### Dry run (no LLM calls) + +```bash +python learn_from_traces.py --dry-run +``` + +Parses and validates sessions without calling the LLM. Useful for checking +that session files are readable before committing to a full run. + +### Process a specific trace file + +```bash +python learn_from_traces.py ~/.openclaw/agents/main/sessions/f967d602.jsonl +``` + +Skips auto-discovery and processes only the given file. The processed log +is not updated when files are passed directly. + +### Process multiple files + +```bash +python learn_from_traces.py session1.jsonl session2.jsonl session3.jsonl +``` + +### Reprocess all sessions + +```bash +python learn_from_traces.py --reprocess +``` + +Ignores `ace_processed.txt` and reprocesses every session file. Useful after +upgrading ACE or when you want to rebuild the skillbook from scratch. + +### Use a different agent + +```bash +python learn_from_traces.py --agent my-agent +``` + +Looks for sessions in `~/.openclaw/agents/my-agent/sessions/` instead of +the default `main`. + +### Write output to a custom directory + +```bash +python learn_from_traces.py --output /tmp/ace-out +``` + +Writes `ace_skillbook.json`, `ace_skillbook.md`, and `ace_processed.txt` +to `/tmp/ace-out/` instead of the script's directory. + +### Enable Opik observability + +```bash +python learn_from_traces.py --opik +``` + +Logs LLM calls and pipeline steps to [Opik](https://www.comet.com/opik) +for debugging and monitoring. + +### Run from the ACE repo (host setup) + +```bash +cd /path/to/agentic-context-engine +uv run python examples/openclaw/kayba-ace/learn_from_traces.py --dry-run +``` + +When running from a repo checkout, `uv run` ensures `ace` is importable. + +### Run inside Docker + +```bash +# Dry run +docker run --rm -v ~/.openclaw:/home/node/.openclaw openclaw:local ace-learn --dry-run + +# Full run with API key +docker run --rm \ + -v ~/.openclaw:/home/node/.openclaw \ + -e AWS_BEARER_TOKEN_BEDROCK="$AWS_BEARER_TOKEN_BEDROCK" \ + openclaw:local ace-learn +``` + +Inside the extended Docker image, `ace-learn` is a wrapper that calls this +script with the correct paths. + +## Environment Variables + +| Variable | Default | Description | +|---|---|---| +| `ACE_MODEL` | `bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0` | LLM model for reflection and skill extraction | +| `OPENCLAW_AGENT_ID` | `main` | Default agent ID (overridden by `--agent`) | +| `OPENCLAW_HOME` | `~/.openclaw` | OpenClaw home directory | + +The script also loads `.env` files from `$OPENCLAW_HOME/.env` and `~/.env`. +Set your API key in one of these variables: + +- `AWS_BEARER_TOKEN_BEDROCK` +- `ANTHROPIC_API_KEY` +- `OPENROUTER_API_KEY` +- `LITELLM_API_KEY` +- `SPH_LITELLM_KEY` + +## Output Files + +| File | Description | +|---|---| +| `ace_skillbook.json` | Machine-readable skillbook (persists across runs) | +| `ace_skillbook.md` | Human-readable skillbook loaded by the agent | +| `ace_processed.txt` | Tracks which sessions have been processed | + +## Setup + +Run the setup script from the ACE repo to copy this skill into your OpenClaw workspace: + +```bash +python examples/openclaw/setup.py +``` + +Full guide: https://kayba-ai.github.io/agentic-context-engine/integrations/openclaw/ diff --git a/examples/openclaw/kayba-ace/learn_from_traces.py b/examples/openclaw/kayba-ace/learn_from_traces.py new file mode 100644 index 0000000000000000000000000000000000000000..490e32d6df71905ef55346a64cefb2d5f10ffd77 --- /dev/null +++ b/examples/openclaw/kayba-ace/learn_from_traces.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +"""Learn from OpenClaw session transcripts. + +Reads OpenClaw session JSONL files, feeds them through the ACE learning +pipeline (TraceAnalyser), and writes the updated skillbook as JSON and +markdown to the output directory. + +Designed to live inside an OpenClaw skills directory:: + + ~/.openclaw/workspace/skills/kayba-ace/ + learn_from_traces.py ← this script + SKILL.md + ace_skillbook.json ← generated + ace_skillbook.md ← generated + +All paths are derived from the script's location. OPENCLAW_HOME is +inferred as three directories up (workspace/skills/<name>/ → .openclaw/). +""" + +import argparse +import os +import sys +from pathlib import Path + +# --------------------------------------------------------------------------- +# Path setup — derive everything from the script's location +# --------------------------------------------------------------------------- + +SCRIPT_DIR = Path(__file__).resolve().parent + +# Expected layout: OPENCLAW_HOME/workspace/skills/<name>/script.py +# Override with OPENCLAW_HOME env var if the script lives elsewhere. +OPENCLAW_HOME = Path(os.getenv("OPENCLAW_HOME") or SCRIPT_DIR.parents[2]).expanduser() + +# Ensure ace is importable (dev: repo root; prod: pip install) +try: + import ace # noqa: F401 +except ImportError: + # Fallback for running from the repo checkout + _repo_root = Path(__file__).resolve().parents[2] + if (_repo_root / "ace").is_dir(): + sys.path.insert(0, str(_repo_root)) + +from dotenv import load_dotenv + +load_dotenv(OPENCLAW_HOME / ".env") +load_dotenv(Path.home() / ".env") + +from ace import ( + LiteLLMClient, + OpikStep, + Reflector, + Skillbook, + SkillManager, + TraceAnalyser, + register_opik_litellm_callback, +) +from ace.core.context import ACEStepContext +from ace.steps.load_traces import LoadTracesStep +from ace.integrations.openclaw import OpenClawToTraceStep +from ace.steps.export_markdown import ExportSkillbookMarkdownStep + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +MODEL = os.getenv("ACE_MODEL", "bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0") + + +# --------------------------------------------------------------------------- +# Processed-session tracking (US3: FR-007, FR-008) +# --------------------------------------------------------------------------- + + +def load_processed(path: Path) -> set[str]: + """Load the set of already-processed session filenames.""" + if path.exists(): + return set(path.read_text().splitlines()) + return set() + + +def save_processed(path: Path, processed: set[str]) -> None: + """Persist the set of processed session filenames.""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(sorted(processed)) + "\n") + + +# --------------------------------------------------------------------------- +# Session parsing via pipeline steps +# --------------------------------------------------------------------------- + +_load_step = LoadTracesStep() +_convert_step = OpenClawToTraceStep() + + +def parse_session(path: Path) -> object | None: + """Parse a single session JSONL file using pipeline steps. + + Returns the trace object (raw events for now, structured dict later) + or None if the file is empty/unparseable. + """ + ctx = ACEStepContext(sample=str(path)) + ctx = _load_step(ctx) + + # Skip empty sessions + if not ctx.trace: + return None + + ctx = _convert_step(ctx) + return ctx.trace + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def section(name: str) -> None: + print(f"\n{'=' * 60}\n {name}\n{'=' * 60}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Learn from OpenClaw session transcripts." + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Parse sessions but skip learning and sync.", + ) + parser.add_argument( + "--reprocess", + action="store_true", + help="Ignore the processed log and reprocess all sessions.", + ) + parser.add_argument( + "--agent", + default=os.getenv("OPENCLAW_AGENT_ID", "main"), + help="OpenClaw agent ID for session discovery (default: $OPENCLAW_AGENT_ID or 'main').", + ) + parser.add_argument( + "--output", + type=Path, + default=SCRIPT_DIR, + help="Output directory for skillbook files (default: script directory).", + ) + parser.add_argument( + "--opik", + action="store_true", + help="Enable Opik observability logging.", + ) + parser.add_argument( + "files", + nargs="*", + type=Path, + help="JSONL trace files to process directly (skips session discovery).", + ) + args = parser.parse_args() + + # -- Resolve paths -- + output_dir = args.output.expanduser().resolve() + processed_log = output_dir / "ace_processed.txt" + sessions_dir = OPENCLAW_HOME / "agents" / args.agent / "sessions" + + # -- Discover new sessions (FR-001, FR-012) -- + section("Discovering sessions") + processed: set[str] = set() + if args.files: + new_sessions = [f.resolve() for f in args.files if f.exists()] + missing = [f for f in args.files if not f.exists()] + for m in missing: + print(f" WARNING: file not found: {m}") + print(f" Direct files: {len(new_sessions)}") + else: + if not sessions_dir.exists(): + print(f" Sessions directory not found: {sessions_dir}") + print(" Is OpenClaw installed and has the agent run at least once?") + sys.exit(1) + + processed = set() if args.reprocess else load_processed(processed_log) + session_files = sorted(sessions_dir.glob("*.jsonl")) + new_sessions = [f for f in session_files if f.name not in processed] + + print(f" Agent: {args.agent}") + print(f" Sessions dir: {sessions_dir}") + print(f" Total sessions: {len(session_files)}") + print(f" Already processed: {len(processed)}") + print(f" New to process: {len(new_sessions)}") + + if not new_sessions: + print(" Nothing new to learn from.") + return + + # -- Parse into traces (FR-002, FR-010) -- + section("Parsing sessions") + traces: list[object] = [] + skipped = 0 + for session_file in new_sessions: + trace = parse_session(session_file) + if trace: + traces.append(trace) + print(f" + {session_file.name}") + else: + skipped += 1 + + print(f" Parsed: {len(traces)}, Skipped (empty): {skipped}") + + if not traces: + print(" No usable traces found.") + return + + # -- Dry run: stop before learning (FR-009) -- + if args.dry_run: + print("\n --dry-run: stopping before learning.") + return + + # -- Load or create skillbook (FR-004) -- + section("Loading skillbook") + skillbook_path = output_dir / "ace_skillbook.json" + if skillbook_path.exists(): + try: + skillbook = Skillbook.load_from_file(str(skillbook_path)) + print( + f" Loaded {len(skillbook.skills())} existing strategies" + f" from {skillbook_path}" + ) + except Exception as exc: + print(f" ERROR: Failed to load skillbook: {exc}") + print(" Starting with empty skillbook instead.") + skillbook = Skillbook() + else: + skillbook = Skillbook() + print(" Starting with empty skillbook") + + skills_before = len(skillbook.skills()) + + # -- Run learning (FR-003) -- + section(f"Learning from {len(traces)} traces") + # Pick the first available API key based on the configured provider. + api_key = ( + os.getenv("AWS_BEARER_TOKEN_BEDROCK") + or os.getenv("ANTHROPIC_API_KEY") + or os.getenv("OPENROUTER_API_KEY") + or os.getenv("LITELLM_API_KEY") + or os.getenv("SPH_LITELLM_KEY") + ) + client = LiteLLMClient( + model=MODEL, + api_key=api_key, + ) + + markdown_path = output_dir / "ace_skillbook.md" + export_md_step = ExportSkillbookMarkdownStep(markdown_path, skillbook) + + extra_steps: list = [export_md_step] + if args.opik: + opik_step = OpikStep( + project_name="openclaw-trace-learning", + tags=["openclaw", "trace-analyser"], + ) + register_opik_litellm_callback(project_name="openclaw-trace-learning") + extra_steps.append(opik_step) + + analyser = TraceAnalyser.from_roles( + reflector=Reflector(client), + skill_manager=SkillManager(client), + skillbook=skillbook, + extra_steps=extra_steps, # type: ignore[arg-type] + ) + + results = analyser.run(traces, epochs=1, wait=True) + + errors = [r for r in results if r.error] + if errors: + for e in errors: + print(f" ERROR: {e.failed_at}: {e.error}") + print(f" Processed: {len(results) - len(errors)}/{len(results)}") + + skills_after = len(skillbook.skills()) + new_skills = skills_after - skills_before + print(f" New strategies: {new_skills} (total: {skills_after})") + + if skills_after > 0: + print("\n Latest strategies:") + for skill in skillbook.skills()[-3:]: + print(f" [{skill.id}] {skill.content[:70]}") + + # -- Save skillbook (FR-004) -- + section("Saving") + json_path = output_dir / "ace_skillbook.json" + json_path.parent.mkdir(parents=True, exist_ok=True) + skillbook.save_to_file(str(json_path)) + print(f" Skillbook JSON: {json_path}") + print(f" Skillbook MD: {markdown_path}") + + # -- Mark sessions processed (FR-007) -- + if not args.files: + processed.update(f.name for f in new_sessions) + save_processed(processed_log, processed) + print(f" Processed log: {processed_log}") + + section("Done") + + +if __name__ == "__main__": + main() diff --git a/examples/openclaw/setup.py b/examples/openclaw/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..89df4f5be2b6c3f054abe88138272c7448d52b6a --- /dev/null +++ b/examples/openclaw/setup.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Set up the ACE skill for an OpenClaw agent. + +Copies the skill folder into the OpenClaw workspace and optionally +appends auto-learning instructions to AGENTS.md. + +Usage: + python examples/openclaw/setup.py # interactive + python examples/openclaw/setup.py --no-agents # skip AGENTS.md + python examples/openclaw/setup.py --openclaw-home /path/to/.openclaw +""" + +import argparse +import shutil +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +SKILL_SRC = SCRIPT_DIR / "kayba-ace" +AGENTS_SNIPPET = SCRIPT_DIR / "AGENTS.md.snippet" + +SKILL_NAME = "kayba-ace" + +# Marker to detect if the snippet was already appended +AGENTS_MARKER = "## Auto-Learning" + + +def find_openclaw_home(override: str | None) -> Path: + """Resolve OPENCLAW_HOME, checking common locations.""" + if override: + p = Path(override).expanduser().resolve() + if p.exists(): + return p + print(f" ERROR: --openclaw-home path does not exist: {p}") + sys.exit(1) + + candidates = [ + Path.home() / ".openclaw", + ] + for c in candidates: + if c.exists(): + return c + + print(" ERROR: Could not find OpenClaw installation.") + print(" Checked: " + ", ".join(str(c) for c in candidates)) + print(" Use --openclaw-home to specify the path manually.") + sys.exit(1) + + +def copy_skill(openclaw_home: Path) -> Path: + """Copy the skill folder into the OpenClaw workspace.""" + dest = openclaw_home / "workspace" / "skills" / SKILL_NAME + dest.mkdir(parents=True, exist_ok=True) + + # Copy all files (don't overwrite generated skillbook/processed files) + generated = {"ace_skillbook.json", "ace_skillbook.md", "ace_processed.txt"} + for src_file in SKILL_SRC.iterdir(): + if src_file.is_file(): + target = dest / src_file.name + if target.exists() and src_file.name in generated: + print(f" SKIP (generated): {target}") + elif target.exists(): + shutil.copy2(src_file, target) + print(f" UPDATED: {src_file.name} -> {target}") + else: + shutil.copy2(src_file, target) + print(f" COPIED: {src_file.name} -> {target}") + + return dest + + +def patch_agents_md(openclaw_home: Path) -> bool: + """Append auto-learning instructions to AGENTS.md if not already present.""" + agents_md = openclaw_home / "workspace" / "AGENTS.md" + + if not AGENTS_SNIPPET.exists(): + print(f" WARNING: snippet not found at {AGENTS_SNIPPET}") + return False + + snippet_text = AGENTS_SNIPPET.read_text() + + # Strip the comment header from the snippet (lines starting with #) + lines = snippet_text.splitlines() + content_lines = [] + in_header = True + for line in lines: + if in_header and line.startswith("#") and not line.startswith("##"): + continue + in_header = False + content_lines.append(line) + snippet_body = "\n".join(content_lines).strip() + + if agents_md.exists(): + existing = agents_md.read_text() + if AGENTS_MARKER in existing: + print(f" SKIP: AGENTS.md already contains '{AGENTS_MARKER}'") + return False + # Append + with open(agents_md, "a") as f: + f.write("\n\n" + snippet_body + "\n") + print(f" UPDATED: {agents_md}") + else: + agents_md.parent.mkdir(parents=True, exist_ok=True) + agents_md.write_text(snippet_body + "\n") + print(f" CREATED: {agents_md}") + + return True + + +def section(name: str) -> None: + print(f"\n{'=' * 50}\n {name}\n{'=' * 50}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Set up the ACE skill for an OpenClaw agent." + ) + parser.add_argument( + "--openclaw-home", + default=None, + help="Path to the OpenClaw home directory (default: ~/.openclaw).", + ) + parser.add_argument( + "--no-agents", + action="store_true", + help="Skip patching AGENTS.md.", + ) + args = parser.parse_args() + + section("Finding OpenClaw") + openclaw_home = find_openclaw_home(args.openclaw_home) + print(f" Found: {openclaw_home}") + + section("Copying skill folder") + skill_dest = copy_skill(openclaw_home) + print(f" Skill directory: {skill_dest}") + + if not args.no_agents: + section("Patching AGENTS.md") + patch_agents_md(openclaw_home) + else: + print("\n Skipping AGENTS.md (--no-agents)") + + section("Done") + print(f""" + Skill installed at: {skill_dest} + + Next steps: + 1. Build the ACE Docker image (see Dockerfile.ace) + 2. Pass your LLM API key in docker-compose.yml + 3. Restart the gateway: docker compose down && docker compose up -d + 4. Send a message — the agent will run ace-learn automatically + + Full guide: https://kayba-ai.github.io/agentic-context-engine/integrations/openclaw/ +""") + + +if __name__ == "__main__": + main() diff --git a/examples/openclaw/trace_examples/b3db607f-7ae8-4089-b806-44800e961672.jsonl b/examples/openclaw/trace_examples/b3db607f-7ae8-4089-b806-44800e961672.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..187f146a39f144339c3a7cf93bd3c6de8343f23b --- /dev/null +++ b/examples/openclaw/trace_examples/b3db607f-7ae8-4089-b806-44800e961672.jsonl @@ -0,0 +1,19 @@ +/speci{"type":"session","version":3,"id":"b3db607f-7ae8-4089-b806-44800e961672","timestamp":"2026-02-26T14:29:37.992Z","cwd":"/app"} +{"type":"message","id":"8511bf90","parentId":null,"timestamp":"2026-02-26T14:29:37.993Z","message":{"role":"assistant","content":[{"type":"text","text":"✅ New session started · model: litellm/anthropic/claude-sonnet-4-5 (default: openai-codex/gpt-5.3-codex)"}],"api":"openai-responses","provider":"openclaw","model":"delivery-mirror","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772116177993}} +{"type":"thinking_level_change","id":"5f7fe792","parentId":"8511bf90","timestamp":"2026-02-26T14:29:38.048Z","thinkingLevel":"high"} +{"type":"custom","customType":"model-snapshot","data":{"timestamp":1772116178049,"provider":"litellm","modelApi":"openai-completions","modelId":"anthropic/claude-sonnet-4-5"},"id":"4fbc976f","parentId":"5f7fe792","timestamp":"2026-02-26T14:29:38.049Z"} +{"type":"message","id":"0b84b221","parentId":"4fbc976f","timestamp":"2026-02-26T14:29:38.054Z","message":{"role":"user","content":[{"type":"text","text":"A new session was started via /new or /reset. Execute your Session Startup sequence now - read the required files before responding to the user. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning."}],"timestamp":1772116178052}} +{"type":"message","id":"cfc29da3","parentId":"0b84b221","timestamp":"2026-02-26T14:29:54.186Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"According to the instructions, I need to execute the Session Startup sequence. Looking at AGENTS.md:\n\n\"Every Session\"\nBefore doing anything else:\n1. Read `SOUL.md` — this is who you are\n2. Read `USER.md` — this is who you're helping\n3. Read `memory/YYYY-MM-DD.md` (today + yesterday) for recent context\n4. **If in MAIN SESSION** (direct chat with your human): Also read `MEMORY.md`\n\nThe inbound context shows this is a direct message on Telegram (chat_type: \"direct\"), so this is a MAIN SESSION. I should read MEMORY.md.\n\nI also need to check IDENTITY.md for persona details.\n\nLet me read these files in order:\n1. SOUL.md (already in context)\n2. USER.md (already in context)\n3. IDENTITY.md (already in context)\n4. MEMORY.md (need to read)\n5. Today's memory file (need to check date first)\n\nLooking at the runtime, I don't have the current date yet. I'll need to use session_status to get it, or just try reading the memory files.\n\nActually, looking at the files loaded:\n- SOUL.md - loaded\n- USER.md - loaded, shows Name: David\n- IDENTITY.md - loaded, shows Name: openclaw\n- BOOTSTRAP.md - exists, which means this might be a first-time setup\n\nWait, BOOTSTRAP.md existing means this is the first conversation. The BOOTSTRAP.md says \"Don't interrogate. Don't be robotic. Just... talk.\" and suggests starting with \"Hey. I just came online. Who am I? Who are you?\"\n\nBut the instruction says to execute the Session Startup sequence from AGENTS.md. Let me check what AGENTS.md says about BOOTSTRAP.md:\n\n\"## First Run\nIf `BOOTSTRAP.md` exists, that's your birth certificate. Follow it, then delete it. \n\nYou won't need it again.\"\n\nSo BOOTSTRAP.md takes precedence. I should follow the bootstrap process.\n\nHowever, the USER.md already has \"Name: David\" filled in, and IDENTITY.md has \"Name: openclaw\" filled in. So maybe this isn't truly first boot? I need to check MEMORY.md and the daily memory files to understand whether there's existing history or if this is genuinely the first interaction.","thinkingSignature":"reasoning_content"},{"type":"toolCall","id":"toolu_vrtx_014HNubtFNrbeeJT12zzBfGU","name":"read","arguments":{"file_path":"/home/node/.openclaw/workspace/MEMORY.md"}},{"type":"toolCall","id":"toolu_vrtx_01VeJMoaKQHn1F64oAg36GEu","name":"session_status","arguments":{}}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":13587,"output":1128,"cacheRead":0,"cacheWrite":0,"totalTokens":14715,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1772116178054}} +{"type":"message","id":"ea968aa5","parentId":"cfc29da3","timestamp":"2026-02-26T14:29:54.209Z","message":{"role":"toolResult","toolCallId":"toolu_vrtx_014HNubtFNrbeeJT12zzBfGU","toolName":"read","content":[{"type":"text","text":"{\n \"status\": \"error\",\n \"tool\": \"read\",\n \"error\": \"ENOENT: no such file or directory, access '/home/node/.openclaw/workspace/MEMORY.md'\"\n}"}],"details":{"status":"error","tool":"read","error":"ENOENT: no such file or directory, access '/home/node/.openclaw/workspace/MEMORY.md'"},"isError":false,"timestamp":1772116194205}} +{"type":"message","id":"ddf80983","parentId":"ea968aa5","timestamp":"2026-02-26T14:29:54.219Z","message":{"role":"toolResult","toolCallId":"toolu_vrtx_01VeJMoaKQHn1F64oAg36GEu","toolName":"session_status","content":[{"type":"text","text":"🦞 OpenClaw 2026.2.23 (a6ceb5b)\n🕒 Time: Thursday, February 26th, 2026 — 2:29 PM (UTC)\n🧠 Model: litellm/anthropic/claude-sonnet-4-5 · 🔑 api-key sk-Eaf…fVNQZg (models.json)\n🧮 Tokens: 14k in / 1.1k out · 💵 Cost: $0.0000\n📚 Context: 14k/200k (7%) · 🧹 Compactions: 0\n🧵 Session: agent:main:telegram:direct:1979163804 • updated just now\n⚙️ Runtime: direct · Think: high · elevated\n🪢 Queue: collect (depth 0)"}],"details":{"ok":true,"sessionKey":"agent:main:telegram:direct:1979163804","changedModel":false,"statusText":"🦞 OpenClaw 2026.2.23 (a6ceb5b)\n🕒 Time: Thursday, February 26th, 2026 — 2:29 PM (UTC)\n🧠 Model: litellm/anthropic/claude-sonnet-4-5 · 🔑 api-key sk-Eaf…fVNQZg (models.json)\n🧮 Tokens: 14k in / 1.1k out · 💵 Cost: $0.0000\n📚 Context: 14k/200k (7%) · 🧹 Compactions: 0\n🧵 Session: agent:main:telegram:direct:1979163804 • updated just now\n⚙️ Runtime: direct · Think: high · elevated\n🪢 Queue: collect (depth 0)"},"isError":false,"timestamp":1772116194217}} +{"type":"message","id":"5f896985","parentId":"ddf80983","timestamp":"2026-02-26T14:29:56.125Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_vrtx_01HyGt6bBwWf9ggsHpyDQAXT","name":"exec","arguments":{"command":"ls -la /home/node/.openclaw/workspace/memory/"}}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":13963,"output":65,"cacheRead":0,"cacheWrite":0,"totalTokens":14028,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1772116194219}} +{"type":"message","id":"d49127ce","parentId":"5f896985","timestamp":"2026-02-26T14:29:56.150Z","message":{"role":"toolResult","toolCallId":"toolu_vrtx_01HyGt6bBwWf9ggsHpyDQAXT","toolName":"exec","content":[{"type":"text","text":"ls: cannot access '/home/node/.openclaw/workspace/memory/': No such file or directory\n\n(Command exited with code 2)"}],"details":{"status":"completed","exitCode":2,"durationMs":18,"aggregated":"ls: cannot access '/home/node/.openclaw/workspace/memory/': No such file or directory\n\n(Command exited with code 2)","cwd":"/home/node/.openclaw/workspace"},"isError":false,"timestamp":1772116196148}} +{"type":"message","id":"636d13b5","parentId":"d49127ce","timestamp":"2026-02-26T14:29:58.917Z","message":{"role":"assistant","content":[{"type":"text","text":"Hey, I'm back. Fresh slate this session. What do you want to work on?\n\n(You're on Claude Sonnet 4.5 right now — your default is GPT-5.3 Codex.)"}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":14071,"output":53,"cacheRead":0,"cacheWrite":0,"totalTokens":14124,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772116196150}} +{"type":"message","id":"1a139a70","parentId":"636d13b5","timestamp":"2026-02-26T14:30:00.049Z","message":{"role":"user","content":[{"type":"text","text":"Hello world"}],"timestamp":1772116200047}} +{"type":"message","id":"6078164a","parentId":"1a139a70","timestamp":"2026-02-26T14:30:03.429Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"The user just said \"Hello world\" - a classic programmer greeting. This is a simple, friendly message. I should respond naturally and casually, keeping with my persona from SOUL.md (helpful, genuine, not performative).\n\nSince this is just a greeting, I can keep it light and conversational. No need to overthink it.","thinkingSignature":"reasoning_content"},{"type":"text","text":"Hello yourself. Ready when you are."}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":14159,"output":161,"cacheRead":0,"cacheWrite":0,"totalTokens":14320,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772116200049}} +{"type":"message","id":"a0f450dc","parentId":"6078164a","timestamp":"2026-02-26T14:30:13.794Z","message":{"role":"user","content":[{"type":"text","text":"Give me a poem of 2 paragraphs"}],"timestamp":1772116213793}} +{"type":"message","id":"3fa4d5d9","parentId":"a0f450dc","timestamp":"2026-02-26T14:30:19.766Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"The user wants a poem of 2 paragraphs. This is a straightforward creative request. I should write a poem with 2 stanzas/paragraphs. I'll keep it genuine and not overly flowery, matching the direct tone I've established.","thinkingSignature":"reasoning_content"},{"type":"text","text":"Here you go:\n\n**Waking Up**\n\nThe light comes slow, then all at once—\na crack beneath the door, a bird outside.\nYou reach for your phone, the world floods in,\na thousand voices asking where you've been.\n\nBut for a moment, in that space between\nthe dream and day, you're no one's anything.\nJust breathing. Just the weight of sheets and skin.\nJust you, before the needing to begin."}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":14184,"output":221,"cacheRead":0,"cacheWrite":0,"totalTokens":14405,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772116213794}} +{"type":"message","id":"9d74a168","parentId":"3fa4d5d9","timestamp":"2026-02-26T14:30:35.168Z","message":{"role":"user","content":[{"type":"text","text":"Each paragraph should be twice longer"}],"timestamp":1772116235166}} +{"type":"message","id":"4a2b69c0","parentId":"9d74a168","timestamp":"2026-02-26T14:30:40.805Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"The user wants each paragraph to be twice as long. So instead of 4 lines each, they want 8 lines each. Let me write a new poem with that structure.","thinkingSignature":"reasoning_content"},{"type":"text","text":"**Waking Up**\n\nThe light comes slow, then all at once—\na crack beneath the door, a bird outside.\nYou reach for your phone, the world floods in,\na thousand voices asking where you've been.\nThe notifications stack like debts unpaid,\nthe calendar already knows your name.\nSomeone needs an answer, someone needs your time,\nand you haven't even opened your eyes yet, not really.\n\nBut for a moment, in that space between\nthe dream and day, you're no one's anything.\nJust breathing. Just the weight of sheets and skin.\nJust you, before the needing to begin.\nThe coffee can wait. The emails too.\nThe version of yourself they all expect—\nthat person lives out there, beyond the door.\nIn here, you're still just learning how to be."}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":14297,"output":267,"cacheRead":0,"cacheWrite":0,"totalTokens":14564,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772116235168}} +{"type":"message","id":"04d20569","parentId":"4a2b69c0","timestamp":"2026-02-27T09:00:36.041Z","message":{"role":"user","content":[{"type":"text","text":"hi"}],"timestamp":1772182836032}} +{"type":"message","id":"5242736d","parentId":"04d20569","timestamp":"2026-02-27T09:00:48.929Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"The user just said \"hi\". This is a casual greeting after we've already been chatting. I should respond briefly and naturally, matching the casual tone. I don't need to do anything elaborate here - just acknowledge them and see if they need something.","thinkingSignature":"reasoning_content"},{"type":"text","text":"Hey. Need something?"}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":14486,"output":119,"cacheRead":0,"cacheWrite":0,"totalTokens":14605,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772182836038}} diff --git a/examples/openclaw/trace_examples/f967d602-325a-4a45-8d54-ee17484cfd96.jsonl b/examples/openclaw/trace_examples/f967d602-325a-4a45-8d54-ee17484cfd96.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..d19c8fc861a677e3bae6ded2c0fbd45131178e4a --- /dev/null +++ b/examples/openclaw/trace_examples/f967d602-325a-4a45-8d54-ee17484cfd96.jsonl @@ -0,0 +1,20 @@ +{"type":"session","version":3,"id":"f967d602-325a-4a45-8d54-ee17484cfd96","timestamp":"2026-02-27T15:29:48.817Z","cwd":"/app"} +{"type":"message","id":"f9e046ce","parentId":null,"timestamp":"2026-02-27T15:29:48.818Z","message":{"role":"assistant","content":[{"type":"text","text":"✅ New session started · model: litellm/anthropic/claude-sonnet-4-5 (default: openai-codex/gpt-5.3-codex)"}],"api":"openai-responses","provider":"openclaw","model":"delivery-mirror","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772206188818}} +{"type":"thinking_level_change","id":"940d5661","parentId":"f9e046ce","timestamp":"2026-02-27T15:29:48.998Z","thinkingLevel":"high"} +{"type":"custom","customType":"model-snapshot","data":{"timestamp":1772206189007,"provider":"litellm","modelApi":"openai-completions","modelId":"anthropic/claude-sonnet-4-5"},"id":"d0b242d2","parentId":"940d5661","timestamp":"2026-02-27T15:29:49.007Z"} +{"type":"message","id":"bf992ddd","parentId":"d0b242d2","timestamp":"2026-02-27T15:29:49.032Z","message":{"role":"user","content":[{"type":"text","text":"A new session was started via /new or /reset. Execute your Session Startup sequence now - read the required files before responding to the user. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning."}],"timestamp":1772206189019}} +{"type":"message","id":"5cebff48","parentId":"bf992ddd","timestamp":"2026-02-27T15:30:05.093Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"I need to execute the Session Startup sequence. According to AGENTS.md:\n\n1. Read `SOUL.md` — this is who I am\n2. Read `USER.md` — this is who I'm helping\n3. Read `memory/YYYY-MM-DD.md` (today + yesterday) for recent context\n4. **If in MAIN SESSION** (direct chat with my human): Also read `MEMORY.md`\n\nThe inbound context shows this is a direct message (chat_type: \"direct\") on Telegram, so this is a main session.\n\nLet me read these files in order:\n1. SOUL.md (already loaded in project context)\n2. USER.md (already loaded in project context)\n3. IDENTITY.md (already loaded in project context)\n4. MEMORY.md (if it exists)\n5. Today's memory file (if it exists)\n\nFrom the project context, I can see:\n- SOUL.md tells me to be genuinely helpful, not performatively helpful. Have opinions. Be resourceful. Be concise.\n- USER.md says the user's name is David\n- IDENTITY.md has name: openclaw, but the other fields are mostly placeholders\n- BOOTSTRAP.md exists, which suggests this might be a fresh setup\n\nLet me check if MEMORY.md and today's memory exist. I need to get the current date first - I can use session_status for that.\n\nActually, looking more carefully at the workspace files, BOOTSTRAP.md is still there, which means I haven't completed the initial setup conversation yet. This is a brand new agent.\n\nWait, let me re-read the instructions. It says \"Execute your Session Startup sequence now\" which means read the required files. The files are already in the project context, but I should still read MEMORY.md if it exists since this is a main session.\n\nLet me try to read MEMORY.md first to see if it exists.","thinkingSignature":"reasoning_content"},{"type":"toolCall","id":"toolu_vrtx_012Zmcgvp1RNMwUJQPzZ5Scw","name":"read","arguments":{"file_path":"/home/node/.openclaw/workspace/MEMORY.md"}}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":13961,"output":897,"cacheRead":0,"cacheWrite":0,"totalTokens":14858,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1772206189027}} +{"type":"message","id":"5ddd0c6d","parentId":"5cebff48","timestamp":"2026-02-27T15:30:05.141Z","message":{"role":"toolResult","toolCallId":"toolu_vrtx_012Zmcgvp1RNMwUJQPzZ5Scw","toolName":"read","content":[{"type":"text","text":"{\n \"status\": \"error\",\n \"tool\": \"read\",\n \"error\": \"ENOENT: no such file or directory, access '/home/node/.openclaw/workspace/MEMORY.md'\"\n}"}],"details":{"status":"error","tool":"read","error":"ENOENT: no such file or directory, access '/home/node/.openclaw/workspace/MEMORY.md'"},"isError":false,"timestamp":1772206205137}} +{"type":"message","id":"e239296b","parentId":"5ddd0c6d","timestamp":"2026-02-27T15:30:06.812Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_vrtx_019izxU3ccxxsRNhGvLqTCcp","name":"exec","arguments":{"command":"date +%Y-%m-%d"}}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":14060,"output":58,"cacheRead":0,"cacheWrite":0,"totalTokens":14118,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1772206205142}} +{"type":"message","id":"03540643","parentId":"e239296b","timestamp":"2026-02-27T15:30:06.850Z","message":{"role":"toolResult","toolCallId":"toolu_vrtx_019izxU3ccxxsRNhGvLqTCcp","toolName":"exec","content":[{"type":"text","text":"2026-02-27"}],"details":{"status":"completed","exitCode":0,"durationMs":19,"aggregated":"2026-02-27","cwd":"/home/node/.openclaw/workspace"},"isError":false,"timestamp":1772206206840}} +{"type":"message","id":"5a24c52c","parentId":"03540643","timestamp":"2026-02-27T15:30:08.959Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_vrtx_01NREUNznAjw2zwfT3189oFE","name":"read","arguments":{"file_path":"/home/node/.openclaw/workspace/memory/2026-02-27.md"}}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":14136,"output":72,"cacheRead":0,"cacheWrite":0,"totalTokens":14208,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1772206206850}} +{"type":"message","id":"4f4569b9","parentId":"5a24c52c","timestamp":"2026-02-27T15:30:08.971Z","message":{"role":"toolResult","toolCallId":"toolu_vrtx_01NREUNznAjw2zwfT3189oFE","toolName":"read","content":[{"type":"text","text":"{\n \"status\": \"error\",\n \"tool\": \"read\",\n \"error\": \"ENOENT: no such file or directory, access '/home/node/.openclaw/workspace/memory/2026-02-27.md'\"\n}"}],"details":{"status":"error","tool":"read","error":"ENOENT: no such file or directory, access '/home/node/.openclaw/workspace/memory/2026-02-27.md'"},"isError":false,"timestamp":1772206208968}} +{"type":"message","id":"8aa4ce59","parentId":"4f4569b9","timestamp":"2026-02-27T15:30:11.517Z","message":{"role":"assistant","content":[{"type":"toolCall","id":"toolu_vrtx_01At6JeqqCK1CWLGVd1bUApD","name":"read","arguments":{"file_path":"/home/node/.openclaw/workspace/memory/2026-02-26.md"}}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":14276,"output":72,"cacheRead":0,"cacheWrite":0,"totalTokens":14348,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1772206208971}} +{"type":"message","id":"b9d394fe","parentId":"8aa4ce59","timestamp":"2026-02-27T15:30:11.529Z","message":{"role":"toolResult","toolCallId":"toolu_vrtx_01At6JeqqCK1CWLGVd1bUApD","toolName":"read","content":[{"type":"text","text":"{\n \"status\": \"error\",\n \"tool\": \"read\",\n \"error\": \"ENOENT: no such file or directory, access '/home/node/.openclaw/workspace/memory/2026-02-26.md'\"\n}"}],"details":{"status":"error","tool":"read","error":"ENOENT: no such file or directory, access '/home/node/.openclaw/workspace/memory/2026-02-26.md'"},"isError":false,"timestamp":1772206211527}} +{"type":"message","id":"4c322f52","parentId":"b9d394fe","timestamp":"2026-02-27T15:30:13.981Z","message":{"role":"assistant","content":[{"type":"text","text":"Hey David. Fresh session here — running on Sonnet (default model is gpt-5.3-codex if you want to switch). What are we doing today?"}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":14416,"output":40,"cacheRead":0,"cacheWrite":0,"totalTokens":14456,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772206211530}} +{"type":"message","id":"ee41d27c","parentId":"4c322f52","timestamp":"2026-02-27T15:30:15.339Z","message":{"role":"user","content":[{"type":"text","text":"Hello"}],"timestamp":1772206215336}} +{"type":"message","id":"fc982841","parentId":"ee41d27c","timestamp":"2026-02-27T15:30:19.845Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"The user just said \"Hello\". This is a simple greeting. I should respond in a friendly, brief way consistent with my SOUL.md persona - be genuine, not performative. I should keep it natural and maybe gently prompt them if they need anything, but not be pushy.\n\nSince I already greeted them in my previous message, I can acknowledge their greeting and keep the conversation going naturally.","thinkingSignature":"reasoning_content"},{"type":"text","text":"Hey! What's up?"}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":14490,"output":181,"cacheRead":0,"cacheWrite":0,"totalTokens":14671,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772206215339}} +{"type":"message","id":"1d1ac7d7","parentId":"fc982841","timestamp":"2026-02-27T15:30:26.087Z","message":{"role":"user","content":[{"type":"text","text":"Please summarize this: It was my father who made the arrangements. My uncle helped, since he lived down south, where all this kind of business is carried out. I’m talking south-south: down past the airport, past the swamp, past the oilfields, everything. Way down at the bottom of the island, down where Columbus landed, long ago. There’s hardly anything down there, just fishing villages. Peaceful, old-time places: a few ramshackle wooden houses, and children playing marbles in the shade under the trees. Everywhere you look, in those places, you see pirogues pulled up onto the sand: little wooden boats, fishing boats, supposedly – although how much fishing the men really do nowadays is anybody’s guess. The boats are very basic. Open-topped, maybe twenty feet long, with just a plank of wood to sit on and a boat-engine clamped onto the back. But somehow they manage to make the journey across those seven miles of ocean between Trinidad and Venezuela – and carrying all kinds of things, not just fish. It’s a dangerous journey, though, even when special arrangements are made, as they were for me. My father brought me the clothes I was to wear: black T-shirt and long-pants; a big long-sleeved men’s shirt to wear over the T-shirt, also in black, and with the buttons cut out. We’d be travelling by night, and anything that might catch in torchlight had to be covered, including my own skin. The Trinidad and Tobago Coast Guard was one difficulty, the Guardia Nacional on the Venezuelan side was another, but the worst problem was bandits. Bandits would steal the engine off the back of the boat and leave you out there to drift, or they would take the whole boat, and throw you in the sea. I also had a pillowcase to go over my head: the reflection of torchlight against things like eyeballs and teeth had been known to give people away. The shirts, the trousers, the pillowcase – all these things were cheap, cheaply made; they weren’t things I would normally have worn, or owned. Probably, they came from some roadside vendor in central Trinidad. I doubt my father bought them himself: more likely, he sent one of his men out to do it – someone unimportant and unknown, who wouldn’t be recognised and connected to our family. I’m only guessing, of course. I didn’t ask about these details at the time. If I had asked, I’m sure someone would have said, ‘Dawn, girl, the less you know, the better.’ Buy the book Love Forms by Claire Adam Hardback Shop now Buying books using the ‘Buy the book’ links helps support our charitable work."}],"timestamp":1772206226084}} +{"type":"message","id":"338bfb91","parentId":"1d1ac7d7","timestamp":"2026-02-27T15:30:39.668Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to summarize this passage. Let me read through it carefully.\n\nThis is a narrative passage about someone named Dawn who is being smuggled from Trinidad to Venezuela by boat. Key points:\n\n1. Her father and uncle arranged the trip\n2. The journey starts from the remote southern tip of Trinidad, near where Columbus landed - an area with just fishing villages\n3. The journey is 7 miles across the ocean from Trinidad to Venezuela in small wooden boats (pirogues)\n4. It's a dangerous nighttime journey - threats include Trinidad Coast Guard, Venezuelan Guardia Nacional, and bandits\n5. She was given specific black clothing with buttons removed, and a pillowcase for her head - all to avoid reflecting torchlight that could give them away\n6. The cheap clothes were likely purchased discretely through an unimportant intermediary to avoid connection to her family\n7. She was kept in the dark about details for safety (\"the less you know, the better\")\n\nThis appears to be an excerpt from a book called \"Love Forms\" by Claire Adam about illegal migration/smuggling between Trinidad and Venezuela.\n\nI should provide a clear, concise summary.","thinkingSignature":"reasoning_content"},{"type":"text","text":"This is a first-person account of a woman named Dawn being smuggled from Trinidad to Venezuela by boat. Her father and uncle arranged the dangerous nighttime journey from a remote fishing village at Trinidad's southern tip—a seven-mile crossing in a small wooden pirogue (fishing boat). \n\nTo avoid detection by the Trinidad Coast Guard, Venezuelan Guardia Nacional, and bandits who steal boats and engines, she was given cheap black clothing with buttons removed and a pillowcase to cover her head—anything that might reflect torchlight (skin, eyes, teeth) had to be concealed. The clothes were likely bought discretely through an intermediary to avoid connecting the trip to her family. She was deliberately kept uninformed about details (\"the less you know, the better\").\n\nIt's an excerpt from the book *Love Forms* by Claire Adam, depicting the risks and secrecy involved in illegal border crossings between the two countries."}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":15096,"output":689,"cacheRead":0,"cacheWrite":0,"totalTokens":15785,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772206226086}} +{"type":"message","id":"73edb88d","parentId":"338bfb91","timestamp":"2026-02-27T15:30:57.691Z","message":{"role":"user","content":[{"type":"text","text":"in one sentence. I dont like long extracts"}],"timestamp":1772206257688}} +{"type":"message","id":"072e62d4","parentId":"73edb88d","timestamp":"2026-02-27T15:31:02.411Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"The user wants a one-sentence summary and doesn't like long extracts. I need to be concise.\n\nOne sentence summary: A woman named Dawn describes being smuggled from Trinidad to Venezuela by night in a small boat, wearing all black clothing and a pillowcase over her head to avoid detection by coast guards and bandits.","thinkingSignature":"reasoning_content"},{"type":"text","text":"A woman named Dawn describes being smuggled from Trinidad to Venezuela by night in a small boat, wearing all black clothing and a pillowcase over her head to avoid detection by coast guards and bandits."}],"api":"openai-completions","provider":"litellm","model":"anthropic/claude-sonnet-4-5","usage":{"input":15306,"output":186,"cacheRead":0,"cacheWrite":0,"totalTokens":15492,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772206257690}} diff --git a/examples/pipeline_composition/compose_custom_pipeline.py b/examples/pipeline_composition/compose_custom_pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..217dce66d3801dfbd7029da1e86e6ad0858e8b24 --- /dev/null +++ b/examples/pipeline_composition/compose_custom_pipeline.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Compose a custom ACE pipeline from individual steps. + +Demonstrates: + 1. Pipeline composition with a single import line from ace + 2. Adding a custom step to the pipeline + 3. Inspecting runner presets via build_steps() + 4. Running the pipeline directly via ACERunner + +Requires: + pip install ace-framework + export OPENAI_API_KEY=... # or any LiteLLM-supported provider +""" + +from __future__ import annotations + +from ace import ( + # Pipeline engine + Pipeline, + StepProtocol, + # ACE context + ACEStepContext, + ACERunner, + # Roles + Agent, + Reflector, + SkillManager, + # Steps + AgentStep, + EvaluateStep, + learning_tail, + # Types + ACE, + LiteLLMClient, + Sample, + Skillbook, + SimpleEnvironment, +) + +# ------------------------------------------------------------------ +# 1. Custom step — print the agent's answer between execute and learn +# ------------------------------------------------------------------ + + +class LogAnswerStep: + """A custom step that logs the agent's output before learning.""" + + requires = frozenset({"agent_output"}) + provides = frozenset() + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + print(f" -> Agent answered: {ctx.agent_output.final_answer}") + return ctx + + +# ------------------------------------------------------------------ +# 2. Compose a custom pipeline +# ------------------------------------------------------------------ + +MODEL = "gpt-4o-mini" + +llm = LiteLLMClient(model=MODEL) +skillbook = Skillbook() + +pipe = Pipeline( + [ + AgentStep(Agent(llm), skillbook), + EvaluateStep(SimpleEnvironment()), + LogAnswerStep(), # <-- custom step injected here + *learning_tail(Reflector(llm), SkillManager(llm), skillbook), + ] +) + +print(f"Custom pipeline: {len(pipe._steps)} steps") +print(f" requires: {pipe.requires}") +print(f" provides: {pipe.provides}") + +# ------------------------------------------------------------------ +# 3. Run using ACERunner +# ------------------------------------------------------------------ + +runner = ACERunner(pipeline=pipe, skillbook=skillbook) + +samples = [ + Sample(question="What is 2+2?", context="", ground_truth="4"), + Sample(question="Capital of France?", context="", ground_truth="Paris"), +] + +# Note: ACERunner._build_context is abstract, so we use ACE which +# provides _build_context for Sample objects. +runner_ace = ACE(pipeline=pipe, skillbook=skillbook) +results = runner_ace.run(samples, epochs=1) + +print(f"\nResults: {len(results)} samples processed") +print(f"Skills learned: {len(skillbook.skills())}") + +# ------------------------------------------------------------------ +# 4. Inspect and modify a runner's default steps with build_steps() +# ------------------------------------------------------------------ + +print("\n--- Inspecting ACE.build_steps() ---") +default_steps = ACE.build_steps( + agent=Agent(llm), + reflector=Reflector(llm), + skill_manager=SkillManager(llm), + environment=SimpleEnvironment(), + skillbook=Skillbook(), +) + +for i, step in enumerate(default_steps): + print(f" [{i}] {type(step).__name__}") + +# Modify: insert LogAnswerStep after EvaluateStep +default_steps.insert(2, LogAnswerStep()) +print(f"\nAfter inserting LogAnswerStep: {len(default_steps)} steps") + +# Build a new pipeline from the modified steps +modified_pipe = Pipeline(default_steps) +print(f"Modified pipeline ready with {len(modified_pipe._steps)} steps") diff --git a/examples/pipeline_ex/pipeline_demo.ipynb b/examples/pipeline_ex/pipeline_demo.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..24befb4f9e820ec41e66a093ade0526e1a17463a --- /dev/null +++ b/examples/pipeline_ex/pipeline_demo.ipynb @@ -0,0 +1,797 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "696836bd", + "metadata": {}, + "source": [ + "# Pipeline Engine — Interactive Demo\n", + "\n", + "This notebook walks through every feature of the generic pipeline engine.\n", + "Each cell is self-contained — run them top to bottom.\n", + "\n", + "**No external dependencies** — only the `pipeline/` package." + ] + }, + { + "cell_type": "markdown", + "id": "bf0704c7", + "metadata": {}, + "source": [ + "## Setup & Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "135705a5", + "metadata": { + "lines_to_next_cell": 1 + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Project root: /home/david/Desktop/projects/Kayba/agentic-context-engine\n", + "pipeline pkg: pipeline.pipeline\n" + ] + } + ], + "source": [ + "import sys, time\n", + "from types import MappingProxyType\n", + "from pathlib import Path\n", + "\n", + "# Jupyter already runs an asyncio event loop. Pipeline.run() calls\n", + "# asyncio.run() internally, which would fail. nest_asyncio patches the\n", + "# loop to allow nested calls.\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "\n", + "# Walk up from the notebook directory until we find the project root\n", + "# (identified by containing a `pipeline/` package directory).\n", + "_here = Path.cwd()\n", + "_root = _here\n", + "for _p in [_here] + list(_here.parents):\n", + " if (_p / \"pipeline\" / \"__init__.py\").exists():\n", + " _root = _p\n", + " break\n", + "sys.path.insert(0, str(_root))\n", + "\n", + "# Clear any stale import (ace/pipeline can shadow the top-level pipeline/).\n", + "if \"pipeline\" in sys.modules:\n", + " del sys.modules[\"pipeline\"]\n", + "\n", + "from pipeline import (\n", + " Pipeline,\n", + " Branch,\n", + " MergeStrategy,\n", + " StepContext,\n", + " SampleResult,\n", + " PipelineOrderError,\n", + " BranchError,\n", + ")\n", + "\n", + "def show(results: list[SampleResult]) -> None:\n", + " \"\"\"Pretty-print a list of SampleResult.\"\"\"\n", + " for r in results:\n", + " tag = \"OK\" if r.error is None else f\"FAIL @ {r.failed_at}\"\n", + " print(f\" [{tag}] sample={r.sample!r}\")\n", + " if r.output:\n", + " named = {k: getattr(r.output, k) for k in (\"agent_output\", \"environment_result\", \"reflection\") if getattr(r.output, k) is not None}\n", + " if named:\n", + " print(f\" fields: {named}\")\n", + " meta = dict(r.output.metadata)\n", + " if meta:\n", + " print(f\" metadata: {meta}\")\n", + " if r.error:\n", + " print(f\" error: {r.error}\")\n", + "\n", + "print(f\"Project root: {_root}\")\n", + "print(f\"pipeline pkg: {Pipeline.__module__}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ac62c4a4", + "metadata": {}, + "source": [ + "## Step Definitions\n", + "\n", + "A **step** is any object with:\n", + "- `requires: frozenset[str]` — metadata keys it reads\n", + "- `provides: frozenset[str]` — metadata keys it writes\n", + "- `__call__(ctx: StepContext) -> StepContext`\n", + "\n", + "No base class — pure duck typing via `StepProtocol`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3802f693", + "metadata": { + "lines_to_next_cell": 1 + }, + "outputs": [], + "source": [ + "class Tokenize:\n", + " \"\"\"Split sample text into words, store token list and count.\"\"\"\n", + " requires = frozenset()\n", + " provides = frozenset({\"tokens\", \"word_count\"})\n", + "\n", + " def __call__(self, ctx: StepContext) -> StepContext:\n", + " tokens = str(ctx.sample).split()\n", + " print(f\" [Tokenize] '{ctx.sample}' → {len(tokens)} tokens\")\n", + " return ctx.replace(metadata=MappingProxyType({\n", + " **ctx.metadata, \"tokens\": tokens, \"word_count\": len(tokens),\n", + " }))\n", + "\n", + "\n", + "class Uppercase:\n", + " \"\"\"Uppercase each token. Requires 'tokens' in metadata.\"\"\"\n", + " requires = frozenset({\"tokens\"})\n", + " provides = frozenset({\"upper_tokens\"})\n", + "\n", + " def __call__(self, ctx: StepContext) -> StepContext:\n", + " upper = [t.upper() for t in ctx.metadata[\"tokens\"]]\n", + " print(f\" [Uppercase] {ctx.metadata['tokens']} → {upper}\")\n", + " return ctx.replace(metadata=MappingProxyType({**ctx.metadata, \"upper_tokens\": upper}))\n", + "\n", + "\n", + "class Reverse:\n", + " \"\"\"Reverse each token. Designed to run in parallel with Uppercase.\"\"\"\n", + " requires = frozenset({\"tokens\"})\n", + " provides = frozenset({\"reversed_tokens\"})\n", + "\n", + " def __call__(self, ctx: StepContext) -> StepContext:\n", + " rev = [t[::-1] for t in ctx.metadata[\"tokens\"]]\n", + " print(f\" [Reverse] {ctx.metadata['tokens']} → {rev}\")\n", + " return ctx.replace(metadata=MappingProxyType({**ctx.metadata, \"reversed_tokens\": rev}))\n", + "\n", + "\n", + "class Summarize:\n", + " \"\"\"Combine processed metadata into a final agent_output string.\"\"\"\n", + " requires = frozenset({\"upper_tokens\", \"reversed_tokens\", \"word_count\"})\n", + " provides = frozenset({\"agent_output\"})\n", + "\n", + " def __call__(self, ctx: StepContext) -> StepContext:\n", + " summary = (\n", + " f\"{ctx.metadata['word_count']} words | \"\n", + " f\"upper={ctx.metadata['upper_tokens']} | \"\n", + " f\"rev={ctx.metadata['reversed_tokens']}\"\n", + " )\n", + " print(f\" [Summarize] → {summary}\")\n", + " return ctx.replace(agent_output=summary)\n", + "\n", + "\n", + "class Boom:\n", + " \"\"\"Always fails — used to demonstrate error handling.\"\"\"\n", + " requires = frozenset()\n", + " provides = frozenset()\n", + "\n", + " def __call__(self, ctx: StepContext) -> StepContext:\n", + " raise RuntimeError(f\"Boom on sample={ctx.sample!r}!\")" + ] + }, + { + "cell_type": "markdown", + "id": "a35014e6", + "metadata": {}, + "source": [ + "---\n", + "## 1. Basic Linear Pipeline\n", + "\n", + "Chain steps with `.then()`. The pipeline infers its **contracts**\n", + "(`requires` / `provides`) from the step chain automatically." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "0814c427", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Pipeline contracts:\n", + " requires = frozenset() ← external inputs the caller must provide\n", + " provides = frozenset({'upper_tokens', 'tokens', 'word_count'}) ← everything the pipeline writes\n", + "\n", + " [Tokenize] 'hello world' → 2 tokens\n", + " [Uppercase] ['hello', 'world'] → ['HELLO', 'WORLD']\n", + " [Tokenize] 'pipeline engine demo' → 3 tokens\n", + " [Uppercase] ['pipeline', 'engine', 'demo'] → ['PIPELINE', 'ENGINE', 'DEMO']\n", + " [OK] sample='hello world'\n", + " metadata: {'tokens': ['hello', 'world'], 'word_count': 2, 'upper_tokens': ['HELLO', 'WORLD']}\n", + " [OK] sample='pipeline engine demo'\n", + " metadata: {'tokens': ['pipeline', 'engine', 'demo'], 'word_count': 3, 'upper_tokens': ['PIPELINE', 'ENGINE', 'DEMO']}\n" + ] + } + ], + "source": [ + "pipe = Pipeline().then(Tokenize()).then(Uppercase())\n", + "\n", + "print(\"Pipeline contracts:\")\n", + "print(f\" requires = {pipe.requires} ← external inputs the caller must provide\")\n", + "print(f\" provides = {pipe.provides} ← everything the pipeline writes\")\n", + "print()\n", + "\n", + "results = pipe.run([\"hello world\", \"pipeline engine demo\"])\n", + "show(results)" + ] + }, + { + "cell_type": "markdown", + "id": "ddad0e30", + "metadata": {}, + "source": [ + "---\n", + "## 2. Contract Validation\n", + "\n", + "The engine validates step ordering at **construction time**.\n", + "If a step needs a field that a *later* step provides → `PipelineOrderError`.\n", + "\n", + "Fields not provided by *any* step are treated as **external inputs** — no error." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7f22a4ea", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Trying: Pipeline().then(Uppercase()).then(Tokenize())\n", + "\n", + " Caught PipelineOrderError:\n", + " Uppercase requires {'tokens'} but these are produced by a later step — check step ordering.\n", + "\n", + "External input is OK: requires = frozenset({'tokens'})\n" + ] + } + ], + "source": [ + "# Wrong order: Uppercase needs 'tokens', but Tokenize comes after\n", + "print(\"Trying: Pipeline().then(Uppercase()).then(Tokenize())\\n\")\n", + "\n", + "try:\n", + " Pipeline().then(Uppercase()).then(Tokenize())\n", + "except PipelineOrderError as e:\n", + " print(f\" Caught PipelineOrderError:\\n {e}\\n\")\n", + "\n", + "# External input: 'tokens' not produced by anyone → valid, caller must provide it\n", + "p = Pipeline().then(Uppercase())\n", + "print(f\"External input is OK: requires = {p.requires}\")" + ] + }, + { + "cell_type": "markdown", + "id": "1489099b", + "metadata": {}, + "source": [ + "---\n", + "## 3. Branch — Parallel Fork/Join\n", + "\n", + "`.branch()` fans out to N child pipelines in parallel, then **merges**\n", + "their outputs back into a single `StepContext`.\n", + "\n", + "```\n", + " ┌── Uppercase ──┐\n", + " Tokenize ──►──┤ ├──► Summarize\n", + " └── Reverse ──┘\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c559cbae", + "metadata": { + "lines_to_next_cell": 1 + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "requires = frozenset()\n", + "provides = frozenset({'upper_tokens', 'reversed_tokens', 'word_count', 'agent_output', 'tokens'})\n", + "\n", + " [Tokenize] 'fork join' → 2 tokens\n", + " [Uppercase] ['fork', 'join'] → ['FORK', 'JOIN']\n", + " [Reverse] ['fork', 'join'] → ['krof', 'nioj']\n", + " [Summarize] → 2 words | upper=['FORK', 'JOIN'] | rev=['krof', 'nioj']\n", + " [OK] sample='fork join'\n", + " fields: {'agent_output': \"2 words | upper=['FORK', 'JOIN'] | rev=['krof', 'nioj']\"}\n", + " metadata: {'tokens': ['fork', 'join'], 'word_count': 2, 'upper_tokens': ['FORK', 'JOIN'], 'reversed_tokens': ['krof', 'nioj']}\n" + ] + } + ], + "source": [ + "pipe = (\n", + " Pipeline()\n", + " .then(Tokenize())\n", + " .branch(\n", + " Pipeline().then(Uppercase()),\n", + " Pipeline().then(Reverse()),\n", + " merge=MergeStrategy.RAISE_ON_CONFLICT,\n", + " )\n", + " .then(Summarize())\n", + ")\n", + "\n", + "print(f\"requires = {pipe.requires}\")\n", + "print(f\"provides = {pipe.provides}\\n\")\n", + "\n", + "results = pipe.run([\"fork join\"])\n", + "show(results)" + ] + }, + { + "cell_type": "markdown", + "id": "ba7edad5", + "metadata": {}, + "source": [ + "---\n", + "## 4. Merge Strategies\n", + "\n", + "When branches write the **same named field**, the merge strategy decides what happens:\n", + "\n", + "| Strategy | Behaviour |\n", + "|---|---|\n", + "| `RAISE_ON_CONFLICT` | `ValueError` if any named field differs (metadata always LWW) |\n", + "| `LAST_WRITE_WINS` | Last branch's value wins for every field |\n", + "| `NAMESPACED` | Each branch stored at `metadata[\"branch_N\"]`, no conflict possible |" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9a49dd9b", + "metadata": {}, + "outputs": [], + "source": [ + "class WriteAnswer:\n", + " requires = frozenset()\n", + " provides = frozenset({\"agent_output\"})\n", + " def __init__(self, val: str):\n", + " self.val = val\n", + " def __call__(self, ctx):\n", + " return ctx.replace(agent_output=self.val)\n", + "\n", + "ctx = StepContext(sample=\"q\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "45c59d4e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "a) RAISE_ON_CONFLICT with conflict:\n", + "\n", + " Caught ValueError: Branch outputs conflict on fields {'agent_output'}. Use a different merge strategy or ensure branches write disjoint fields.\n" + ] + } + ], + "source": [ + "# a) RAISE_ON_CONFLICT — two branches write different values → error\n", + "print(\"a) RAISE_ON_CONFLICT with conflict:\\n\")\n", + "\n", + "b = Branch(\n", + " Pipeline().then(WriteAnswer(\"yes\")),\n", + " Pipeline().then(WriteAnswer(\"no\")),\n", + " merge=MergeStrategy.RAISE_ON_CONFLICT,\n", + ")\n", + "try:\n", + " b(ctx)\n", + "except ValueError as e:\n", + " print(f\" Caught ValueError: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "23d814f5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "b) LAST_WRITE_WINS:\n", + "\n", + " agent_output = 'no' ← second branch wins\n" + ] + } + ], + "source": [ + "# b) LAST_WRITE_WINS — second branch always wins\n", + "print(\"b) LAST_WRITE_WINS:\\n\")\n", + "\n", + "b = Branch(\n", + " Pipeline().then(WriteAnswer(\"yes\")),\n", + " Pipeline().then(WriteAnswer(\"no\")),\n", + " merge=MergeStrategy.LAST_WRITE_WINS,\n", + ")\n", + "out = b(ctx)\n", + "print(f\" agent_output = {out.agent_output!r} ← second branch wins\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "ac780329", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "c) NAMESPACED:\n", + "\n", + " agent_output = 'yes' ← from first branch\n", + " branch_0.agent_output = 'yes'\n", + " branch_1.agent_output = 'no'\n" + ] + } + ], + "source": [ + "# c) NAMESPACED — each branch isolated, accessible via metadata key\n", + "print(\"c) NAMESPACED:\\n\")\n", + "\n", + "b = Branch(\n", + " Pipeline().then(WriteAnswer(\"yes\")),\n", + " Pipeline().then(WriteAnswer(\"no\")),\n", + " merge=MergeStrategy.NAMESPACED,\n", + ")\n", + "out = b(ctx)\n", + "print(f\" agent_output = {out.agent_output!r} ← from first branch\")\n", + "print(f\" branch_0.agent_output = {out.metadata['branch_0'].agent_output!r}\")\n", + "print(f\" branch_1.agent_output = {out.metadata['branch_1'].agent_output!r}\")" + ] + }, + { + "cell_type": "markdown", + "id": "d626b1c3", + "metadata": {}, + "source": [ + "---\n", + "## 5. Error Handling\n", + "\n", + "Every sample produces a `SampleResult` — nothing is dropped silently.\n", + "A failing sample sets `error` and `failed_at`; other samples continue." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "577e2e13", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "All samples fail:\n", + "\n", + " [Tokenize] 'good luck' → 2 tokens\n", + " [FAIL @ Boom] sample='good luck'\n", + " error: Boom on sample='good luck'!\n" + ] + } + ], + "source": [ + "print(\"All samples fail:\\n\")\n", + "results = Pipeline().then(Tokenize()).then(Boom()).run([\"good luck\"])\n", + "show(results)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "62824a52", + "metadata": { + "lines_to_next_cell": 1 + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mixed success / failure:\n", + "\n", + " [Tokenize] 'ok' → 1 tokens\n", + " [Tokenize] 'bad input' → 2 tokens\n", + " [Tokenize] 'fine' → 1 tokens\n", + " [OK] sample='ok'\n", + " metadata: {'tokens': ['ok'], 'word_count': 1}\n", + " [FAIL @ MaybeBoom] sample='bad input'\n", + " error: bad sample!\n", + " [OK] sample='fine'\n", + " metadata: {'tokens': ['fine'], 'word_count': 1}\n" + ] + } + ], + "source": [ + "print(\"Mixed success / failure:\\n\")\n", + "\n", + "class MaybeBoom:\n", + " requires = frozenset()\n", + " provides = frozenset()\n", + " def __call__(self, ctx):\n", + " if \"bad\" in str(ctx.sample):\n", + " raise RuntimeError(\"bad sample!\")\n", + " return ctx\n", + "\n", + "results = Pipeline().then(Tokenize()).then(MaybeBoom()).run([\"ok\", \"bad input\", \"fine\"])\n", + "show(results)" + ] + }, + { + "cell_type": "markdown", + "id": "6261bf1e", + "metadata": {}, + "source": [ + "---\n", + "## 6. Async Boundary — Fire-and-Forget Background\n", + "\n", + "Set `async_boundary = True` on a step. Everything from that step onward\n", + "runs in a **background thread**. `run()` returns immediately.\n", + "\n", + "```\n", + " Foreground (fast) Background (slow)\n", + " ┌──────────┐ ┌───────────┐\n", + " │ Tokenize │ ──────►──── │ SlowScore │\n", + " └──────────┘ └───────────┘\n", + " │ │\n", + " run() returns updated later\n", + "```\n", + "\n", + "Call `wait_for_background()` to join all background threads." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "b84bca63", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " [Tokenize] 'fast return' → 2 tokens\n", + " [Tokenize] 'also fast' → 2 tokens\n", + "\n", + "run() returned in 0.004s — background still scoring\n", + "\n", + " [OK] sample='fast return'\n", + " [OK] sample='also fast'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " [SlowScore] sample='fast return' score=20 (background)\n", + " [SlowScore] sample='also fast' score=20 (background)\n" + ] + } + ], + "source": [ + "class SlowScore:\n", + " \"\"\"Expensive scoring step that runs in background.\"\"\"\n", + " requires = frozenset()\n", + " provides = frozenset({\"score\"})\n", + " async_boundary = True\n", + " max_workers = 2\n", + "\n", + " def __call__(self, ctx: StepContext) -> StepContext:\n", + " time.sleep(0.1)\n", + " score = ctx.metadata.get(\"word_count\", 0) * 10\n", + " print(f\" [SlowScore] sample={ctx.sample!r} score={score} (background)\")\n", + " return ctx.replace(metadata=MappingProxyType({**ctx.metadata, \"score\": score}))\n", + "\n", + "pipe = Pipeline().then(Tokenize()).then(SlowScore())\n", + "\n", + "t0 = time.monotonic()\n", + "results = pipe.run([\"fast return\", \"also fast\"], workers=2)\n", + "elapsed = time.monotonic() - t0\n", + "\n", + "print(f\"\\nrun() returned in {elapsed:.3f}s — background still scoring\\n\")\n", + "show(results)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "db875020", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waiting for background...\n", + "\n", + "Done!\n", + "\n", + " [OK] sample='fast return'\n", + " metadata: {'tokens': ['fast', 'return'], 'word_count': 2, 'score': 20}\n", + " [OK] sample='also fast'\n", + " metadata: {'tokens': ['also', 'fast'], 'word_count': 2, 'score': 20}\n" + ] + } + ], + "source": [ + "# Now wait for background to finish and inspect the updated results\n", + "print(\"Waiting for background...\\n\")\n", + "pipe.wait_for_background(timeout=5.0)\n", + "print(\"Done!\\n\")\n", + "show(results)" + ] + }, + { + "cell_type": "markdown", + "id": "a3805436", + "metadata": {}, + "source": [ + "---\n", + "## 7. Nested Pipelines\n", + "\n", + "A `Pipeline` satisfies `StepProtocol`, so it can be used as a step\n", + "inside another pipeline. Contracts are inferred recursively." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "6f21c83b", + "metadata": { + "lines_to_next_cell": 1 + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Inner: requires=frozenset(), provides=frozenset({'upper_tokens', 'tokens', 'word_count'})\n", + "Outer: requires=frozenset(), provides=frozenset({'upper_tokens', 'tokens', 'word_count', 'reversed_tokens'})\n", + "\n", + " [Tokenize] 'nested demo' → 2 tokens\n", + " [Uppercase] ['nested', 'demo'] → ['NESTED', 'DEMO']\n", + " [Reverse] ['nested', 'demo'] → ['detsen', 'omed']\n", + " [OK] sample='nested demo'\n", + " metadata: {'tokens': ['nested', 'demo'], 'word_count': 2, 'upper_tokens': ['NESTED', 'DEMO'], 'reversed_tokens': ['detsen', 'omed']}\n" + ] + } + ], + "source": [ + "inner = Pipeline().then(Tokenize()).then(Uppercase())\n", + "outer = Pipeline().then(inner).then(Reverse())\n", + "\n", + "print(f\"Inner: requires={inner.requires}, provides={inner.provides}\")\n", + "print(f\"Outer: requires={outer.requires}, provides={outer.provides}\\n\")\n", + "\n", + "results = outer.run([\"nested demo\"])\n", + "show(results)" + ] + }, + { + "cell_type": "markdown", + "id": "7eedb5cd", + "metadata": {}, + "source": [ + "---\n", + "## 8. Workers — Concurrent Sample Processing\n", + "\n", + "The `workers` parameter on `run()` controls how many samples are processed\n", + "in parallel (via an `asyncio.Semaphore` in the foreground event loop).\n", + "This is independent of `max_workers` on individual steps." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "ffe86852", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " workers=1 : 0.60s\n", + " workers=6 : 0.10s\n", + " speedup : 5.9x\n" + ] + } + ], + "source": [ + "class SlowStep:\n", + " requires = frozenset()\n", + " provides = frozenset({\"done\"})\n", + " def __call__(self, ctx):\n", + " time.sleep(0.1)\n", + " return ctx.replace(metadata=MappingProxyType({**ctx.metadata, \"done\": True}))\n", + "\n", + "samples = [f\"s{i}\" for i in range(6)]\n", + "pipe = Pipeline().then(SlowStep())\n", + "\n", + "t0 = time.monotonic()\n", + "pipe.run(samples, workers=1)\n", + "seq = time.monotonic() - t0\n", + "\n", + "t0 = time.monotonic()\n", + "pipe.run(samples, workers=6)\n", + "par = time.monotonic() - t0\n", + "\n", + "print(f\" workers=1 : {seq:.2f}s\")\n", + "print(f\" workers=6 : {par:.2f}s\")\n", + "print(f\" speedup : {seq / par:.1f}x\")" + ] + }, + { + "cell_type": "markdown", + "id": "7b426125", + "metadata": {}, + "source": [ + "---\n", + "## Summary\n", + "\n", + "| Concept | API |\n", + "|---|---|\n", + "| Linear chain | `Pipeline().then(A()).then(B())` |\n", + "| Contract inference | `pipe.requires`, `pipe.provides` |\n", + "| Parallel fork/join | `.branch(Pipeline().then(A()), Pipeline().then(B()))` |\n", + "| Merge control | `merge=MergeStrategy.RAISE_ON_CONFLICT / LAST_WRITE_WINS / NAMESPACED` |\n", + "| Error isolation | `SampleResult.error`, `SampleResult.failed_at` |\n", + "| Background execution | `async_boundary = True` on a step class |\n", + "| Background join | `pipe.wait_for_background(timeout=...)` |\n", + "| Nesting | Use a `Pipeline` as a step inside another `Pipeline` |\n", + "| Sample concurrency | `pipe.run(samples, workers=N)` |\n", + "\n", + "The pipeline engine is **domain-agnostic** — it knows nothing about ACE.\n", + "The `ace2/` package will add domain-specific steps (Agent, Evaluate,\n", + "Reflect, Update) on top of this engine." + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "executable": "/usr/bin/env python3", + "main_language": "python", + "notebook_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "ace-framework (3.12.0)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/pipeline_ex/pipeline_demo.py b/examples/pipeline_ex/pipeline_demo.py new file mode 100644 index 0000000000000000000000000000000000000000..719ce9f04cc5a981ac2957894f69c4fac6c8892a --- /dev/null +++ b/examples/pipeline_ex/pipeline_demo.py @@ -0,0 +1,444 @@ +#!/usr/bin/env python3 +# %% [markdown] +# # Pipeline Engine — Interactive Demo +# +# This notebook walks through every feature of the generic pipeline engine. +# Each cell is self-contained — run them top to bottom. +# +# **No external dependencies** — only the `pipeline/` package. + +# %% [markdown] +# ## Setup & Imports + +# %% +import sys, time +from types import MappingProxyType +from pathlib import Path + +# Jupyter already runs an asyncio event loop. Pipeline.run() calls +# asyncio.run() internally, which would fail. nest_asyncio patches the +# loop to allow nested calls. +import nest_asyncio + +nest_asyncio.apply() + +# Walk up from the script/notebook directory until we find the project root +# (identified by containing a `pipeline/` package directory). This works +# regardless of where the file lives under examples/. +_here = Path(__file__).resolve().parent if "__file__" in dir() else Path.cwd() +_root = _here +for _p in [_here] + list(_here.parents): + if (_p / "pipeline" / "__init__.py").exists(): + _root = _p + break +sys.path.insert(0, str(_root)) + +# Clear any stale import (ace/pipeline can shadow the top-level pipeline/). +if "pipeline" in sys.modules: + del sys.modules["pipeline"] + +from pipeline import ( + Pipeline, + Branch, + MergeStrategy, + StepContext, + SampleResult, + PipelineOrderError, + BranchError, +) + + +def show(results: list[SampleResult]) -> None: + """Pretty-print a list of SampleResult.""" + for r in results: + tag = "OK" if r.error is None else f"FAIL @ {r.failed_at}" + print(f" [{tag}] sample={r.sample!r}") + if r.output: + named = { + k: getattr(r.output, k) + for k in ("agent_output", "environment_result", "reflection") + if getattr(r.output, k) is not None + } + if named: + print(f" fields: {named}") + meta = dict(r.output.metadata) + if meta: + print(f" metadata: {meta}") + if r.error: + print(f" error: {r.error}") + + +# %% [markdown] +# ## Step Definitions +# +# A **step** is any object with: +# - `requires: frozenset[str]` — metadata keys it reads +# - `provides: frozenset[str]` — metadata keys it writes +# - `__call__(ctx: StepContext) -> StepContext` +# +# No base class — pure duck typing via `StepProtocol`. + + +# %% +class Tokenize: + """Split sample text into words, store token list and count.""" + + requires = frozenset() + provides = frozenset({"tokens", "word_count"}) + + def __call__(self, ctx: StepContext) -> StepContext: + tokens = str(ctx.sample).split() + print(f" [Tokenize] '{ctx.sample}' → {len(tokens)} tokens") + return ctx.replace( + metadata=MappingProxyType( + { + **ctx.metadata, + "tokens": tokens, + "word_count": len(tokens), + } + ) + ) + + +class Uppercase: + """Uppercase each token. Requires 'tokens' in metadata.""" + + requires = frozenset({"tokens"}) + provides = frozenset({"upper_tokens"}) + + def __call__(self, ctx: StepContext) -> StepContext: + upper = [t.upper() for t in ctx.metadata["tokens"]] + print(f" [Uppercase] {ctx.metadata['tokens']} → {upper}") + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "upper_tokens": upper}) + ) + + +class Reverse: + """Reverse each token. Designed to run in parallel with Uppercase.""" + + requires = frozenset({"tokens"}) + provides = frozenset({"reversed_tokens"}) + + def __call__(self, ctx: StepContext) -> StepContext: + rev = [t[::-1] for t in ctx.metadata["tokens"]] + print(f" [Reverse] {ctx.metadata['tokens']} → {rev}") + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "reversed_tokens": rev}) + ) + + +class Summarize: + """Combine processed metadata into a final agent_output string.""" + + requires = frozenset({"upper_tokens", "reversed_tokens", "word_count"}) + provides = frozenset({"agent_output"}) + + def __call__(self, ctx: StepContext) -> StepContext: + summary = ( + f"{ctx.metadata['word_count']} words | " + f"upper={ctx.metadata['upper_tokens']} | " + f"rev={ctx.metadata['reversed_tokens']}" + ) + print(f" [Summarize] → {summary}") + return ctx.replace(agent_output=summary) + + +class Boom: + """Always fails — used to demonstrate error handling.""" + + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + raise RuntimeError(f"Boom on sample={ctx.sample!r}!") + + +# %% [markdown] +# --- +# ## 1. Basic Linear Pipeline +# +# Chain steps with `.then()`. The pipeline infers its **contracts** +# (`requires` / `provides`) from the step chain automatically. + +# %% +pipe = Pipeline().then(Tokenize()).then(Uppercase()) + +print("Pipeline contracts:") +print(f" requires = {pipe.requires} ← external inputs the caller must provide") +print(f" provides = {pipe.provides} ← everything the pipeline writes") +print() + +results = pipe.run(["hello world", "pipeline engine demo"]) +show(results) + +# %% [markdown] +# --- +# ## 2. Contract Validation +# +# The engine validates step ordering at **construction time**. +# If a step needs a field that a *later* step provides → `PipelineOrderError`. +# +# Fields not provided by *any* step are treated as **external inputs** — no error. + +# %% +# Wrong order: Uppercase needs 'tokens', but Tokenize comes after +print("Trying: Pipeline().then(Uppercase()).then(Tokenize())\n") + +try: + Pipeline().then(Uppercase()).then(Tokenize()) +except PipelineOrderError as e: + print(f" Caught PipelineOrderError:\n {e}\n") + +# External input: 'tokens' not produced by anyone → valid, caller must provide it +p = Pipeline().then(Uppercase()) +print(f"External input is OK: requires = {p.requires}") + +# %% [markdown] +# --- +# ## 3. Branch — Parallel Fork/Join +# +# `.branch()` fans out to N child pipelines in parallel, then **merges** +# their outputs back into a single `StepContext`. +# +# ``` +# ┌── Uppercase ──┐ +# Tokenize ──►──┤ ├──► Summarize +# └── Reverse ──┘ +# ``` + +# %% +pipe = ( + Pipeline() + .then(Tokenize()) + .branch( + Pipeline().then(Uppercase()), + Pipeline().then(Reverse()), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + .then(Summarize()) +) + +print(f"requires = {pipe.requires}") +print(f"provides = {pipe.provides}\n") + +results = pipe.run(["fork join"]) +show(results) + +# %% [markdown] +# --- +# ## 4. Merge Strategies +# +# When branches write the **same named field**, the merge strategy decides what happens: +# +# | Strategy | Behaviour | +# |---|---| +# | `RAISE_ON_CONFLICT` | `ValueError` if any named field differs (metadata always LWW) | +# | `LAST_WRITE_WINS` | Last branch's value wins for every field | +# | `NAMESPACED` | Each branch stored at `metadata["branch_N"]`, no conflict possible | + + +# %% +class WriteAnswer: + requires = frozenset() + provides = frozenset({"agent_output"}) + + def __init__(self, val: str): + self.val = val + + def __call__(self, ctx): + return ctx.replace(agent_output=self.val) + + +ctx = StepContext(sample="q") + +# %% +# a) RAISE_ON_CONFLICT — two branches write different values → error +print("a) RAISE_ON_CONFLICT with conflict:\n") + +b = Branch( + Pipeline().then(WriteAnswer("yes")), + Pipeline().then(WriteAnswer("no")), + merge=MergeStrategy.RAISE_ON_CONFLICT, +) +try: + b(ctx) +except ValueError as e: + print(f" Caught ValueError: {e}") + +# %% +# b) LAST_WRITE_WINS — second branch always wins +print("b) LAST_WRITE_WINS:\n") + +b = Branch( + Pipeline().then(WriteAnswer("yes")), + Pipeline().then(WriteAnswer("no")), + merge=MergeStrategy.LAST_WRITE_WINS, +) +out = b(ctx) +print(f" agent_output = {out.agent_output!r} ← second branch wins") + +# %% +# c) NAMESPACED — each branch isolated, accessible via metadata key +print("c) NAMESPACED:\n") + +b = Branch( + Pipeline().then(WriteAnswer("yes")), + Pipeline().then(WriteAnswer("no")), + merge=MergeStrategy.NAMESPACED, +) +out = b(ctx) +print(f" agent_output = {out.agent_output!r} ← from first branch") +print(f" branch_0.agent_output = {out.metadata['branch_0'].agent_output!r}") +print(f" branch_1.agent_output = {out.metadata['branch_1'].agent_output!r}") + +# %% [markdown] +# --- +# ## 5. Error Handling +# +# Every sample produces a `SampleResult` — nothing is dropped silently. +# A failing sample sets `error` and `failed_at`; other samples continue. + +# %% +print("All samples fail:\n") +results = Pipeline().then(Tokenize()).then(Boom()).run(["good luck"]) +show(results) + +# %% +print("Mixed success / failure:\n") + + +class MaybeBoom: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx): + if "bad" in str(ctx.sample): + raise RuntimeError("bad sample!") + return ctx + + +results = Pipeline().then(Tokenize()).then(MaybeBoom()).run(["ok", "bad input", "fine"]) +show(results) + +# %% [markdown] +# --- +# ## 6. Async Boundary — Fire-and-Forget Background +# +# Set `async_boundary = True` on a step. Everything from that step onward +# runs in a **background thread**. `run()` returns immediately. +# +# ``` +# Foreground (fast) Background (slow) +# ┌──────────┐ ┌───────────┐ +# │ Tokenize │ ──────►──── │ SlowScore │ +# └──────────┘ └───────────┘ +# │ │ +# run() returns updated later +# ``` +# +# Call `wait_for_background()` to join all background threads. + + +# %% +class SlowScore: + """Expensive scoring step that runs in background.""" + + requires = frozenset() + provides = frozenset({"score"}) + async_boundary = True + max_workers = 2 + + def __call__(self, ctx: StepContext) -> StepContext: + time.sleep(0.1) + score = ctx.metadata.get("word_count", 0) * 10 + print(f" [SlowScore] sample={ctx.sample!r} score={score} (background)") + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "score": score})) + + +pipe = Pipeline().then(Tokenize()).then(SlowScore()) + +t0 = time.monotonic() +results = pipe.run(["fast return", "also fast"], workers=2) +elapsed = time.monotonic() - t0 + +print(f"\nrun() returned in {elapsed:.3f}s — background still scoring\n") +show(results) + +# %% +# Now wait for background to finish and inspect the updated results +print("Waiting for background...\n") +pipe.wait_for_background(timeout=5.0) +print("Done!\n") +show(results) + +# %% [markdown] +# --- +# ## 7. Nested Pipelines +# +# A `Pipeline` satisfies `StepProtocol`, so it can be used as a step +# inside another pipeline. Contracts are inferred recursively. + +# %% +inner = Pipeline().then(Tokenize()).then(Uppercase()) +outer = Pipeline().then(inner).then(Reverse()) + +print(f"Inner: requires={inner.requires}, provides={inner.provides}") +print(f"Outer: requires={outer.requires}, provides={outer.provides}\n") + +results = outer.run(["nested demo"]) +show(results) + +# %% [markdown] +# --- +# ## 8. Workers — Concurrent Sample Processing +# +# The `workers` parameter on `run()` controls how many samples are processed +# in parallel (via an `asyncio.Semaphore` in the foreground event loop). +# This is independent of `max_workers` on individual steps. + + +# %% +class SlowStep: + requires = frozenset() + provides = frozenset({"done"}) + + def __call__(self, ctx): + time.sleep(0.1) + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "done": True})) + + +samples = [f"s{i}" for i in range(6)] +pipe = Pipeline().then(SlowStep()) + +t0 = time.monotonic() +pipe.run(samples, workers=1) +seq = time.monotonic() - t0 + +t0 = time.monotonic() +pipe.run(samples, workers=6) +par = time.monotonic() - t0 + +print(f" workers=1 : {seq:.2f}s") +print(f" workers=6 : {par:.2f}s") +print(f" speedup : {seq / par:.1f}x") + +# %% [markdown] +# --- +# ## Summary +# +# | Concept | API | +# |---|---| +# | Linear chain | `Pipeline().then(A()).then(B())` | +# | Contract inference | `pipe.requires`, `pipe.provides` | +# | Parallel fork/join | `.branch(Pipeline().then(A()), Pipeline().then(B()))` | +# | Merge control | `merge=MergeStrategy.RAISE_ON_CONFLICT / LAST_WRITE_WINS / NAMESPACED` | +# | Error isolation | `SampleResult.error`, `SampleResult.failed_at` | +# | Background execution | `async_boundary = True` on a step class | +# | Background join | `pipe.wait_for_background(timeout=...)` | +# | Nesting | Use a `Pipeline` as a step inside another `Pipeline` | +# | Sample concurrency | `pipe.run(samples, workers=N)` | +# +# The pipeline engine is **domain-agnostic** — it knows nothing about ACE. +# The `ace2/` package will add domain-specific steps (Agent, Evaluate, +# Reflect, Update) on top of this engine. diff --git a/examples/seahorse-emoji-ace.gif b/examples/seahorse-emoji-ace.gif new file mode 100644 index 0000000000000000000000000000000000000000..b12c3559ca8a5e8f1f86e0dd2ca503a367c3bcf5 --- /dev/null +++ b/examples/seahorse-emoji-ace.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42807a0dcb58a3dbfaf547bed1abab023d7c64eaf080d7b3dabc572396f2cd85 +size 8477485 diff --git a/examples/tracing_glm_example.py b/examples/tracing_glm_example.py new file mode 100644 index 0000000000000000000000000000000000000000..7de3b8297d931052bde7edaadc53bc06d29bdc49 --- /dev/null +++ b/examples/tracing_glm_example.py @@ -0,0 +1,98 @@ +"""Example: instrument a GLM agent with Kayba tracing. + +Run: + uv run python examples/tracing_glm_example.py +""" + +from __future__ import annotations + +import os + +from dotenv import load_dotenv +from openai import OpenAI + +from ace.tracing import configure, start_span, trace + +load_dotenv() + +# --- Kayba tracing setup --------------------------------------------------- +configure( + api_key=os.environ["KAYBA_SDK_KEY"], + folder="examples", +) + +# --- GLM client via Zhipu's OpenAI-compatible endpoint ---------------------- +client = OpenAI( + base_url=os.environ["OPENAI_BASE_URL"], + api_key=os.environ["OPENAI_API_KEY"], +) +MODEL = "glm-5.1" + + +# --- Traced helper functions ------------------------------------------------ +@trace(name="llm_call", span_type="LLM") +def llm_call(messages: list[dict[str, str]]) -> str: + """Send a chat completion to GLM and return the text.""" + response = client.chat.completions.create( + model=MODEL, + messages=messages, + temperature=0.7, + ) + return response.choices[0].message.content or "" + + +@trace(name="research_agent") +def research_agent(topic: str) -> str: + """Agent that gathers key facts about a topic.""" + with start_span("build_prompt") as span: + messages = [ + { + "role": "system", + "content": "You are a research assistant. List 3 key facts.", + }, + {"role": "user", "content": f"Research this topic: {topic}"}, + ] + span.set_inputs({"topic": topic}) + span.set_outputs({"message_count": len(messages)}) + + result = llm_call(messages) + return result + + +@trace(name="summariser_agent") +def summariser_agent(facts: str) -> str: + """Agent that summarises research into a single paragraph.""" + with start_span("build_prompt") as span: + messages = [ + { + "role": "system", + "content": ( + "You are a summariser. Condense the following facts " + "into one concise paragraph." + ), + }, + {"role": "user", "content": facts}, + ] + span.set_inputs({"facts_length": len(facts)}) + span.set_outputs({"message_count": len(messages)}) + + result = llm_call(messages) + return result + + +@trace(name="pipeline") +def run_pipeline(topic: str) -> str: + """Two-agent pipeline: research → summarise.""" + facts = research_agent(topic) + print(f"\n--- Research Agent ---\n{facts}") + + summary = summariser_agent(facts) + print(f"\n--- Summariser Agent ---\n{summary}") + + return summary + + +# --- Main ------------------------------------------------------------------- +if __name__ == "__main__": + result = run_pipeline("The history of the Silk Road") + print(f"\n--- Final result ---\n{result}") diff --git a/main.py b/main.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a8ebfbc801b467cb230e9381f71621fa50c47d6d 100644 --- a/main.py +++ b/main.py @@ -0,0 +1,60 @@ +from fastapi import FastAPI, Request, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +import os +import json +import asyncio + +# Import ACE from the cloned agentic-context-engine directory +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), "src")) +from ace import ACELiteLLM + +app = FastAPI(title="Logic Engine with ACE") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +class ChatRequest(BaseModel): + prompt: str + model: str = "gpt-4o-mini" # Or any openrouter/nim model supported by LiteLLM + +# Configuration for Node 1 (Redis) and External Tools +REDIS_URL = os.environ.get("REDIS_URL", "https://augment17-redis-memory-core.hf.space") +VECTOR_DB_URL = os.environ.get("VECTOR_DB_URL", "") + +# Initialize ACE agent (using LiteLLM under the hood to support 100+ providers) +agent = ACELiteLLM(model=os.environ.get("DEFAULT_MODEL", "gpt-4o-mini")) + +@app.post("/chat") +async def chat_endpoint(request: ChatRequest): + try: + # Step 1: Use GitNexus tool (mocked via prompt injection for now) to sync latest repo state + repo_context = "GitNexus Synced: Repository 'kilo-code' is up to date." + + # Step 2: Use Tree-sitter AST Graph (mocked) + ast_context = "AST Graph: Found 3 dependent files in Redis Node 1." + + # Build enriched prompt + enriched_prompt = f"System Context:\n{repo_context}\n{ast_context}\n\nUser Request:\n{request.prompt}" + + # Step 3: Run ACE agent.ask() + # ACE automatically checks its Skillbook (which can be backed by Redis) for past learnings + response = agent.ask(enriched_prompt) + + # Step 4: After execution, trigger async reflection to learn from this trace + # agent.learn_from_traces(...) + + return {"response": response, "doc": True} + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/health") +def health(): + return {"status": "ACE Logic Engine Running"} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..7517821bce22f0e8c6ac3c1153237c7b249b0782 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,107 @@ +site_name: ACE Documentation +site_description: Agentic Context Engineering — Self-Improving Language Model Agents +site_url: https://kayba-ai.github.io/agentic-context-engine/ +repo_url: https://github.com/kayba-ai/agentic-context-engine +repo_name: kayba-ai/agentic-context-engine + +theme: + name: material + custom_dir: overrides + logo: assets/Kayba_logo_rounded.png + favicon: assets/Kayba_logo_rounded.png + palette: + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - navigation.top + - search.highlight + - search.share + - content.code.copy + - content.tabs.link + +plugins: + - search + - mike: + alias_type: symlink + canonical_version: latest + +extra: + version: + provider: mike + default: latest + +nav: + - Home: index.md + - Getting Started: + - Installation: getting-started/installation.md + - Setup: getting-started/setup.md + - Quick Start: getting-started/quick-start.md + - Concepts: + - How ACE Works: concepts/overview.md + - The Skillbook: concepts/skillbook.md + - Three Roles: concepts/roles.md + - Insight Levels: concepts/insight-levels.md + - Update Operations: concepts/updates.md + - Guides: + - Full Pipeline: guides/full-pipeline.md + - Integration Pattern: guides/integration.md + - Prompt Engineering: guides/prompts.md + - Async Learning: guides/async-learning.md + - Testing: guides/testing.md + - Pipeline Engine: + - Overview: pipeline/index.md + - Quick Start: pipeline/quick-start.md + - Core Concepts: pipeline/core-concepts.md + - Execution Model: pipeline/execution.md + - Branching & Parallelism: pipeline/branching.md + - Error Handling: pipeline/error-handling.md + - Building Custom Steps: pipeline/custom-steps.md + - API Reference: pipeline/api-reference.md + - Integrations: + - Overview: integrations/index.md + - LiteLLM: integrations/litellm.md + - LangChain: integrations/langchain.md + - Browser-Use: integrations/browser-use.md + - Claude Code: integrations/claude-code.md + - Claude SDK: integrations/claude-sdk.md + - MCP Server: integrations/mcp.md + - MCP Client Setup: integrations/mcp-client-setup.md + - Opik Observability: integrations/opik.md + - OpenClaw: integrations/openclaw.md + - Hosted API: integrations/hosted-api.md + - API Reference: api/index.md + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.tabbed: + alternate_style: true + - pymdownx.snippets + - attr_list + - md_in_html + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - toc: + permalink: true diff --git a/mlflow.db b/mlflow.db new file mode 100644 index 0000000000000000000000000000000000000000..8e6ba56ca1d8209a57512b5b880277bf28161dd8 --- /dev/null +++ b/mlflow.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6aa5d918b985bffa0040feb11f683e70272a30816ad1447e6c575c30b3609b8 +size 663552 diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000000000000000000000000000000000000..e6a695769b4a17811b37cec5c5bcc5329fce024a --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block htmltitle %} +<title>ACE Documentation +{% endblock %} diff --git a/pipeline/__init__.py b/pipeline/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..32b1e6132fcdf2bf82cdd5d1257468e03ba1102d --- /dev/null +++ b/pipeline/__init__.py @@ -0,0 +1,42 @@ +"""Generic pipeline engine, create and Compose pipelines, control execution mode. + +Public surface:: + + from pipeline import ( + Pipeline, + Branch, + MergeStrategy, + StepProtocol, + PipelineHook, + StepContext, + SampleResult, + CancellationToken, + cancel_token_var, + PipelineOrderError, + PipelineConfigError, + PipelineCancelled, + BranchError, + ) +""" + +from .branch import Branch, MergeStrategy +from .context import StepContext +from .errors import BranchError, CancellationToken, PipelineCancelled, PipelineConfigError, PipelineOrderError, cancel_token_var +from .pipeline import Pipeline +from .protocol import PipelineHook, SampleResult, StepProtocol + +__all__ = [ + "Pipeline", + "Branch", + "MergeStrategy", + "StepProtocol", + "PipelineHook", + "StepContext", + "SampleResult", + "CancellationToken", + "cancel_token_var", + "PipelineOrderError", + "PipelineConfigError", + "PipelineCancelled", + "BranchError", +] diff --git a/pipeline/branch.py b/pipeline/branch.py new file mode 100644 index 0000000000000000000000000000000000000000..6ec933fed04a1000530a993469c8a331ba539c25 --- /dev/null +++ b/pipeline/branch.py @@ -0,0 +1,209 @@ +"""Branch — parallel fork/join step.""" + +from __future__ import annotations + +import asyncio +import dataclasses +from concurrent.futures import ThreadPoolExecutor +from enum import Enum +from types import MappingProxyType +from typing import Callable + +from .context import StepContext +from .errors import BranchError + + +class MergeStrategy(Enum): + """Built-in merge strategies for Branch outputs.""" + + RAISE_ON_CONFLICT = "raise_on_conflict" + LAST_WRITE_WINS = "last_write_wins" + NAMESPACED = "namespaced" + + +# --------------------------------------------------------------------------- +# Built-in merge functions +# --------------------------------------------------------------------------- + + +def _merge_raise_on_conflict(ctxs: list[StepContext]) -> StepContext: + """Raise if any two branches wrote different values for the same field. + + Metadata is always merged (union across all branches; last writer wins + within metadata — there is no named-field semantic there). + + Uses ``type(ctxs[0])`` so subclass fields are included in the comparison. + """ + if len(ctxs) == 1: + return ctxs[0] + + conflicts: set[str] = set() + for f in dataclasses.fields(type(ctxs[0])): + if f.name == "metadata": + continue + first_val = getattr(ctxs[0], f.name) + if any(getattr(ctx, f.name) != first_val for ctx in ctxs[1:]): + conflicts.add(f.name) + + if conflicts: + raise ValueError( + f"Branch outputs conflict on fields {conflicts!r}. " + "Use a different merge strategy or ensure branches write disjoint fields." + ) + + merged_meta: dict = {} + for ctx in ctxs: + merged_meta.update(ctx.metadata) + + return dataclasses.replace(ctxs[0], metadata=MappingProxyType(merged_meta)) + + +def _merge_last_write_wins(ctxs: list[StepContext]) -> StepContext: + """Last branch's value wins for every conflicting field. + + Uses ``type(ctxs[0])`` so subclass fields are included in the comparison. + """ + if len(ctxs) == 1: + return ctxs[0] + + # Start from first context, overlay with each subsequent one + result = ctxs[0] + ctx_type = type(ctxs[0]) + for ctx in ctxs[1:]: + changes: dict = {} + for f in dataclasses.fields(ctx_type): + if f.name == "metadata": + continue + val = getattr(ctx, f.name) + if val != getattr(result, f.name): + changes[f.name] = val + if changes: + result = dataclasses.replace(result, **changes) + + merged_meta: dict = {} + for ctx in ctxs: + merged_meta.update(ctx.metadata) + + return dataclasses.replace(result, metadata=MappingProxyType(merged_meta)) + + +def _merge_namespaced(ctxs: list[StepContext]) -> StepContext: + """Each branch's output is stored at ``ctx.metadata["branch_N"]``. + + Named fields are taken from the first branch; no conflict is possible + because branch outputs are kept in separate metadata keys. + """ + base = ctxs[0] + extra: dict = {f"branch_{i}": ctx for i, ctx in enumerate(ctxs)} + merged_meta = MappingProxyType({**base.metadata, **extra}) + return dataclasses.replace(base, metadata=merged_meta) + + +_BUILTIN_MERGES: dict[MergeStrategy, Callable] = { + MergeStrategy.RAISE_ON_CONFLICT: _merge_raise_on_conflict, + MergeStrategy.LAST_WRITE_WINS: _merge_last_write_wins, + MergeStrategy.NAMESPACED: _merge_namespaced, +} + + +# --------------------------------------------------------------------------- +# Branch +# --------------------------------------------------------------------------- + + +class Branch: + """Runs multiple pipelines in parallel, then merges their outputs. + + ``Branch`` satisfies ``StepProtocol`` — it can be used wherever a step + is expected. ``requires`` and ``provides`` are inferred from the union + of the child pipelines' contracts. + + In sync contexts (called directly), fan-out is via + ``ThreadPoolExecutor``. In async contexts (awaited), fan-out is via + ``asyncio.gather``. + + All branches always run to completion before any failure is raised — + ``BranchError`` carries the full list of failures. + """ + + def __init__( + self, + *pipelines: object, + merge: MergeStrategy | Callable = MergeStrategy.RAISE_ON_CONFLICT, + ) -> None: + if not pipelines: + raise ValueError("Branch requires at least one child pipeline.") + + self.pipelines = list(pipelines) + + if callable(merge) and not isinstance(merge, MergeStrategy): + self._merge_fn: Callable = merge + else: + self._merge_fn = _BUILTIN_MERGES[merge] # type: ignore[index] + + # Infer requires/provides from the union of child contracts + all_requires: set[str] = set() + all_provides: set[str] = set() + for p in self.pipelines: + all_requires |= set(getattr(p, "requires", frozenset())) + all_provides |= set(getattr(p, "provides", frozenset())) + + self.requires: frozenset[str] = frozenset(all_requires) + self.provides: frozenset[str] = frozenset(all_provides) + + # ------------------------------------------------------------------ + # Sync execution + # ------------------------------------------------------------------ + + def __call__(self, ctx: StepContext) -> StepContext: + """Sync fan-out via ThreadPoolExecutor. + + All branches receive the same (frozen) context — no copy needed. + All branches run to completion before any failure is raised. + """ + with ThreadPoolExecutor(max_workers=len(self.pipelines)) as executor: + futures: list = [executor.submit(p, ctx) for p in self.pipelines] # type: ignore[arg-type] + results: list[StepContext] = [] + failures: list[BaseException] = [] + for f in futures: + try: + results.append(f.result()) + except BaseException as exc: # noqa: BLE001 + failures.append(exc) + + if failures: + raise BranchError(failures) + + return self._merge_fn(results) + + # ------------------------------------------------------------------ + # Async execution + # ------------------------------------------------------------------ + + async def __call_async__(self, ctx: StepContext) -> StepContext: + """Async fan-out via asyncio.gather. + + ``return_exceptions=True`` guarantees all branches run to completion + even when one fails; the full failure list is surfaced via + ``BranchError``. + + Sync child pipelines are wrapped with ``asyncio.to_thread`` so they + run in a thread pool rather than blocking the event loop. + """ + + async def _run_child(child: object) -> StepContext: + if asyncio.iscoroutinefunction(getattr(child, "__call__", None)): + return await child(ctx) # type: ignore[operator] + if hasattr(child, "__call_async__"): + return await child.__call_async__(ctx) # type: ignore[union-attr] + # Sync callable — run in thread pool so it doesn't block the loop + return await asyncio.to_thread(child, ctx) # type: ignore[arg-type] + + raw = await asyncio.gather( + *[_run_child(p) for p in self.pipelines], + return_exceptions=True, + ) + failures = [r for r in raw if isinstance(r, BaseException)] + if failures: + raise BranchError(failures) + return self._merge_fn([r for r in raw if not isinstance(r, BaseException)]) diff --git a/pipeline/context.py b/pipeline/context.py new file mode 100644 index 0000000000000000000000000000000000000000..21f9530caff88d1bcb26b4a65a342ccca5a2e976 --- /dev/null +++ b/pipeline/context.py @@ -0,0 +1,37 @@ +"""Immutable step context — the single object that flows through every step.""" + +from __future__ import annotations + +import dataclasses +from dataclasses import dataclass, field +from types import MappingProxyType +from typing import Any, Self + + +@dataclass(frozen=True) +class StepContext: + """Frozen context object passed from step to step. + + The pipeline engine only requires ``sample`` and ``metadata``. All + domain-specific fields are added by subclassing — the engine never reads + anything beyond these two fields. + + Consuming applications subclass ``StepContext`` to add named fields for + concepts shared across their pipelines. Integration-specific data goes + in ``metadata`` to prevent field accumulation on the subclass. + + Steps never mutate the incoming context — they call ``.replace()`` to + produce a new one. + """ + + sample: Any = None + metadata: MappingProxyType = field(default_factory=lambda: MappingProxyType({})) + + def __post_init__(self) -> None: + # Coerce plain dict → MappingProxyType so mutation is a hard runtime error + if not isinstance(self.metadata, MappingProxyType): + object.__setattr__(self, "metadata", MappingProxyType(self.metadata)) + + def replace(self, **changes: Any) -> Self: + """Return a new context with the given fields replaced.""" + return dataclasses.replace(self, **changes) diff --git a/pipeline/errors.py b/pipeline/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..6085f30620c2b01f8d5d27f77e8c97659ce2cf8f --- /dev/null +++ b/pipeline/errors.py @@ -0,0 +1,78 @@ +"""Pipeline error types and cancellation primitives.""" + +from __future__ import annotations + +import threading +from contextvars import ContextVar + + +class PipelineOrderError(Exception): + """A step requires a field that no earlier step provides.""" + + +class PipelineConfigError(Exception): + """Invalid pipeline wiring. + + Examples: + - More than one ``async_boundary = True`` step in the same pipeline. + - An ``async_boundary = True`` step inside a Branch child. + """ + + +class BranchError(Exception): + """One or more branch pipelines failed. + + All branches always run to completion before this is raised. + ``failures`` contains the full list of exceptions — one per failed branch. + """ + + def __init__(self, failures: list[BaseException]) -> None: + self.failures = failures + super().__init__( + f"{len(failures)} branch(es) failed: " + + "; ".join(type(e).__name__ for e in failures) + ) + + +class PipelineCancelled(Exception): + """A ``cancel_token`` was triggered between steps. + + Surfaces in ``SampleResult.error`` — never propagated to the caller of + ``run()`` / ``run_async()``. Callers check for this type to distinguish + cancellation from step failures. + """ + + +class CancellationToken: + """Thread-safe cancellation signal for pipeline runs. + + Create a fresh token per ``run()`` / ``run_async()`` invocation. The + pipeline checks ``is_cancelled`` between steps; call ``cancel()`` from + any thread to stop processing. + + Uses ``threading.Event`` so it is safe to signal from a web endpoint + handler, a background task, or a signal handler. + """ + + def __init__(self) -> None: + self._cancelled = threading.Event() + + def cancel(self) -> None: + """Signal cancellation. Thread-safe, idempotent.""" + self._cancelled.set() + + @property + def is_cancelled(self) -> bool: + return self._cancelled.is_set() + + +# --------------------------------------------------------------------------- +# Contextvar bridge — makes the current cancel token visible inside steps +# without threading it through every method signature. +# Pipeline.run_async() sets this; LLM clients read it. +# asyncio.to_thread() copies contextvars automatically. +# --------------------------------------------------------------------------- + +cancel_token_var: ContextVar[CancellationToken | None] = ContextVar( + "cancel_token_var", default=None +) diff --git a/pipeline/pipeline.py b/pipeline/pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..60d26cfe52d3b6f71bf001f5326da1986d8b3918 --- /dev/null +++ b/pipeline/pipeline.py @@ -0,0 +1,473 @@ +"""Pipeline — concrete, composable, sequential step runner.""" + +from __future__ import annotations + +import asyncio +import logging +import threading +import time +import warnings +from collections.abc import Callable, Iterable +from concurrent.futures import ThreadPoolExecutor +from typing import Any + +from .branch import Branch, MergeStrategy +from .context import StepContext +from .errors import CancellationToken, PipelineCancelled, PipelineConfigError, PipelineOrderError, cancel_token_var +from .protocol import PipelineHook, SampleResult + + +# --------------------------------------------------------------------------- +# Per-step-class background executor registry +# --------------------------------------------------------------------------- + +_executor_lock = threading.Lock() + + +def _get_class_executor(step_cls: type) -> ThreadPoolExecutor: + """Return the class-level ThreadPoolExecutor for *step_cls*, creating it lazily. + + The executor is stored on the class itself (``step_cls._executor``) so it + is shared across all pipeline instances. ``max_workers`` defaults to 1 + if not declared on the class. + """ + if not hasattr(step_cls, "_executor") or getattr(step_cls, "_executor") is None: + with _executor_lock: + # Double-checked locking + if ( + not hasattr(step_cls, "_executor") + or getattr(step_cls, "_executor") is None + ): + max_workers = getattr(step_cls, "max_workers", 1) + setattr( + step_cls, "_executor", ThreadPoolExecutor(max_workers=max_workers) + ) + return getattr(step_cls, "_executor") + + +# --------------------------------------------------------------------------- +# Pipeline +# --------------------------------------------------------------------------- + + +class Pipeline: + """Ordered sequence of steps. Satisfies StepProtocol — can be nested. + + Build via the fluent API:: + + pipe = ( + Pipeline() + .then(AgentStep()) + .then(EvaluateStep()) + .then(ReflectStep()) # ReflectStep.async_boundary = True + .then(UpdateStep()) + ) + + Fan-out across samples:: + + results = pipe.run(samples, workers=4) + + ``requires`` and ``provides`` are inferred from the step chain and kept + up-to-date as steps are added, so a ``Pipeline`` can itself be used as a + step inside another pipeline without extra annotation. + """ + + def __init__( + self, + steps: list | None = None, + hooks: list[PipelineHook] | None = None, + ) -> None: + self._steps: list = list(steps or []) + self._hooks: list[PipelineHook] = list(hooks or []) + self.requires, self.provides = self._infer_contracts(self._steps) + self._validate_steps(self._steps) + + # Background thread tracking (per Pipeline instance) + self._bg_threads: list[threading.Thread] = [] + self._bg_lock = threading.Lock() + + # ------------------------------------------------------------------ + # Hook helpers + # ------------------------------------------------------------------ + + def _fire_before(self, step_name: str, ctx: StepContext) -> None: + for hook in self._hooks: + try: + hook.before_step(step_name, ctx) + except Exception: + logging.getLogger(__name__).exception( + "Hook %s.before_step raised — ignoring", type(hook).__name__ + ) + + def _fire_after(self, step_name: str, ctx: StepContext) -> None: + for hook in self._hooks: + try: + hook.after_step(step_name, ctx) + except Exception: + logging.getLogger(__name__).exception( + "Hook %s.after_step raised — ignoring", type(hook).__name__ + ) + + # ------------------------------------------------------------------ + # Contract inference + # ------------------------------------------------------------------ + + @staticmethod + def _infer_contracts(steps: list) -> tuple[frozenset, frozenset]: + """Compute (requires, provides) for the full step chain. + + ``requires`` — fields the pipeline needs from the outside + (what its first steps need that no earlier inner + step provides). + ``provides`` — union of everything any inner step writes. + """ + provided_so_far: set[str] = set() + external_requires: set[str] = set() + for step in steps: + step_requires = set(getattr(step, "requires", frozenset())) + step_provides = set(getattr(step, "provides", frozenset())) + external_requires |= step_requires - provided_so_far + provided_so_far |= step_provides + return frozenset(external_requires), frozenset(provided_so_far) + + # ------------------------------------------------------------------ + # Validation + # ------------------------------------------------------------------ + + @staticmethod + def _validate_steps(steps: list) -> None: + """Raise PipelineOrderError or PipelineConfigError for invalid wiring. + + Order check: + If step B requires field X, and field X is produced by some step + in the pipeline but that step appears *after* B, raise + ``PipelineOrderError``. Fields not produced by any step in the + pipeline are treated as external inputs — no error. + + Config checks: + - More than one ``async_boundary = True`` step in the same pipeline. + - Any ``async_boundary = True`` step inside a Branch child. + - Warning (not error) when ``async_boundary`` is set on a nested + Pipeline (the boundary is ignored when the pipeline runs as a step). + """ + # Pre-compute all fields ever produced internally + all_provided_internally: set[str] = set() + for step in steps: + all_provided_internally |= set(getattr(step, "provides", frozenset())) + + provided_so_far: set[str] = set() + boundary_count = 0 + + for step in steps: + step_requires = set(getattr(step, "requires", frozenset())) + step_provides = set(getattr(step, "provides", frozenset())) + + # Ordering: field is produced internally but not yet available + out_of_order = (step_requires & all_provided_internally) - provided_so_far + if out_of_order: + raise PipelineOrderError( + f"{type(step).__name__} requires {out_of_order!r} but these " + f"are produced by a later step — check step ordering." + ) + + provided_so_far |= step_provides + + # async_boundary: only one per pipeline + if getattr(step, "async_boundary", False): + boundary_count += 1 + if boundary_count > 1: + raise PipelineConfigError( + f"Only one async_boundary step is allowed per pipeline; " + f"{type(step).__name__} is a duplicate." + ) + + # async_boundary inside a Branch child is forbidden + if isinstance(step, Branch): + for child in step.pipelines: + for child_step in getattr(child, "_steps", []): + if getattr(child_step, "async_boundary", False): + raise PipelineConfigError( + f"async_boundary is not allowed inside a Branch " + f"child (found on {type(child_step).__name__})." + ) + + # Warn when async_boundary is set on a nested Pipeline (ignored) + if isinstance(step, Pipeline) and getattr(step, "async_boundary", False): + warnings.warn( + f"async_boundary declared on a nested Pipeline " + f"({type(step).__name__}) is ignored — the boundary only " + f"fires when the pipeline is used as a top-level runner.", + stacklevel=4, + ) + + # ------------------------------------------------------------------ + # Fluent builder + # ------------------------------------------------------------------ + + def then(self, step: object) -> "Pipeline": + """Append *step* and return ``self`` for chaining.""" + new_steps = self._steps + [step] + # Validate before mutating so errors are raised immediately + self._validate_steps(new_steps) + self._steps = new_steps + self.requires, self.provides = self._infer_contracts(self._steps) + return self + + def branch( + self, + *pipelines: object, + merge: MergeStrategy | Any = MergeStrategy.RAISE_ON_CONFLICT, + ) -> "Pipeline": + """Append a Branch step and return ``self`` for chaining.""" + return self.then(Branch(*pipelines, merge=merge)) + + # ------------------------------------------------------------------ + # __call__ — for use as a nested step + # ------------------------------------------------------------------ + + def __call__(self, ctx: StepContext) -> StepContext: + """Run all steps sequentially (sync). + + When used as a nested step inside another pipeline, ``async_boundary`` + markers are **ignored** (a warning is already emitted at construction + time). All steps — sync and async — are executed to completion before + returning. + """ + for step in self._steps: + if asyncio.iscoroutinefunction(step.__call__): + # Run the coroutine in a new event loop (safe in non-async contexts) + ctx = asyncio.run(step(ctx)) + elif isinstance(step, Branch): + # Branch.__call__ is sync (ThreadPoolExecutor) + ctx = step(ctx) + else: + ctx = step(ctx) + return ctx + + # ------------------------------------------------------------------ + # Async_boundary helpers + # ------------------------------------------------------------------ + + def _find_boundary_index(self) -> int | None: + """Return the index of the first async_boundary step, or None.""" + for i, step in enumerate(self._steps): + if getattr(step, "async_boundary", False): + return i + return None + + # ------------------------------------------------------------------ + # Background execution + # ------------------------------------------------------------------ + + def _submit_background( + self, + ctx: StepContext, + background_steps: list, + result: SampleResult, + ) -> None: + """Run *background_steps* sequentially in a background thread. + + Each step is submitted to its own class-level executor so concurrency + across samples is controlled by ``max_workers`` on the step class — + independent of how many pipeline instances or background tails are + running. + + ``result`` is mutated in-place when the tail completes (or fails). + """ + + def run_tail() -> None: + current_ctx = ctx + for step in background_steps: + step_cls = type(step) + executor = _get_class_executor(step_cls) + try: + # Submit to per-step-class pool; block until slot is free + future = executor.submit(step, current_ctx) + current_ctx = future.result() + except Exception as exc: + result.error = exc + result.failed_at = step_cls.__name__ + result.output = None + return + result.output = current_ctx + + t = threading.Thread(target=run_tail, daemon=True, name="pipeline-bg") + with self._bg_lock: + self._bg_threads.append(t) + t.start() + + def wait_for_background(self, timeout: float | None = None) -> None: + """Block until all background tasks submitted by this pipeline finish. + + Raises ``TimeoutError`` if *timeout* seconds elapse before all tasks + complete. Completed threads are removed from the tracking list. + """ + with self._bg_lock: + threads = list(self._bg_threads) + + deadline = None if timeout is None else time.monotonic() + timeout + + for t in threads: + if deadline is not None: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError( + "Background pipeline steps did not drain within timeout." + ) + t.join(timeout=remaining) + if t.is_alive(): + raise TimeoutError( + "Background pipeline steps did not drain within timeout." + ) + else: + t.join() + + # Remove completed threads + with self._bg_lock: + self._bg_threads = [t for t in self._bg_threads if t.is_alive()] + + def background_stats(self) -> dict[str, int]: + """Return a snapshot of background task progress. + + Returns a dict with ``active`` and ``completed`` counts. Safe to + call from any thread while the pipeline is running. + """ + with self._bg_lock: + threads = list(self._bg_threads) + active = sum(1 for t in threads if t.is_alive()) + completed = len(threads) - active + return {"active": active, "completed": completed} + + # ------------------------------------------------------------------ + # run() — sync entry point + # ------------------------------------------------------------------ + + def run( + self, + contexts: Iterable[StepContext], + workers: int = 1, + on_sample_done: Callable[[SampleResult], None] | None = None, + cancel_token: CancellationToken | None = None, + ) -> list[SampleResult]: + """Process *contexts* through the pipeline (sync entry point). + + Each item must be a fully-initialized ``StepContext``. The pipeline + never wraps or re-creates contexts — it processes what it receives. + + Splits at the first ``async_boundary`` step: + - Foreground steps run in the calling context (with up to ``workers`` + samples in parallel via a semaphore inside the event loop). + - Background steps are submitted to per-step-class executors and run + asynchronously. Call ``wait_for_background()`` to block until they + finish and ``SampleResult`` objects are fully populated. + + Every context produces exactly one ``SampleResult`` — nothing is + dropped silently. + + Args: + contexts: Input contexts to process. + workers: Maximum number of contexts processed concurrently. + on_sample_done: Optional callback invoked after each sample + completes its foreground steps (or fails). Receives the + ``SampleResult``. Must not block the event loop. + cancel_token: Optional cancellation signal. Checked before + each step and each new sample. Pass a fresh token per + invocation; the pipeline object stays reusable. + """ + return asyncio.run( + self.run_async( + contexts, + workers=workers, + on_sample_done=on_sample_done, + cancel_token=cancel_token, + ) + ) + + # ------------------------------------------------------------------ + # run_async() — async entry point + # ------------------------------------------------------------------ + + async def run_async( + self, + contexts: Iterable[StepContext], + workers: int = 1, + on_sample_done: Callable[[SampleResult], None] | None = None, + cancel_token: CancellationToken | None = None, + ) -> list[SampleResult]: + """Async entry point; use ``await pipe.run_async(contexts)`` from + coroutine contexts (e.g. inside browser-use tasks). + + Args: + contexts: Input contexts to process. + workers: Maximum number of contexts processed concurrently. + on_sample_done: Optional callback invoked after each sample + completes its foreground steps (or fails). Receives the + ``SampleResult``. Must not block the event loop. + cancel_token: Optional cancellation signal. Checked before + each step and each new sample. + """ + boundary_idx = self._find_boundary_index() + if boundary_idx is None: + foreground_steps = self._steps + background_steps: list = [] + else: + foreground_steps = self._steps[:boundary_idx] + background_steps = self._steps[boundary_idx:] + + sem = asyncio.Semaphore(workers) + + async def process_one(ctx: StepContext) -> SampleResult: + async with sem: + result = SampleResult( + sample=ctx.sample, output=None, error=None, failed_at=None + ) + last_step_name: str | None = None + try: + for step in foreground_steps: + step_name = type(step).__name__ + + # Cancel check — before each step + if cancel_token is not None and cancel_token.is_cancelled: + result.error = PipelineCancelled( + f"Cancelled before {step_name}" + ) + result.failed_at = step_name + if on_sample_done is not None: + on_sample_done(result) + return result + + last_step_name = step_name + self._fire_before(step_name, ctx) + + if asyncio.iscoroutinefunction(step.__call__): + ctx = await step(ctx) + elif hasattr(step, "__call_async__"): + ctx = await step.__call_async__(ctx) + else: + ctx = await asyncio.to_thread(step, ctx) + + self._fire_after(step_name, ctx) + except Exception as exc: + result.error = exc + result.failed_at = last_step_name + if on_sample_done is not None: + on_sample_done(result) + return result + + if background_steps: + # Fire and forget — result updated by background thread + self._submit_background(ctx, background_steps, result) + else: + result.output = ctx + + if on_sample_done is not None: + on_sample_done(result) + return result + + # Set the contextvar so code inside steps (e.g. LLM clients) can + # read the cancel token without explicit parameter passing. + _reset = cancel_token_var.set(cancel_token) + try: + return list(await asyncio.gather(*[process_one(c) for c in contexts])) + finally: + cancel_token_var.reset(_reset) diff --git a/pipeline/protocol.py b/pipeline/protocol.py new file mode 100644 index 0000000000000000000000000000000000000000..e84fd5f95ea2f6b2ab43044daac6e7cdd2fc3e48 --- /dev/null +++ b/pipeline/protocol.py @@ -0,0 +1,71 @@ +"""Structural protocol and result type for the pipeline engine.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Protocol, TypeVar, runtime_checkable + +from .context import StepContext + +Ctx = TypeVar("Ctx", bound=StepContext) + + +@runtime_checkable +class PipelineHook(Protocol): + """Observation-only hook fired around each foreground step. + + Hooks observe execution — they do **not** transform data. Both methods + return ``None``; context flow stays exclusively in the step chain via + ``requires``/``provides``. + + Hooks must not block the event loop. Heavy work (HTTP, disk) should be + dispatched to a background task or queue. + + If a hook raises, the pipeline logs the error and continues. A broken + hook must never kill the pipeline. + """ + + def before_step(self, step_name: str, ctx: StepContext) -> None: ... + def after_step(self, step_name: str, ctx: StepContext) -> None: ... + + +@runtime_checkable +class StepProtocol(Protocol[Ctx]): + """Structural protocol that every step (and Pipeline/Branch) must satisfy. + + Generic over the context type — use ``StepProtocol[ACEStepContext]`` to + type-check steps that accept a specific ``StepContext`` subclass. + + ``@runtime_checkable`` lets the pipeline validator use + ``isinstance(step, StepProtocol)`` at construction time to give a clear + error if a step is missing required attributes. + """ + + requires: frozenset[str] + provides: frozenset[str] + + def __call__(self, ctx: Ctx) -> Ctx: ... + + +@dataclass +class SampleResult: + """Outcome for one sample after the pipeline has run. + + Every sample produces exactly one ``SampleResult`` — nothing is dropped + silently. After ``run()`` returns, inspect ``error`` / ``failed_at`` to + detect failures; ``output`` is ``None`` whenever a step raised. + + For background steps (after ``async_boundary``), ``output`` / ``error`` + may still be ``None`` when ``run()`` returns. Call + ``pipeline.wait_for_background()`` to block until all background work + completes and results are finalised. + + When a ``Branch`` step fails, ``failed_at == "Branch"`` and ``cause`` + holds the inner exception from the failing branch. + """ + + sample: Any + output: StepContext | None + error: Exception | None + failed_at: str | None + cause: Exception | None = None diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..b4a5726390c898710743cba0d3dc24758512260b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,209 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "ace-framework" +version = "0.12.0" +description = "Build self-improving AI agents that learn from experience" +readme = "README.md" +requires-python = ">=3.12" +license = {text = "Apache-2.0"} +authors = [ + {name = "Kayba.ai", email = "hello@kayba.ai"}, +] +maintainers = [ + {name = "Kayba.ai", email = "hello@kayba.ai"}, +] +keywords = [ + "ai", + "llm", + "agents", + "machine-learning", + "self-improvement", + "context-engineering", + "ace", + "openai", + "anthropic", + "claude", + "gpt", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +dependencies = [ + "click>=8.1.0", + "litellm>=1.83.0", + "pydantic>=2.0.0", + "pydantic-ai-slim[litellm]>=0.0.36", + "rank-bm25>=0.2.2", + "tau2", + "tenacity>=9.1.4", +] + +[project.optional-dependencies] +claude-sdk = [ + "anthropic>=0.76.0", +] +claude-code = [ + "python-dotenv>=1.0.0", + "tenacity>=8.0.0", +] +instructor = [ + "instructor>=1.0.0", +] +deduplication = [ + "numpy>=1.24.0", + "sentence-transformers>=2.2.0", +] +browser-use = [ + "browser-use>=0.9.0", +] +logfire = [ + "logfire[pydantic-ai]>=3.0.0", +] +tracing = [ + "kayba-tracing>=0.9.4", +] +bedrock = [ + "boto3>=1.42.50", +] +langchain = [ + "langchain-openai>=0.3.35", + "langchain-anthropic>=0.3.0", + "langchain-litellm>=0.2.0", + "langgraph>=0.2.0", +] +transformers = [ + "transformers>=4.30.0", + "torch>=2.0.0", + "accelerate>=0.20.0", +] +all = [ + "python-dotenv>=1.0.0", + "tenacity>=8.0.0", + "instructor>=1.0.0", + "boto3>=1.42.50", + "rank-bm25>=0.2.2", + "numpy>=1.24.0", + "sentence-transformers>=2.2.0", + "browser-use>=0.9.0", + "langchain-openai>=0.3.35", + "langchain-anthropic>=0.3.0", + "langchain-litellm>=0.2.0", + "langgraph>=0.2.0", + "transformers>=4.30.0", + "torch>=2.0.0", + "accelerate>=0.20.0", + "kayba-tracing>=0.9.4", + "requests>=2.31.0", +] +cloud = [ + "requests>=2.31.0", + "questionary>=2.0.0", +] +mcp = [ + "mcp>=1.22.0", + "pydantic-settings>=2.0.0", + "tenacity>=8.0.0", +] + +[project.scripts] +ace = "ace.cli.setup:main" +kayba = "ace.cli:main" +ace-mcp = "ace.integrations.mcp.server:main" + +[project.urls] +Homepage = "https://kayba.ai" +Documentation = "https://github.com/Kayba-ai/agentic-context-engine#readme" +Repository = "https://github.com/Kayba-ai/agentic-context-engine" +Issues = "https://github.com/Kayba-ai/agentic-context-engine/issues" + +[dependency-groups] +dev = [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "pytest-cov>=4.0.0", + "black>=23.0.0", + "mypy>=1.0.0", + "pre-commit>=3.0.0", + "git-changelog>=2.5.0", + "boto3>=1.42.50", + "types-requests>=2.32.4.20260107", + "nest-asyncio>=1.6.0", +] +tau-bench = [ + "tau2 @ git+https://github.com/sierra-research/tau2-bench.git@dev/tau3", +] +demos = [ + "browser-use>=0.9.0", + "rich>=13.0.0", + "datasets>=2.0.0", + "pyyaml>=6.0.0", + "pandas>=2.0.0", + "openpyxl>=3.0.0", + "playwright>=1.40.0", +] + +[tool.setuptools.packages.find] +include = ["ace", "ace.*", "pipeline", "pipeline.*"] + +[tool.setuptools.package-data] +ace = ["py.typed", "**/*.md"] + +[tool.black] +line-length = 88 +target-version = ['py312'] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.venv + | build + | dist +)/ +''' + +[tool.mypy] +python_version = "3.12" +warn_return_any = false +warn_unused_configs = true +disallow_untyped_defs = false +ignore_missing_imports = true +files = ["ace"] +exclude = [ + "^tests/", + "^examples/", + "^scripts/", + "^benchmarks/", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "-v", + "--strict-markers", + "--tb=short", + "-m", + "not integration and not requires_api", +] +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "integration: marks tests as integration tests", + "unit: marks tests as unit tests", + "requires_api: marks tests that need live API credentials", +] + +[tool.uv.sources] +tau2 = { git = "https://github.com/sierra-research/tau2-bench.git", branch = "dev/tau3" } +kayba-tracing = { path = "sdk/python", editable = true } diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000000000000000000000000000000000000..907b1e3ccc9bbb7977faf3ab05ee952fce095a46 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,24 @@ +# Benchmark Scripts + +Scripts for running ACE benchmarks and analyzing results. + +## Scripts + +- `run_benchmark.py` - CLI to run ACE benchmarks with train/test splits +- `analyze_ace_results.py` - Analyze benchmark results +- `explain_ace_performance.py` - Generate explanations for ACE performance patterns + +## Usage + +```bash +# List available benchmarks +uv run python scripts/run_benchmark.py list + +# Run ACE evaluation +uv run python scripts/run_benchmark.py simple_qa --limit 50 + +# Compare baseline vs ACE +uv run python scripts/run_benchmark.py simple_qa --limit 50 --compare +``` + +See [benchmarks/README.md](../benchmarks/README.md) for full documentation. diff --git a/scripts/clean_skillbook.py b/scripts/clean_skillbook.py new file mode 100644 index 0000000000000000000000000000000000000000..adbf87c2b48905dcc71f637a0b2b4362df0fedfe --- /dev/null +++ b/scripts/clean_skillbook.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Clean v3 Haiku skillbook by removing 29 problematic skills. + +Removes skills that: +- Reference hallucinated tools (get_available_flights, rebook_passenger, etc.) +- Encode fabricated policies (loyalty points, weather monitoring, insurance) +- Are harmful/counterproductive (false fraud assumptions, blocking alternatives) +- Were never validated as helpful (0 helpful, only neutral counts) + +Also edits skill 00024 to remove reference to nonexistent cancellation_reason param. +""" + +import json +from pathlib import Path + +INPUT = Path( + "tau_benchmark_results/tau_airline_claude-haiku-4-5-20251001_ace_20260209_154102_skillbook.json" +) +OUTPUT = Path("tau_benchmark_results/cleaned_haiku_skillbook.json") + +# 29 skills to remove +REMOVE_IDS = { + # Hallucinated tool names (16) + "flight_rebooking-00017", # get_available_flights + "flight_rebooking-00022", # rebook_passenger + "reservation_management-00026", # cancellation_reason param + "reservation_management-00035", # get_reservations_by_user_id + "flight_modification-00047", # change_reservation + "reservation_management-00051", # update_reservation (combined) + "balance_management-00061", # get_gift_card_balance + "balance_management-00062", # get_certificate_balance + "balance_management-00063", # depends on 00061 + "balance_management-00064", # depends on 00062 + "flight_modification-00072", # modify_reservation + "flight_modification-00074", # modify_reservation (verify date) + "flight_modification-00075", # modify_reservation (confirm itinerary) + "reservation_management-00087", # get_reservations_by_user_id (bulk) + "reservation_management-00094", # get_current_time + "reservation_management-00095", # depends on get_current_time + # Hallucinated policies/systems (6) + "loyalty_compensation-00019", # 5,000 loyalty points system + "complaint_resolution-00085", # weather monitoring, 30-min buffer + "reservation_management-00105", # insurance claims eligibility + "reservation_management-00106", # 5-7 business days insurance refund + "reservation_management-00107", # medical documentation requirement + "reservation_management-00108", # insurance vs airline distinction + # Harmful/counterproductive (2) + "fraud_prevention-00016", # assumes fraud on missing details + "complaint_resolution-00104", # blocks offering alternatives + # Never validated (5) + "reservation_management-00027", # helpful=0, neutral=1 + "flight_rebooking-00028", # helpful=0, neutral=1 + "complaint_resolution-00029", # helpful=0, neutral=1 + "reservation_management-00030", # helpful=0, neutral=1 + "reservation_management-00096", # all zeros +} + + +def main(): + data = json.loads(INPUT.read_text()) + + before = len(data["skills"]) + assert before == 108, f"Expected 108 skills, got {before}" + + # Remove skills + for sid in REMOVE_IDS: + removed = data["skills"].pop(sid, None) + assert removed is not None, f"Skill {sid} not found" + + # Edit skill 00024: remove "and cancellation_reason parameter" + s24 = data["skills"]["reservation_management-00024"] + old = s24["content"] + s24["content"] = old.replace(" and cancellation_reason parameter", "") + assert "cancellation_reason" not in s24["content"], "Edit failed" + + # Rebuild sections index + new_sections = {} + for sid, skill in data["skills"].items(): + sec = skill["section"] + new_sections.setdefault(sec, []).append(sid) + data["sections"] = new_sections + + after = len(data["skills"]) + assert after == 79, f"Expected 79 skills, got {after}" + + # Verify empty sections are gone + assert ( + "balance_management" not in data["sections"] + ), "balance_management should be removed" + assert ( + "loyalty_compensation" not in data["sections"] + ), "loyalty_compensation should be removed" + + # Verify no hallucinated tool references remain in any skill + hallucinated = [ + "get_gift_card_balance", + "get_certificate_balance", + "get_available_flights", + "rebook_passenger", + "change_reservation", + "modify_reservation", + "get_current_time", + "get_reservations_by_user_id", + ] + for sid, skill in data["skills"].items(): + for h in hallucinated: + assert h not in skill["content"], f"Skill {sid} still references {h}" + + OUTPUT.write_text(json.dumps(data, indent=2) + "\n") + print(f"Cleaned: {before} -> {after} skills ({before - after} removed)") + print(f"Sections: {sorted(data['sections'].keys())}") + print(f"Saved to {OUTPUT}") + + +if __name__ == "__main__": + main() diff --git a/scripts/clean_skillbook_v4.py b/scripts/clean_skillbook_v4.py new file mode 100644 index 0000000000000000000000000000000000000000..7b0c5ae672e7709e5a421883dee16bf28b6fcf8c --- /dev/null +++ b/scripts/clean_skillbook_v4.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Clean v4 Haiku skillbook: remove harmful/duplicate skills, merge near-duplicates, update incomplete ones. + +Changes from v3 (79 skills): +- Remove 2 harmful skills contradicting airline policy (00001, 00002) +- Remove 5 duplicates (00068, 00080, 00078, 00020, 00101) +- Merge 00091+00092 into single name-change verification skill +- Update 00041 with urgency from deleted 00101 +- Update 00009 with full baggage allowance table (3 tiers × 3 cabins) +- Update 00007 with cancellation eligibility rules + +Expected result: 72 skills (79 - 7 removed) +""" + +import argparse +import json +from pathlib import Path + +DEFAULT_INPUT = Path("tau_benchmark_results/cleaned_haiku_skillbook.json") +DEFAULT_OUTPUT = Path("tau_benchmark_results/cleaned_v4_haiku_skillbook.json") + +# Skills to remove entirely +REMOVE_IDS = { + # Harmful: contradicts airline policy + "reservation_management-00001", # "Accept contextual justifications for policy exceptions" + "reservation_management-00002", # "Process cancellations without additional verification" + # Duplicate of 00004+00011: "request IDs upfront" + "reservation_management-00068", + "task_management-00080", + # Duplicate of 00033: "modification sequencing" + "flight_modification-00078", + # Duplicate of 00069: "identity verification" + "fraud_prevention-00020", + # Merged into 00041: "task transition timing" + "task_management-00101", +} + +# Merge 00091 + 00092 → keep 00092 with merged content, remove 00091 +MERGE_REMOVE = "passenger_management-00091" + + +def main(): + parser = argparse.ArgumentParser(description="Clean skillbook v4") + parser.add_argument("--input", type=Path, default=DEFAULT_INPUT) + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + args = parser.parse_args() + + data = json.loads(args.input.read_text()) + before = len(data["skills"]) + assert before == 79, f"Expected 79 skills, got {before}" + + # 1. Remove harmful and duplicate skills + for sid in REMOVE_IDS: + removed = data["skills"].pop(sid, None) + assert removed is not None, f"Skill {sid} not found" + + # 2. Merge 00091 into 00092 + assert MERGE_REMOVE in data["skills"], f"{MERGE_REMOVE} not found" + data["skills"].pop(MERGE_REMOVE) + s92 = data["skills"]["passenger_management-00092"] + s92["content"] = ( + "Verify old and new passenger names explicitly before executing name change: " + "confirm current name on reservation matches, then confirm new name with user" + ) + s92["helpful"] = 2 # sum of both + + # 3. Update 00041: add urgency from 00101 + s41 = data["skills"]["task_management-00041"] + s41["content"] = ( + "Transition explicitly between tasks with confirmation before moving to the next task; " + "move from information gathering to action execution within the first few conversation steps" + ) + + # 4. Update 00009: full baggage allowance table (3 tiers × 3 cabins) + s09 = data["skills"]["baggage_policy-00009"] + s09["content"] = ( + "Baggage allowance (checked bags per person): " + "Regular — economy:1, business:2, first:3; " + "Silver — economy:2, business:3, first:3; " + "Gold — economy:3, business:3, first:3. " + "All tiers get 1 free carry-on and 1 personal item." + ) + + # 5. Update 00007: add cancellation eligibility rules + s07 = data["skills"]["reservation_management-00007"] + s07["content"] = ( + "Follow all required flight_cancellation workflow steps sequentially: " + "identify_reservation, check_cancellation_eligibility, confirm_cancellation_policy, " + "process_cancellation, provide_confirmation. " + "Cancellation is only allowed if: booked within last 24 hours, OR flight was cancelled by airline, " + "OR cabin is business/first, OR reservation has travel insurance. " + "The API does NOT enforce eligibility — the agent must check these conditions." + ) + + # 6. Rebuild sections index + new_sections: dict[str, list[str]] = {} + for sid, skill in data["skills"].items(): + sec = skill["section"] + new_sections.setdefault(sec, []).append(sid) + data["sections"] = new_sections + + after = len(data["skills"]) + expected = before - len(REMOVE_IDS) - 1 # -1 for merge + assert after == expected, f"Expected {expected} skills, got {after}" + + args.output.write_text(json.dumps(data, indent=2) + "\n") + print(f"Cleaned: {before} -> {after} skills ({before - after} removed/merged)") + print(f"Sections: {sorted(data['sections'].keys())}") + print(f"Updated skills: 00007, 00009, 00041, 00092") + print(f"Saved to {args.output}") + + +if __name__ == "__main__": + main() diff --git a/scripts/merge_tau_results.py b/scripts/merge_tau_results.py new file mode 100644 index 0000000000000000000000000000000000000000..08c713116296c11e6a5c66d59ad64f0eba77858e --- /dev/null +++ b/scripts/merge_tau_results.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Merge partial TAU-bench re-run results into an existing detailed JSON. + +Replaces failed/dead task entries in the original with fresh results from +a partial re-run, then recomputes pass^k metrics over all tasks. + +Usage: + uv run python scripts/merge_tau_results.py \ + --original tau_benchmark_results/tau_airline_..._223116_detailed.json \ + --patch tau_benchmark_results/tau_airline_..._HHMMSS_detailed.json \ + --output tau_benchmark_results/tau_airline_..._merged +""" + +from __future__ import annotations + +import argparse +import json +from math import comb +from pathlib import Path + + +def pass_hat_k(n: int, s: int, k: int) -> float: + """Compute pass^k = C(s, k) / C(n, k). + + Args: + n: Total number of trials. + s: Number of successes. + k: k value for pass^k. + + Returns: + Combinatorial probability that all k chosen trials succeed. + """ + if k > n or k > s: + return 0.0 + return comb(s, k) / comb(n, k) + + +def merge(original: dict, patch: dict) -> dict: + """Replace task entries in original with matching entries from patch. + + Matching is by task_id. Only tasks present in the patch are replaced. + """ + patch_by_id = {r["task_id"]: r for r in patch["results"]} + + merged_results = [] + replaced = [] + for task_result in original["results"]: + tid = task_result["task_id"] + if tid in patch_by_id: + merged_results.append(patch_by_id[tid]) + replaced.append(tid) + else: + merged_results.append(task_result) + + print(f"Replaced {len(replaced)} tasks: {replaced}") + + # Recompute pass^k metrics + k = original["k"] + n_tasks = len(merged_results) + pass_sums = {str(j): 0.0 for j in range(1, k + 1)} + + for task_result in merged_results: + trials = task_result["trials"] + n_trials = len(trials) + n_successes = sum(1 for t in trials if t.get("success", False)) + + # Recompute per-task pass_k_values + task_pass_k = {} + for j in range(1, k + 1): + task_pass_k[str(j)] = pass_hat_k(n_trials, n_successes, j) + task_result["pass_k_values"] = task_pass_k + task_result["passed_all"] = all(t.get("success", False) for t in trials) + + for j in range(1, k + 1): + pass_sums[str(j)] += task_pass_k[str(j)] + + metrics = {} + for j in range(1, k + 1): + metrics[f"pass_{j}"] = pass_sums[str(j)] / n_tasks if n_tasks > 0 else 0.0 + + merged = { + "tasks_evaluated": n_tasks, + "k": k, + "pass_sums": pass_sums, + "metrics": metrics, + "results": merged_results, + } + + return merged + + +def main() -> None: + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "--original", required=True, help="Path to the original detailed JSON" + ) + parser.add_argument( + "--patch", required=True, help="Path to the partial re-run detailed JSON" + ) + parser.add_argument( + "--output", + required=True, + help="Output path prefix (will create _detailed.json and _summary.json)", + ) + args = parser.parse_args() + + original = json.loads(Path(args.original).read_text()) + patch = json.loads(Path(args.patch).read_text()) + + merged = merge(original, patch) + + # Validate: no tasks with 0 steps remaining + zero_step_tasks = [ + r["task_id"] + for r in merged["results"] + if all(t.get("steps", 0) == 0 for t in r["trials"]) + ] + if zero_step_tasks: + print( + f"WARNING: {len(zero_step_tasks)} tasks still have all-zero steps: {zero_step_tasks}" + ) + + # Save detailed + detailed_path = Path(f"{args.output}_detailed.json") + detailed_path.write_text(json.dumps(merged, indent=2, default=str)) + print(f"Saved detailed: {detailed_path}") + + # Save summary + summary = { + "tasks_evaluated": merged["tasks_evaluated"], + "k": merged["k"], + "pass_sums": merged["pass_sums"], + "metrics": merged["metrics"], + } + summary_path = Path(f"{args.output}_summary.json") + summary_path.write_text(json.dumps(summary, indent=2)) + print(f"Saved summary: {summary_path}") + + # Print metrics + print(f"\nMerged pass^k metrics ({merged['tasks_evaluated']} tasks):") + for j in range(1, merged["k"] + 1): + print(f" pass^{j}: {merged['metrics'][f'pass_{j}']:.2%}") + + +if __name__ == "__main__": + main() diff --git a/sdk/openclaw/.gitignore b/sdk/openclaw/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b6e73145747d8b4f8a49a11d991146721185058f --- /dev/null +++ b/sdk/openclaw/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/sdk/openclaw/README.md b/sdk/openclaw/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6610727c590dc3457d8c990b85be0537cd99286a --- /dev/null +++ b/sdk/openclaw/README.md @@ -0,0 +1,78 @@ +# @kayba_ai/openclaw-tracing + +OpenClaw plugin that captures every agent turn — user message, full LLM prompt and response (including thinking blocks), tool calls, final reply — and ships it as a structured Kayba trace. + +Pairs with [@kayba_ai/tracing](../typescript/). Whatever signals a tool-level plugin (e.g. a trader plugin) is already emitting via `kayba.trace()` will land in the same Kayba folder and can be cross-referenced with these turn-level traces by `runId`. + +## What you get + +- One trace per agent turn, named `agent.turn` +- Child span `llm.call` capturing the full LLM input/output for the turn +- Span attributes: `openclaw.runId`, `openclaw.sessionId`, `openclaw.agentId`, `openclaw.channelId`, `openclaw.senderId` +- Token usage, stop reason, and the assistant's full content blocks (thinking + text + tool calls) on the `llm.call` span output +- Compatible with ACE — the captured shape contains everything `OpenClawToTraceStep` needs + +## Install + +```bash +openclaw plugins install @kayba_ai/openclaw-tracing +``` + +Then add the config block to `~/.openclaw/openclaw.json`. The `hooks.allowConversationAccess` flag is required because conversation-content hooks are gated for non-bundled plugins: + +```json +{ + "plugins": { + "allow": ["kayba-tracing"], + "entries": { + "kayba-tracing": { + "enabled": true, + "hooks": { + "allowConversationAccess": true + }, + "config": { + "apiKey": "kayba_ak_...", + "folder": "main" + } + } + } + } +} +``` + +Restart the gateway. From this point on, every agent turn produces one trace at https://use.kayba.ai/traces/v2. + +## Config + +| Key | Type | Default | Notes | +|---|---|---|---| +| `apiKey` | string | (required) | From https://use.kayba.ai/settings/api-keys | +| `baseUrl` | string | `https://use.kayba.ai` | For self-hosted Kayba | +| `folder` | string | `null` | Dashboard folder for grouping | +| `captureSystemPrompt` | boolean | `true` | Include the (potentially large) system prompt on each `llm.call` span | +| `captureHistory` | boolean | `true` | Include `historyMessages` on each `llm.call` span (large for long sessions) | +| `maxAttributeBytes` | integer | `65536` | Per-attribute truncation cap | + +## How it works + +The plugin subscribes to OpenClaw's typed hooks via `api.on(...)`: + +| Hook | Used for | +|---|---| +| `message_received` | open turn, capture user message + sender | +| `before_agent_start` | bind `runId` | +| `llm_input` | capture prompt, system prompt, history, model, provider | +| `llm_output` | capture response, assistant content blocks, usage | +| `agent_end` | finalize and emit the trace | + +Race protection: `agent_end` and `llm_output` can fire in either order. The plugin defers finalize by `~250ms` after `agent_end` to absorb a late `llm_output`, and falls through immediately if both have already arrived. + +Stale-turn safety: turn state older than 5 minutes is evicted. A turn that never reaches `agent_end` (crashed mid-loop) is dropped silently — the trader plugin's per-tool spans still land independently. + +## Why a separate plugin and not part of `@kayba_ai/tracing` + +OpenClaw's plugin loader requires a manifest (`openclaw.plugin.json` + `package.json` with `openclaw.extensions[]`) and must be installed via `openclaw plugins install`. Bundling that into the generic SDK would force the OpenClaw runtime as a dependency on every SDK consumer, including the trader plugin which uses the SDK directly without the OpenClaw plugin contract. + +## License + +MIT diff --git a/sdk/openclaw/build.mjs b/sdk/openclaw/build.mjs new file mode 100644 index 0000000000000000000000000000000000000000..fd82ac27b83b7f9c2e18beddc54a8d8cf51918fa --- /dev/null +++ b/sdk/openclaw/build.mjs @@ -0,0 +1,16 @@ +import { build } from "esbuild"; +import { rmSync } from "fs"; + +rmSync("./dist", { recursive: true, force: true }); + +await build({ + entryPoints: ["./src/index.ts"], + outdir: "./dist", + format: "esm", + platform: "node", + target: "node22", + bundle: true, + external: ["@kayba_ai/tracing", "mlflow-tracing", "openclaw/plugin-sdk"], +}); + +console.log("Build complete → dist/"); diff --git a/sdk/openclaw/openclaw.plugin.json b/sdk/openclaw/openclaw.plugin.json new file mode 100644 index 0000000000000000000000000000000000000000..583766cd5f51711c0f460e438812746867f82495 --- /dev/null +++ b/sdk/openclaw/openclaw.plugin.json @@ -0,0 +1,45 @@ +{ + "id": "kayba-tracing", + "name": "Kayba Tracing", + "description": "Captures every OpenClaw agent turn (user message, full LLM prompt + response with thinking, tool calls, final reply) and ships it as a structured Kayba trace. Powers /traces/v2 dashboards and ACE skill extraction.", + "enabledByDefault": true, + "configSchema": { + "type": "object", + "additionalProperties": false, + "required": ["apiKey"], + "properties": { + "apiKey": { + "type": "string", + "description": "Kayba API key (kayba_ak_...). Get one at https://use.kayba.ai/settings/api-keys." + }, + "baseUrl": { + "type": "string", + "description": "Kayba ingest base URL. Defaults to https://use.kayba.ai. Override for self-hosted deployments.", + "default": "https://use.kayba.ai" + }, + "folder": { + "type": "string", + "description": "Folder name in the Kayba dashboard. Traces from this gateway will be grouped under it. Defaults to the openclaw agent id (e.g. 'main').", + "maxLength": 256 + }, + "captureSystemPrompt": { + "type": "boolean", + "description": "Whether to capture the system prompt. Captured once per session (first turn) regardless — disabling skips it entirely.", + "default": true + }, + "captureHistory": { + "type": "string", + "enum": ["delta", "full", "none"], + "description": "How much of the LLM input historyMessages to include on each llm.call span. 'delta' (default) ships only messages added since the prior turn for this session — keeps traces small while letting the full conversation be reconstructed by joining traces ordered by time within sessionId. 'full' ships the entire history every turn (large). 'none' drops it.", + "default": "delta" + }, + "maxAttributeBytes": { + "type": "integer", + "description": "Truncate any single span attribute value above this size (in bytes). Prevents blowing past kayba's per-attribute limits with huge prompts.", + "default": 65536, + "minimum": 1024, + "maximum": 1048576 + } + } + } +} diff --git a/sdk/openclaw/package-lock.json b/sdk/openclaw/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..1589828e51260fd3c9c9e76df56de5380680e9ec --- /dev/null +++ b/sdk/openclaw/package-lock.json @@ -0,0 +1,1858 @@ +{ + "name": "@kayba_ai/openclaw-tracing", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@kayba_ai/openclaw-tracing", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@kayba_ai/tracing": "^0.10.0" + }, + "devDependencies": { + "esbuild": "^0.24.0", + "typescript": "^5.4.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "mlflow-tracing": "^0.1.3", + "openclaw": ">=2026.4.0" + }, + "peerDependenciesMeta": { + "openclaw": { + "optional": true + } + } + }, + "node_modules/@databricks/sdk-experimental": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@databricks/sdk-experimental/-/sdk-experimental-0.15.0.tgz", + "integrity": "sha512-HkoMiF7dNDt6WRW0xhi7oPlBJQfxJ9suJhEZRFt08VwLMaWcw2PiF8monfHlkD4lkufEYV6CTxi5njQkciqiHA==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.5.0", + "ini": "^6.0.0", + "reflect-metadata": "^0.2.2", + "semver": "^7.7.3" + }, + "engines": { + "node": ">=22.0", + "npm": ">=10.0.0" + } + }, + "node_modules/@databricks/sdk-experimental/node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@kayba_ai/tracing": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@kayba_ai/tracing/-/tracing-0.10.0.tgz", + "integrity": "sha512-HEszGhLI6zhX0jQpsj2Fmk+dFRZocvNkjfdp0TClnuWrhb8HbL4jNWmDzlEHvEpD7vfXDpI5zIiI0Ki7mBAg/A==", + "license": "MIT", + "dependencies": { + "mlflow-tracing": "^0.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.205.0.tgz", + "integrity": "sha512-wBlPk1nFB37Hsm+3Qy73yQSobVn28F4isnWIBvKpd5IUH/eat8bwcL02H9yzmHyyPmukeccSl2mbN5sDQZYnPg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.1.0.tgz", + "integrity": "sha512-zOyetmZppnwTyPrt4S7jMfXiSX9yyfF0hxlA8B5oo2TtKl+/RGCy7fi4DrBfIf3lCPrkKsRBWZZD7RFojK7FDg==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.1.0.tgz", + "integrity": "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.205.0.tgz", + "integrity": "sha512-jQlw7OHbqZ8zPt+pOrW2KGN7T55P50e3NXBMr4ckPOF+DWDwSy4W7mkG09GpYWlQAQ5C9BXg5gfUlv5ldTgWsw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/sdk-logs": "0.205.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.205.0.tgz", + "integrity": "sha512-5JteMyVWiro4ghF0tHQjfE6OJcF7UBUcoEqX3UIQ5jutKP1H+fxFdyhqjjpmeHMFxzOHaYuLlNR1Bn7FOjGyJg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/sdk-logs": "0.205.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.205.0.tgz", + "integrity": "sha512-q3VS9wS+lpZ01txKxiDGBtBpTNge3YhbVEFDgem9ZQR9eI3EZ68+9tVZH9zJcSxI37nZPJ6lEEZO58yEjYZsVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-logs": "0.205.0", + "@opentelemetry/sdk-trace-base": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.205.0.tgz", + "integrity": "sha512-1Vxlo4lUwqSKYX+phFkXHKYR3DolFHxCku6lVMP1H8sVE3oj4wwmwxMzDsJ7zF+sXd8M0FCr+ckK4SnNNKkV+w==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.205.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-metrics": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.205.0.tgz", + "integrity": "sha512-fFxNQ/HbbpLmh1pgU6HUVbFD1kNIjrkoluoKJkh88+gnmpFD92kMQ8WFNjPnSbjg2mNVnEkeKXgCYEowNW+p1w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-metrics": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.205.0.tgz", + "integrity": "sha512-qIbNnedw9QfFjwpx4NQvdgjK3j3R2kWH/2T+7WXAm1IfMFe9fwatYxE61i7li4CIJKf8HgUC3GS8Du0C3D+AuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.205.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-metrics": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.205.0.tgz", + "integrity": "sha512-xsot/Qm9VLDTag4GEwAunD1XR1U8eBHTLAgO7IZNo2JuD/c/vL7xmDP7mQIUr6Lk3gtj/yGGIR2h3vhTeVzv4w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-metrics": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.205.0.tgz", + "integrity": "sha512-ZBksUk84CcQOuDJB65yu5A4PORkC4qEsskNwCrPZxDLeWjPOFZNSWt0E0jQxKCY8PskLhjNXJYo12YaqsYvGFA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.205.0.tgz", + "integrity": "sha512-vr2bwwPCSc9u7rbKc74jR+DXFvyMFQo9o5zs+H/fgbK672Whw/1izUKVf+xfWOdJOvuwTnfWxy+VAY+4TSo74Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.205.0.tgz", + "integrity": "sha512-bGtFzqiENO2GpJk988mOBMe0MfeNpTQjbLm/LBijas6VRyEDQarUzdBHpFlu89A25k1+BCntdWGsWTa9Ai4FyA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.1.0.tgz", + "integrity": "sha512-0mEI0VDZrrX9t5RE1FhAyGz+jAGt96HSuXu73leswtY3L5YZD11gtcpARY2KAx/s6Z2+rj5Mhj566JsI2C7mfA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.205.0.tgz", + "integrity": "sha512-cgvm7tvQdu9Qo7VurJP84wJ7ZV9F6WqDDGZpUc6rUEXwjV7/bXWs0kaYp9v+1Vh1+3TZCD3i6j/lUBcPhu8NhA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.205.0.tgz", + "integrity": "sha512-2MN0C1IiKyo34M6NZzD6P9Nv9Dfuz3OJ3rkZwzFmF6xzjDfqqCTatc9v1EpNfaP55iDOCLHFyYNCgs61FFgtUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-transformer": "0.205.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.205.0.tgz", + "integrity": "sha512-AeuLfrciGYffqsp4EUTdYYc6Ee2BQS+hr08mHZk1C524SFWx0WnfcTnV0NFXbVURUNU6DZu1DhS89zRRrcx/hg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.205.0.tgz", + "integrity": "sha512-KmObgqPtk9k/XTlWPJHdMbGCylRAmMJNXIRh6VYJmvlRDMfe+DonH41G7eenG8t4FXn3fxOGh14o/WiMRR6vPg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-logs": "0.205.0", + "@opentelemetry/sdk-metrics": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.1.0.tgz", + "integrity": "sha512-yOdHmFseIChYanddMMz0mJIFQHyjwbNhoxc65fEAA8yanxcBPwoFDoh1+WBUWAO/Z0NRgk+k87d+aFIzAZhcBw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.1.0.tgz", + "integrity": "sha512-QYo7vLyMjrBCUTpwQBF/e+rvP7oGskrSELGxhSvLj5gpM0az9oJnu/0O4l2Nm7LEhAff80ntRYKkAcSwVgvSVQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.1.0.tgz", + "integrity": "sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.205.0.tgz", + "integrity": "sha512-nyqhNQ6eEzPWQU60Nc7+A5LIq8fz3UeIzdEVBQYefB4+msJZ2vuVtRuk9KxPMw1uHoHDtYEwkr2Ct0iG29jU8w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.1.0.tgz", + "integrity": "sha512-J9QX459mzqHLL9Y6FZ4wQPRZG4TOpMCyPOh6mkr/humxE1W2S3Bvf4i75yiMW9uyed2Kf5rxmLhTm/UK8vNkAw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.205.0.tgz", + "integrity": "sha512-Y4Wcs8scj/Wy1u61pX1ggqPXPtCsGaqx/UnFu7BtRQE1zCQR+b0h56K7I0jz7U2bRlPUZIFdnNLtoaJSMNzz2g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.205.0", + "@opentelemetry/exporter-logs-otlp-http": "0.205.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.205.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.205.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.205.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.205.0", + "@opentelemetry/exporter-prometheus": "0.205.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.205.0", + "@opentelemetry/exporter-trace-otlp-http": "0.205.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.205.0", + "@opentelemetry/exporter-zipkin": "2.1.0", + "@opentelemetry/instrumentation": "0.205.0", + "@opentelemetry/propagator-b3": "2.1.0", + "@opentelemetry/propagator-jaeger": "2.1.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-logs": "0.205.0", + "@opentelemetry/sdk-metrics": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0", + "@opentelemetry/sdk-trace-node": "2.1.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.1.0.tgz", + "integrity": "sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.1.0.tgz", + "integrity": "sha512-SvVlBFc/jI96u/mmlKm86n9BbTCbQ35nsPoOohqJX6DXH92K0kTe73zGY5r8xoI1QkjR9PizszVJLzMC966y9Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.1.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/import-in-the-middle": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz", + "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/mlflow-tracing": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/mlflow-tracing/-/mlflow-tracing-0.1.3.tgz", + "integrity": "sha512-Koqkwaid5ubGHuLprBP6J7Su70WddlD11f2vgzgxbFFHYKsAsJatMGvjIck5CkyhT/gMUyBqpA3Lkl+zC3W3uQ==", + "license": "Apache-2.0", + "dependencies": { + "@databricks/sdk-experimental": "0.15.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-node": "^0.205.0", + "bignumber.js": "^9.0.0", + "fast-safe-stringify": "^2.1.1", + "ini": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/sdk/openclaw/package.json b/sdk/openclaw/package.json new file mode 100644 index 0000000000000000000000000000000000000000..1e5c50707734926248f2bbbb1c160d8ab4d38dd1 --- /dev/null +++ b/sdk/openclaw/package.json @@ -0,0 +1,50 @@ +{ + "name": "@kayba_ai/openclaw-tracing", + "version": "0.1.1", + "description": "OpenClaw plugin that captures full agent turns (user message + LLM call + tool calls + reply) and emits them as Kayba traces. Pairs with @kayba_ai/tracing.", + "type": "module", + "main": "./dist/index.js", + "files": [ + "dist/", + "openclaw.plugin.json", + "README.md" + ], + "openclaw": { + "extensions": [ + "./dist/index.js" + ] + }, + "scripts": { + "build": "node build.mjs", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "kayba", + "openclaw", + "tracing", + "mlflow", + "ace", + "agent", + "observability" + ], + "license": "MIT", + "engines": { + "node": ">=22" + }, + "dependencies": { + "@kayba_ai/tracing": "^0.10.0" + }, + "devDependencies": { + "esbuild": "^0.24.0", + "typescript": "^5.4.0" + }, + "peerDependencies": { + "mlflow-tracing": "^0.1.3", + "openclaw": ">=2026.4.0" + }, + "peerDependenciesMeta": { + "openclaw": { + "optional": true + } + } +} diff --git a/sdk/openclaw/src/index.ts b/sdk/openclaw/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7622d6882f7178c4ec300977f708a21454b1e13a --- /dev/null +++ b/sdk/openclaw/src/index.ts @@ -0,0 +1,522 @@ +/** + * @kayba_ai/openclaw-tracing — OpenClaw plugin that captures full agent turns + * (user message, LLM input/output with thinking, tool calls, final reply) and + * emits one structured Kayba trace per turn with real wall-clock timing, + * trace-level sessionId/userId, and folder tagging. + * + * Pairs with `@kayba_ai/tracing`. The trader plugin (or any other tool plugin) + * can keep its existing `kayba.trace()` wrapping for tool-level spans — those + * land in the same kayba folder and can be cross-referenced by `runId`. + */ + +import kayba from "@kayba_ai/tracing"; +import { + startSpan as mlflowStartSpan, + updateCurrentTrace, + SpanStatusCode, + SpanType, +} from "mlflow-tracing"; + +// ── Hook payload shapes (probed against openclaw 2026.4.24) ──────────── + +interface PluginApi { + pluginConfig?: Record; + logger: { info: (m: string) => void; warn: (m: string) => void; error?: (m: string) => void }; + on: (event: string, handler: (event: unknown, ctx: unknown) => unknown) => void; +} + +interface MessageReceivedEvent { + from?: string; + content?: string; + timestamp?: number; + messageId?: string; + senderId?: string; + sessionKey?: string; + metadata?: Record; +} + +interface MessageReceivedCtx { + channelId?: string; + sessionKey?: string; + messageId?: string; + senderId?: string; +} + +interface BeforeAgentStartEvent { + prompt?: string; + runId?: string; +} + +interface BeforeAgentStartCtx { + runId?: string; + agentId?: string; + sessionKey?: string; + sessionId?: string; + channelId?: string; +} + +interface LlmInputEvent { + runId: string; + sessionId: string; + provider: string; + model: string; + systemPrompt?: string; + prompt: string; + historyMessages: unknown[]; + imagesCount: number; +} + +interface LlmOutputEvent { + runId: string; + sessionId: string; + provider: string; + model: string; + resolvedRef?: string; + harnessId?: string; + assistantTexts: string[]; + lastAssistant?: { + role?: string; + content?: Array<{ type: string; text?: string; thinking?: string; name?: string; arguments?: unknown }>; + usage?: unknown; + stopReason?: string; + timestamp?: string | number; + responseId?: string; + }; + usage?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number; total?: number }; +} + +interface AgentEndEvent { + runId?: string; + messages: unknown[]; + success: boolean; + error?: string; + durationMs?: number; +} + +interface ConversationCtx { + runId?: string; + trace?: { traceId?: string; spanId?: string; traceFlags?: string }; + agentId?: string; + sessionKey?: string; + sessionId?: string; + channelId?: string; + trigger?: string; +} + +// ── Per-turn state ───────────────────────────────────────────────────── + +interface PendingTurn { + runId?: string; + sessionId?: string; + sessionKey?: string; + agentId?: string; + channelId?: string; + senderId?: string; + userMessage?: string; + startedAtMs: number; + llmInputAtMs?: number; + llmOutputAtMs?: number; + endedAtMs?: number; + llmIn?: LlmInputEvent; + llmOut?: LlmOutputEvent; + agentEnd?: AgentEndEvent; + agentEndArrived?: boolean; + finalized?: boolean; +} + +const TURN_FINALIZE_DELAY_MS = 250; +const TURN_TIMEOUT_MS = 5 * 60 * 1000; + +// ── Config ───────────────────────────────────────────────────────────── + +interface PluginConfig { + apiKey: string; + baseUrl?: string; + folder?: string; + captureSystemPrompt: boolean; + /** + * "delta" — only messages added since the previous turn for this sessionId (default). + * Traces stay ~5–10 KB regardless of conversation length. + * "full" — full historyMessages array on every turn. Bigger traces, no reconstruction needed. + * "none" — drop history entirely. + */ + captureHistory: "delta" | "full" | "none"; + maxAttributeBytes: number; + userField: "agentId" | "senderId"; +} + +function parseConfig(raw: unknown): PluginConfig { + const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as Record) : {}; + let captureHistory: PluginConfig["captureHistory"] = "delta"; + if (obj.captureHistory === "full" || obj.captureHistory === true) captureHistory = "full"; + else if (obj.captureHistory === "none" || obj.captureHistory === false) captureHistory = "none"; + // Back-compat: systemPrompt should default to "first turn only" — capture once per session. + // We model it by capturing only when captureSystemPrompt is true AND it's the session's first turn. + return { + apiKey: typeof obj.apiKey === "string" ? obj.apiKey : "", + baseUrl: typeof obj.baseUrl === "string" ? obj.baseUrl : undefined, + folder: typeof obj.folder === "string" ? obj.folder : undefined, + captureSystemPrompt: obj.captureSystemPrompt !== false, + captureHistory, + maxAttributeBytes: typeof obj.maxAttributeBytes === "number" ? obj.maxAttributeBytes : 65536, + userField: obj.userField === "senderId" ? "senderId" : "agentId", + }; +} + +// ── Helpers ──────────────────────────────────────────────────────────── + +function truncate(value: unknown, maxBytes: number): unknown { + if (typeof value !== "string") return value; + if (value.length <= maxBytes) return value; + return value.slice(0, maxBytes) + `…[truncated ${value.length - maxBytes} bytes]`; +} + +/** + * Recursively parse JSON-string fields back into structured values. + * + * OpenClaw's `historyMessages` is array, where each string is a JSON + * encoding of `{role, content, ...}`. The `content` field of those decoded + * objects is *itself* a JSON-encoded array of `[{type, text|thinking|...}]`. + * The same nesting shows up on `lastAssistant.content`, `usage`, `cost`, etc. + * + * If we ship those raw, mlflow JSON-stringifies the whole inputs/outputs blob + * one more time on top, producing an unreadable wall of `\\\\\\\"`. Unwrapping + * once before handoff yields clean, single-level JSON in the dashboard. + * + * Heuristic: a string is "wrapped JSON" if it starts with `{` or `[` and + * `JSON.parse` succeeds. We cap recursion depth so a malicious payload can't + * blow the stack. + */ +function unwrapJsonStrings(value: unknown, depth = 0): unknown { + if (depth > 8) return value; + if (typeof value === "string") { + const trimmed = value.trim(); + if (trimmed.length >= 2 && (trimmed[0] === "{" || trimmed[0] === "[")) { + try { + return unwrapJsonStrings(JSON.parse(trimmed), depth + 1); + } catch { + return value; + } + } + return value; + } + if (Array.isArray(value)) return value.map((v) => unwrapJsonStrings(v, depth + 1)); + if (value && typeof value === "object") { + const out: Record = {}; + for (const [k, v] of Object.entries(value as Record)) { + out[k] = unwrapJsonStrings(v, depth + 1); + } + return out; + } + return value; +} + +function safe(fn: () => T, label: string, log: PluginApi["logger"]): T | undefined { + try { + return fn(); + } catch (err) { + log.warn(`[kayba-tracing] ${label} failed: ${err instanceof Error ? err.message : String(err)}`); + return undefined; + } +} + +function resolveUserId(turn: PendingTurn, field: PluginConfig["userField"]): string { + return (field === "senderId" ? turn.senderId : turn.agentId) ?? ""; +} + +// ── Plugin entry ─────────────────────────────────────────────────────── + +export function register(api: PluginApi): void { + const cfg = parseConfig(api.pluginConfig); + if (!cfg.apiKey) { + api.logger.error?.( + `[kayba-tracing] missing required "apiKey" in plugin config; tracing disabled. ` + + `Add plugins.entries.kayba-tracing.config.apiKey in openclaw.json.`, + ); + return; + } + + safe( + () => + kayba.configure({ + apiKey: cfg.apiKey, + baseUrl: cfg.baseUrl, + folder: cfg.folder, + }), + "kayba.configure", + api.logger, + ); + + if (!kayba.isConfigured()) { + api.logger.warn(`[kayba-tracing] kayba SDK did not configure; tracing disabled`); + return; + } + api.logger.info(`[kayba-tracing] configured (folder=${cfg.folder ?? ""}, base=${cfg.baseUrl ?? "default"})`); + + // Per-turn state. Keyed by runId once known; before that, by `${sessionKey}:${messageId}` for + // the brief window between message_received and before_agent_start. + const turnsByRunId = new Map(); + const turnsByMessageKey = new Map(); + + // Per-session bookkeeping for delta-mode history capture and one-shot system prompt. + // Maps sessionId → number of historyMessages we've already shipped on prior turns. + const sessionHistoryCursor = new Map(); + const sessionsWithSystemPromptShipped = new Set(); + + function evictStaleTurns(): void { + const cutoff = Date.now() - TURN_TIMEOUT_MS; + for (const [k, t] of turnsByRunId) if (t.startedAtMs < cutoff) turnsByRunId.delete(k); + for (const [k, t] of turnsByMessageKey) if (t.startedAtMs < cutoff) turnsByMessageKey.delete(k); + } + + // ── message_received: open a turn keyed by sessionKey+messageId ───── + + api.on("message_received", (rawEvent, rawCtx) => { + api.logger.info(`[kayba-tracing] hook: message_received`); + const event = (rawEvent ?? {}) as MessageReceivedEvent; + const ctx = (rawCtx ?? {}) as MessageReceivedCtx; + const sessionKey = event.sessionKey ?? ctx.sessionKey ?? ""; + const messageId = event.messageId ?? ctx.messageId ?? ""; + if (!sessionKey || !messageId) return; + const key = `${sessionKey}:${messageId}`; + turnsByMessageKey.set(key, { + sessionKey, + channelId: ctx.channelId, + senderId: event.senderId ?? ctx.senderId, + userMessage: event.content, + startedAtMs: typeof event.timestamp === "number" ? event.timestamp : Date.now(), + }); + evictStaleTurns(); + }); + + // ── before_agent_start: bind runId, promote to runId-keyed map ────── + + api.on("before_agent_start", (rawEvent, rawCtx) => { + api.logger.info(`[kayba-tracing] hook: before_agent_start`); + const event = (rawEvent ?? {}) as BeforeAgentStartEvent; + const ctx = (rawCtx ?? {}) as BeforeAgentStartCtx; + const runId = event.runId ?? ctx.runId; + if (!runId) return; + let turn: PendingTurn | undefined; + if (ctx.sessionKey) { + for (const [k, t] of turnsByMessageKey) { + if (k.startsWith(ctx.sessionKey + ":") && !t.runId) { + turn = t; + turnsByMessageKey.delete(k); + break; + } + } + } + if (!turn) { + // No prior message_received (e.g. CLI-initiated agent run). + turn = { startedAtMs: Date.now() }; + } + turn.runId = runId; + turn.sessionId = ctx.sessionId; + turn.agentId = ctx.agentId; + turn.channelId = turn.channelId ?? ctx.channelId; + turnsByRunId.set(runId, turn); + }); + + // ── llm_input: stash the prompt + history, mark start time ────────── + + api.on("llm_input", (rawEvent, rawCtx) => { + api.logger.info(`[kayba-tracing] hook: llm_input`); + const event = (rawEvent ?? {}) as LlmInputEvent; + const ctx = (rawCtx ?? {}) as ConversationCtx; + const runId = event.runId ?? ctx.runId; + if (!runId) return; + let turn = turnsByRunId.get(runId); + if (!turn) { + turn = { runId, sessionId: event.sessionId, agentId: ctx.agentId, channelId: ctx.channelId, startedAtMs: Date.now() }; + turnsByRunId.set(runId, turn); + } + turn.llmIn = event; + turn.llmInputAtMs = Date.now(); + turn.sessionId = turn.sessionId ?? event.sessionId; + }); + + // ── llm_output: stash response, mark end time ─────────────────────── + + api.on("llm_output", (rawEvent, rawCtx) => { + api.logger.info(`[kayba-tracing] hook: llm_output`); + const event = (rawEvent ?? {}) as LlmOutputEvent; + const ctx = (rawCtx ?? {}) as ConversationCtx; + const runId = event.runId ?? ctx.runId; + if (!runId) return; + const turn = turnsByRunId.get(runId); + if (!turn) return; + turn.llmOut = event; + turn.llmOutputAtMs = Date.now(); + if (turn.agentEndArrived) { + void finalizeTurn(runId); + } + }); + + // ── agent_end: defer finalize a tick to absorb any straggling llm_output ─ + + api.on("agent_end", (rawEvent, rawCtx) => { + api.logger.info(`[kayba-tracing] hook: agent_end`); + const event = (rawEvent ?? {}) as AgentEndEvent; + const ctx = (rawCtx ?? {}) as ConversationCtx; + const runId = event.runId ?? ctx.runId; + if (!runId) return; + const turn = turnsByRunId.get(runId); + if (!turn) return; + turn.agentEnd = event; + turn.endedAtMs = Date.now(); + turn.agentEndArrived = true; + setTimeout(() => void finalizeTurn(runId), TURN_FINALIZE_DELAY_MS); + }); + + // ── Finalize: emit one trace per turn ─────────────────────────────── + + async function finalizeTurn(runId: string): Promise { + const turn = turnsByRunId.get(runId); + if (!turn || turn.finalized) { + api.logger.info(`[kayba-tracing] finalize skipped (turn=${!!turn} finalized=${turn?.finalized})`); + return; + } + turn.finalized = true; + turnsByRunId.delete(runId); + api.logger.info(`[kayba-tracing] finalizing turn runId=${runId} sess=${turn.sessionId} hasLlmIn=${!!turn.llmIn} hasLlmOut=${!!turn.llmOut}`); + + const sessionId = turn.sessionId ?? ""; + const userId = resolveUserId(turn, cfg.userField); + const success = turn.agentEnd?.success ?? true; + const realDurationMs = turn.agentEnd?.durationMs ?? (turn.endedAtMs ?? Date.now()) - turn.startedAtMs; + + // Set process-global session/user so the kayba SDK injects them into trace metadata. + safe(() => kayba.setSession(sessionId || null), "setSession", api.logger); + safe(() => kayba.setUser(userId || null), "setUser", api.logger); + + const traced = kayba.trace( + async () => { + // Trace-level metadata + previews. We're inside an active trace context here. + safe( + () => + updateCurrentTrace({ + metadata: { + "openclaw.runId": runId, + "openclaw.agentId": turn.agentId ?? "", + "openclaw.channelId": turn.channelId ?? "", + "openclaw.realDurationMs": String(realDurationMs), + }, + requestPreview: turn.userMessage?.slice(0, 200), + responsePreview: turn.llmOut?.assistantTexts?.[0]?.slice(0, 200), + }), + "updateCurrentTrace", + api.logger, + ); + + // Nested llm.call span. Wall-clock duration is captured as an attribute + // (Number.MAX_SAFE_INTEGER < Date.now() * 1_000_000, so passing startTimeNs + // explicitly to mlflow corrupts the span — the OTel API expects nanos as a + // number which JS can't represent past ~285k years from epoch). + if (turn.llmIn || turn.llmOut) { + const llmRealDurationMs = + (turn.llmOutputAtMs ?? turn.endedAtMs ?? Date.now()) - + (turn.llmInputAtMs ?? turn.startedAtMs); + + // Resolve which slice of historyMessages to ship. + const fullHistory = turn.llmIn?.historyMessages ?? []; + let historyToShip: unknown[] | undefined; + let historyMode: "full" | "delta" | "none" = "none"; + let historySkipped = 0; + if (cfg.captureHistory === "full" && fullHistory.length > 0) { + historyToShip = fullHistory; + historyMode = "full"; + } else if (cfg.captureHistory === "delta" && sessionId) { + const cursor = sessionHistoryCursor.get(sessionId) ?? 0; + historySkipped = Math.min(cursor, fullHistory.length); + historyToShip = fullHistory.slice(historySkipped); + historyMode = "delta"; + sessionHistoryCursor.set(sessionId, fullHistory.length); + } + + // Capture systemPrompt only on the first turn of each session (it rarely changes). + const shouldShipSystemPrompt = + cfg.captureSystemPrompt && + !!turn.llmIn?.systemPrompt && + !!sessionId && + !sessionsWithSystemPromptShipped.has(sessionId); + if (shouldShipSystemPrompt && sessionId) sessionsWithSystemPromptShipped.add(sessionId); + + const llmSpan = mlflowStartSpan({ + name: "llm.call", + spanType: SpanType.LLM, + attributes: { + "openclaw.realDurationMs": String(llmRealDurationMs), + "openclaw.startedAtMs": String(turn.llmInputAtMs ?? ""), + "openclaw.endedAtMs": String(turn.llmOutputAtMs ?? ""), + "openclaw.historyMode": historyMode, + "openclaw.historySkipped": String(historySkipped), + "openclaw.historyTotalLength": String(fullHistory.length), + }, + inputs: { + provider: turn.llmIn?.provider, + model: turn.llmIn?.model, + ...(shouldShipSystemPrompt + ? { systemPrompt: truncate(turn.llmIn!.systemPrompt!, cfg.maxAttributeBytes) } + : {}), + prompt: truncate(turn.llmIn?.prompt, cfg.maxAttributeBytes), + ...(historyToShip && historyToShip.length > 0 + ? { historyMessages: unwrapJsonStrings(historyToShip) } + : {}), + imagesCount: turn.llmIn?.imagesCount ?? 0, + }, + }); + llmSpan.end({ + outputs: { + assistantTexts: turn.llmOut?.assistantTexts, + lastAssistant: unwrapJsonStrings(turn.llmOut?.lastAssistant), + usage: unwrapJsonStrings(turn.llmOut?.usage), + stopReason: turn.llmOut?.lastAssistant?.stopReason, + resolvedRef: turn.llmOut?.resolvedRef, + }, + status: success ? SpanStatusCode.OK : SpanStatusCode.ERROR, + }); + } + + return { + runId, + sessionId, + userId, + channel: turn.channelId, + senderId: turn.senderId, + userMessage: turn.userMessage, + assistantText: turn.llmOut?.assistantTexts?.[0], + success, + realDurationMs, + }; + }, + { + name: "agent.turn", + spanType: SpanType.AGENT, + attributes: { + "openclaw.runId": runId, + "openclaw.sessionId": sessionId, + "openclaw.agentId": turn.agentId ?? "", + "openclaw.channelId": turn.channelId ?? "", + "openclaw.senderId": turn.senderId ?? "", + "openclaw.success": String(success), + "openclaw.realDurationMs": String(realDurationMs), + }, + }, + ); + + try { + await traced(); + api.logger.info(`[kayba-tracing] emitted trace for runId=${runId} (real ${realDurationMs}ms)`); + } catch (err) { + api.logger.warn(`[kayba-tracing] trace emit failed: ${err instanceof Error ? err.message : String(err)}`); + } + } +} + +export default { + id: "kayba-tracing", + name: "Kayba Tracing", + description: "Captures every OpenClaw agent turn and ships it as a Kayba trace.", + register, +}; diff --git a/sdk/openclaw/tsconfig.json b/sdk/openclaw/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..bf3e60d6261ea1777de8e8b7f4f6d42ee7f20b9b --- /dev/null +++ b/sdk/openclaw/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..4c689666ad118ef84af55802812b43d7f2afcd47 --- /dev/null +++ b/sdk/python/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "kayba-tracing" +version = "0.9.7" +description = "Kayba tracing SDK — instrument your AI agents and send traces to Kayba" +readme = "README.md" +requires-python = ">=3.10" +license = {text = "MIT"} +authors = [ + {name = "Kayba.ai", email = "hello@kayba.ai"}, +] +keywords = ["kayba", "tracing", "mlflow", "observability", "ai", "agents"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +dependencies = [ + "mlflow>=3.1.0", +] + +[project.urls] +Homepage = "https://kayba.ai" +Repository = "https://github.com/Kayba-ai/agentic-context-engine" + +[tool.setuptools.packages.find] +where = ["src"] +include = ["kayba_tracing", "kayba_tracing.*"] diff --git a/sdk/python/src/kayba_tracing/__init__.py b/sdk/python/src/kayba_tracing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..399e4e7063e057930d00e9d6f4ddfa98cfc0f07b --- /dev/null +++ b/sdk/python/src/kayba_tracing/__init__.py @@ -0,0 +1,44 @@ +"""Kayba tracing — instrument your agents and send traces to Kayba. + +Usage:: + + from kayba_tracing import configure, trace, start_span + + configure(api_key="kb-...") + + @trace + def my_agent(query: str) -> str: + with start_span("retrieval") as span: + span.set_inputs({"query": query}) + results = search(query) + span.set_outputs(results) + return synthesize(results) + +Install:: + + pip install kayba-tracing +""" + +from kayba_tracing._wrapper import ( + configure, + disable, + enable, + get_folder, + get_trace, + search_traces, + set_folder, + start_span, + trace, +) + +__all__ = [ + "configure", + "disable", + "enable", + "get_folder", + "get_trace", + "search_traces", + "set_folder", + "start_span", + "trace", +] diff --git a/sdk/python/src/kayba_tracing/_wrapper.py b/sdk/python/src/kayba_tracing/_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..a1b6447fd9e00f6562f06fa5776c2feb5ef84f00 --- /dev/null +++ b/sdk/python/src/kayba_tracing/_wrapper.py @@ -0,0 +1,228 @@ +"""Thin Kayba-branded wrapper around MLflow tracing. + +All public symbols re-export MLflow functionality so that users never +need to ``import mlflow`` directly. The :func:`configure` helper sets +the MLflow tracking URI and auth to point at the Kayba backend. +""" + +from __future__ import annotations + +import functools +import os +import re +from contextlib import contextmanager +from typing import Any, Callable, Generator, TypeVar, overload + +_TRACING_INSTALL_HINT = ( + "Tracing requires mlflow: pip install kayba-tracing" +) + +try: + import mlflow + import mlflow.tracing # noqa: F401 — ensure tracing sub-module is loaded +except ImportError as exc: + raise ImportError(_TRACING_INSTALL_HINT) from exc + +DEFAULT_BASE_URL = "https://use.kayba.ai" + +# Module-level state set by configure() / set_folder(). +_folder: str | None = None + +_MAX_FOLDER_LENGTH = 256 +_SAFE_FOLDER_RE = re.compile(r"[^a-zA-Z0-9 _\-/.]") + + +def _sanitize_folder(name: str) -> str: + """Sanitize a folder name to prevent injection attacks. + + Strips control characters, HTML tags, and characters outside an + allowlist. Truncates to ``_MAX_FOLDER_LENGTH``. + """ + # Strip HTML tags. + clean = re.sub(r"<[^>]*>", "", name) + # Remove anything outside the safe set. + clean = _SAFE_FOLDER_RE.sub("", clean) + return clean.strip()[:_MAX_FOLDER_LENGTH] + + +_P = TypeVar("_P") +_R = TypeVar("_R") + + +def configure( + *, + api_key: str | None = None, + base_url: str | None = None, + experiment: str | None = None, + folder: str | None = None, +) -> None: + """Configure Kayba tracing. + + Sets the MLflow tracking URI and authentication so that all + subsequent ``@trace`` / ``start_span`` calls export to Kayba. + + Args: + api_key: Kayba API key. Falls back to ``KAYBA_API_KEY`` env var. + base_url: Kayba API base URL. Falls back to ``KAYBA_API_URL`` env + var, then to ``https://use.kayba.ai``. + experiment: Alias for ``folder``. If both are provided, ``folder`` + takes precedence. + folder: Optional folder name. Traces will be filed into this + folder in the Kayba dashboard. + """ + global _folder + + resolved_key = api_key or os.environ.get("KAYBA_API_KEY", "") + if not resolved_key: + raise ValueError( + "No API key provided. Pass api_key= or set the KAYBA_API_KEY " + "environment variable." + ) + + resolved_url = base_url or os.environ.get("KAYBA_API_URL") or DEFAULT_BASE_URL + # Strip trailing slash, then append the MLflow-compatible mount path. + tracking_uri = resolved_url.rstrip("/") + "/api/mlflow" + + # Configure MLflow under the hood. + os.environ["MLFLOW_TRACKING_TOKEN"] = resolved_key + mlflow.set_tracking_uri(tracking_uri) + + resolved_folder = folder or experiment + _folder = _sanitize_folder(resolved_folder) or None if resolved_folder else None + + +def set_folder(folder: str | None) -> None: + """Change the target folder for subsequent traces. + + Args: + folder: Folder name, or ``None`` to clear (traces go to Unfiled). + """ + global _folder + _folder = _sanitize_folder(folder) or None if folder else None + + +def get_folder() -> str | None: + """Return the currently configured folder, or ``None``.""" + return _folder + + +# --------------------------------------------------------------------------- +# Wrapped MLflow tracing primitives that inject the folder tag +# --------------------------------------------------------------------------- + + +def _inject_folder_tag() -> None: + """Inject ``kayba.folder`` tag into the active trace if a folder is set.""" + if _folder is not None: + mlflow.update_current_trace(tags={"kayba.folder": _folder}) + + +@overload +def trace(func: Callable[..., _R]) -> Callable[..., _R]: ... + + +@overload +def trace( + func: None = None, + *, + name: str | None = None, + span_type: str = ..., + attributes: dict[str, Any] | None = None, +) -> Callable[[Callable[..., _R]], Callable[..., _R]]: ... + + +def trace( + func: Callable[..., Any] | None = None, + *, + name: str | None = None, + span_type: str = "UNKNOWN", + attributes: dict[str, Any] | None = None, +) -> Any: + """Decorator that creates a trace span for the decorated function. + + Works identically to ``mlflow.trace`` but automatically tags + the trace with the configured Kayba folder. + """ + + def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: + # Wrap the original function so the folder tag is injected + # *inside* the trace context (before MLflow closes it). + @functools.wraps(fn) + def fn_with_tag(*args: Any, **kwargs: Any) -> Any: + result = fn(*args, **kwargs) + _inject_folder_tag() + return result + + # Let MLflow handle the actual tracing. + mlflow_kwargs: dict[str, Any] = {} + if name is not None: + mlflow_kwargs["name"] = name + if span_type != "UNKNOWN": + mlflow_kwargs["span_type"] = span_type + if attributes is not None: + mlflow_kwargs["attributes"] = attributes + + if mlflow_kwargs: + traced = mlflow.trace(**mlflow_kwargs)(fn_with_tag) + else: + traced = mlflow.trace(fn_with_tag) + + @functools.wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> Any: + return traced(*args, **kwargs) + + return wrapper + + if func is not None: + # Called as @trace without parentheses. + return decorator(func) + return decorator + + +@contextmanager +def start_span( + name: str = "span", + span_type: str | None = "UNKNOWN", + attributes: dict[str, Any] | None = None, +) -> Generator[Any, None, None]: + """Context manager that creates a child span. + + Works identically to ``mlflow.start_span`` but automatically tags + the trace with the configured Kayba folder when used as a root span. + """ + with mlflow.start_span( + name=name, span_type=span_type, attributes=attributes + ) as span: + yield span + # Inject folder tag while the trace context is still open. + _inject_folder_tag() + + +# --------------------------------------------------------------------------- +# Utility functions +# --------------------------------------------------------------------------- + + +def enable() -> None: + """Enable Kayba tracing (enabled by default after :func:`configure`).""" + mlflow.tracing.enable() + + +def disable() -> None: + """Disable Kayba tracing without removing the configuration.""" + mlflow.tracing.disable() + + +def get_trace(trace_id: str) -> Any: + """Retrieve a trace by ID.""" + return mlflow.get_trace(trace_id) + + +def search_traces( + experiment_names: list[str] | None = None, + **kwargs: Any, +) -> Any: + """Search for traces, optionally filtered by experiment names.""" + if experiment_names is None: + experiment_names = ["Default"] + return mlflow.search_traces(experiment_names=experiment_names, **kwargs) diff --git a/sdk/typescript/.gitignore b/sdk/typescript/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b6e73145747d8b4f8a49a11d991146721185058f --- /dev/null +++ b/sdk/typescript/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/sdk/typescript/example.ts b/sdk/typescript/example.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3c7484f2f8670ad6b4cca7c885427a127ab5726 --- /dev/null +++ b/sdk/typescript/example.ts @@ -0,0 +1,115 @@ +/** + * Example: instrument a GLM agent pipeline with Kayba tracing (TypeScript). + * + * Mirrors the Python example in examples/tracing_glm_example.py. + * + * Run: npx tsx example.ts + */ + +import "dotenv/config"; +import OpenAI from "openai"; +import kayba, { SpanType } from "./src/index"; + +// ── Kayba tracing setup ──────────────────────────────────────────────── +kayba.configure({ + apiKey: process.env.KAYBA_SDK_KEY, + baseUrl: process.env.KAYBA_BASE_URL, + folder: "ts-sdk-examples", +}); + +// ── OpenAI client (GLM-compatible endpoint) ──────────────────────────── +const client = new OpenAI({ + baseUrl: process.env.OPENAI_BASE_URL, + apiKey: process.env.OPENAI_API_KEY, +}); +const MODEL = "glm-5.1"; + +// ── Traced helper functions ──────────────────────────────────────────── + +const llmCall = kayba.trace( + async (messages: OpenAI.ChatCompletionMessageParam[]) => { + const response = await client.chat.completions.create({ + model: MODEL, + messages, + temperature: 0.7, + }); + return response.choices[0].message.content ?? ""; + }, + { name: "llm_call", spanType: SpanType.LLM }, +); + +const researchAgent = kayba.trace( + async (topic: string) => { + const span = kayba.startSpan({ + name: "build_prompt", + spanType: SpanType.TOOL, + inputs: { topic }, + }); + + const messages: OpenAI.ChatCompletionMessageParam[] = [ + { + role: "system", + content: "You are a research assistant. List 3 key facts.", + }, + { role: "user", content: `Research this topic: ${topic}` }, + ]; + + span.end({ outputs: { message_count: messages.length }, status: "OK" }); + + return await llmCall(messages); + }, + { name: "research_agent", spanType: SpanType.AGENT }, +); + +const summariserAgent = kayba.trace( + async (facts: string) => { + const span = kayba.startSpan({ + name: "build_prompt", + spanType: SpanType.TOOL, + inputs: { facts_length: facts.length }, + }); + + const messages: OpenAI.ChatCompletionMessageParam[] = [ + { + role: "system", + content: + "You are a summariser. Condense the following facts into one concise paragraph.", + }, + { role: "user", content: facts }, + ]; + + span.end({ outputs: { message_count: messages.length }, status: "OK" }); + + return await llmCall(messages); + }, + { name: "summariser_agent", spanType: SpanType.AGENT }, +); + +const runPipeline = kayba.trace( + async (topic: string) => { + const facts = await researchAgent(topic); + console.log(`\n--- Research Agent ---\n${facts}`); + + const summary = await summariserAgent(facts); + console.log(`\n--- Summariser Agent ---\n${summary}`); + + return summary; + }, + { name: "pipeline", spanType: SpanType.CHAIN }, +); + +// ── Main ─────────────────────────────────────────────────────────────── + +async function main() { + console.log("Running TypeScript tracing example...\n"); + + const result = await runPipeline("The history of the Silk Road"); + console.log(`\n--- Final result ---\n${result}`); + + // Give MLflow time to flush traces to Kayba + console.log("\nFlushing traces..."); + await new Promise((r) => setTimeout(r, 3000)); + console.log("Done!"); +} + +main().catch(console.error); diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..126cbee3154135b37783a7c292bf9f4333c6f5de --- /dev/null +++ b/sdk/typescript/package-lock.json @@ -0,0 +1,2809 @@ +{ + "name": "@kayba_ai/tracing", + "version": "0.10.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@kayba_ai/tracing", + "version": "0.10.0", + "license": "MIT", + "dependencies": { + "mlflow-tracing": "^0.1.0" + }, + "devDependencies": { + "tsup": "^8.0.0", + "tsx": "^4.21.0", + "typescript": "^5.5.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@databricks/sdk-experimental": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@databricks/sdk-experimental/-/sdk-experimental-0.15.0.tgz", + "integrity": "sha512-HkoMiF7dNDt6WRW0xhi7oPlBJQfxJ9suJhEZRFt08VwLMaWcw2PiF8monfHlkD4lkufEYV6CTxi5njQkciqiHA==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.5.0", + "ini": "^6.0.0", + "reflect-metadata": "^0.2.2", + "semver": "^7.7.3" + }, + "engines": { + "node": ">=22.0", + "npm": ">=10.0.0" + } + }, + "node_modules/@databricks/sdk-experimental/node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.205.0.tgz", + "integrity": "sha512-wBlPk1nFB37Hsm+3Qy73yQSobVn28F4isnWIBvKpd5IUH/eat8bwcL02H9yzmHyyPmukeccSl2mbN5sDQZYnPg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.1.0.tgz", + "integrity": "sha512-zOyetmZppnwTyPrt4S7jMfXiSX9yyfF0hxlA8B5oo2TtKl+/RGCy7fi4DrBfIf3lCPrkKsRBWZZD7RFojK7FDg==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.1.0.tgz", + "integrity": "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.205.0.tgz", + "integrity": "sha512-jQlw7OHbqZ8zPt+pOrW2KGN7T55P50e3NXBMr4ckPOF+DWDwSy4W7mkG09GpYWlQAQ5C9BXg5gfUlv5ldTgWsw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/sdk-logs": "0.205.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.205.0.tgz", + "integrity": "sha512-5JteMyVWiro4ghF0tHQjfE6OJcF7UBUcoEqX3UIQ5jutKP1H+fxFdyhqjjpmeHMFxzOHaYuLlNR1Bn7FOjGyJg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/sdk-logs": "0.205.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.205.0.tgz", + "integrity": "sha512-q3VS9wS+lpZ01txKxiDGBtBpTNge3YhbVEFDgem9ZQR9eI3EZ68+9tVZH9zJcSxI37nZPJ6lEEZO58yEjYZsVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-logs": "0.205.0", + "@opentelemetry/sdk-trace-base": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.205.0.tgz", + "integrity": "sha512-1Vxlo4lUwqSKYX+phFkXHKYR3DolFHxCku6lVMP1H8sVE3oj4wwmwxMzDsJ7zF+sXd8M0FCr+ckK4SnNNKkV+w==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.205.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-metrics": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.205.0.tgz", + "integrity": "sha512-fFxNQ/HbbpLmh1pgU6HUVbFD1kNIjrkoluoKJkh88+gnmpFD92kMQ8WFNjPnSbjg2mNVnEkeKXgCYEowNW+p1w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-metrics": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.205.0.tgz", + "integrity": "sha512-qIbNnedw9QfFjwpx4NQvdgjK3j3R2kWH/2T+7WXAm1IfMFe9fwatYxE61i7li4CIJKf8HgUC3GS8Du0C3D+AuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.205.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-metrics": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.205.0.tgz", + "integrity": "sha512-xsot/Qm9VLDTag4GEwAunD1XR1U8eBHTLAgO7IZNo2JuD/c/vL7xmDP7mQIUr6Lk3gtj/yGGIR2h3vhTeVzv4w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-metrics": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.205.0.tgz", + "integrity": "sha512-ZBksUk84CcQOuDJB65yu5A4PORkC4qEsskNwCrPZxDLeWjPOFZNSWt0E0jQxKCY8PskLhjNXJYo12YaqsYvGFA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.205.0.tgz", + "integrity": "sha512-vr2bwwPCSc9u7rbKc74jR+DXFvyMFQo9o5zs+H/fgbK672Whw/1izUKVf+xfWOdJOvuwTnfWxy+VAY+4TSo74Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.205.0.tgz", + "integrity": "sha512-bGtFzqiENO2GpJk988mOBMe0MfeNpTQjbLm/LBijas6VRyEDQarUzdBHpFlu89A25k1+BCntdWGsWTa9Ai4FyA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.1.0.tgz", + "integrity": "sha512-0mEI0VDZrrX9t5RE1FhAyGz+jAGt96HSuXu73leswtY3L5YZD11gtcpARY2KAx/s6Z2+rj5Mhj566JsI2C7mfA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.205.0.tgz", + "integrity": "sha512-cgvm7tvQdu9Qo7VurJP84wJ7ZV9F6WqDDGZpUc6rUEXwjV7/bXWs0kaYp9v+1Vh1+3TZCD3i6j/lUBcPhu8NhA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.205.0.tgz", + "integrity": "sha512-2MN0C1IiKyo34M6NZzD6P9Nv9Dfuz3OJ3rkZwzFmF6xzjDfqqCTatc9v1EpNfaP55iDOCLHFyYNCgs61FFgtUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-transformer": "0.205.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.205.0.tgz", + "integrity": "sha512-AeuLfrciGYffqsp4EUTdYYc6Ee2BQS+hr08mHZk1C524SFWx0WnfcTnV0NFXbVURUNU6DZu1DhS89zRRrcx/hg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/otlp-exporter-base": "0.205.0", + "@opentelemetry/otlp-transformer": "0.205.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.205.0.tgz", + "integrity": "sha512-KmObgqPtk9k/XTlWPJHdMbGCylRAmMJNXIRh6VYJmvlRDMfe+DonH41G7eenG8t4FXn3fxOGh14o/WiMRR6vPg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-logs": "0.205.0", + "@opentelemetry/sdk-metrics": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.1.0.tgz", + "integrity": "sha512-yOdHmFseIChYanddMMz0mJIFQHyjwbNhoxc65fEAA8yanxcBPwoFDoh1+WBUWAO/Z0NRgk+k87d+aFIzAZhcBw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.1.0.tgz", + "integrity": "sha512-QYo7vLyMjrBCUTpwQBF/e+rvP7oGskrSELGxhSvLj5gpM0az9oJnu/0O4l2Nm7LEhAff80ntRYKkAcSwVgvSVQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.1.0.tgz", + "integrity": "sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.205.0.tgz", + "integrity": "sha512-nyqhNQ6eEzPWQU60Nc7+A5LIq8fz3UeIzdEVBQYefB4+msJZ2vuVtRuk9KxPMw1uHoHDtYEwkr2Ct0iG29jU8w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.1.0.tgz", + "integrity": "sha512-J9QX459mzqHLL9Y6FZ4wQPRZG4TOpMCyPOh6mkr/humxE1W2S3Bvf4i75yiMW9uyed2Kf5rxmLhTm/UK8vNkAw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.205.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.205.0.tgz", + "integrity": "sha512-Y4Wcs8scj/Wy1u61pX1ggqPXPtCsGaqx/UnFu7BtRQE1zCQR+b0h56K7I0jz7U2bRlPUZIFdnNLtoaJSMNzz2g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.205.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.205.0", + "@opentelemetry/exporter-logs-otlp-http": "0.205.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.205.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.205.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.205.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.205.0", + "@opentelemetry/exporter-prometheus": "0.205.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.205.0", + "@opentelemetry/exporter-trace-otlp-http": "0.205.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.205.0", + "@opentelemetry/exporter-zipkin": "2.1.0", + "@opentelemetry/instrumentation": "0.205.0", + "@opentelemetry/propagator-b3": "2.1.0", + "@opentelemetry/propagator-jaeger": "2.1.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/sdk-logs": "0.205.0", + "@opentelemetry/sdk-metrics": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0", + "@opentelemetry/sdk-trace-node": "2.1.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.1.0.tgz", + "integrity": "sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.1.0.tgz", + "integrity": "sha512-SvVlBFc/jI96u/mmlKm86n9BbTCbQ35nsPoOohqJX6DXH92K0kTe73zGY5r8xoI1QkjR9PizszVJLzMC966y9Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.1.0", + "@opentelemetry/core": "2.1.0", + "@opentelemetry/sdk-trace-base": "2.1.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/import-in-the-middle": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz", + "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mlflow-tracing": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/mlflow-tracing/-/mlflow-tracing-0.1.3.tgz", + "integrity": "sha512-Koqkwaid5ubGHuLprBP6J7Su70WddlD11f2vgzgxbFFHYKsAsJatMGvjIck5CkyhT/gMUyBqpA3Lkl+zC3W3uQ==", + "license": "Apache-2.0", + "dependencies": { + "@databricks/sdk-experimental": "0.15.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-node": "^0.205.0", + "bignumber.js": "^9.0.0", + "fast-safe-stringify": "^2.1.1", + "ini": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "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 + } + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "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 + } + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json new file mode 100644 index 0000000000000000000000000000000000000000..d6194ccb83a37171e4a331ea1ba2f0c8d298d082 --- /dev/null +++ b/sdk/typescript/package.json @@ -0,0 +1,54 @@ +{ + "name": "@kayba_ai/tracing", + "version": "0.10.0", + "description": "Kayba tracing SDK — instrument your AI agents and send traces to Kayba", + "license": "MIT", + "author": "Kayba.ai ", + "repository": { + "type": "git", + "url": "https://github.com/kayba-ai/agentic-context-engine", + "directory": "sdk/typescript" + }, + "homepage": "https://kayba.ai", + "keywords": [ + "kayba", + "tracing", + "mlflow", + "observability", + "ai", + "agents" + ], + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "lint": "tsc --noEmit", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "mlflow-tracing": "^0.1.0" + }, + "devDependencies": { + "tsup": "^8.0.0", + "tsx": "^4.21.0", + "typescript": "^5.5.0" + }, + "engines": { + "node": ">=18" + } +} diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..cf55dcc4af9f37786b2758196013d173cfd8e40e --- /dev/null +++ b/sdk/typescript/src/index.ts @@ -0,0 +1,322 @@ +/** + * Kayba tracing SDK for TypeScript/Node.js. + * + * Thin wrapper around `mlflow-tracing` that configures auth and + * injects Kayba-specific folder tags. + * + * @example + * ```ts + * import kayba from "@kayba_ai/tracing"; + * + * kayba.configure({ + * apiKey: process.env.KAYBA_API_KEY, + * folder: "my-project", + * }); + * + * const myAgent = kayba.trace(async (topic: string) => { + * const span = kayba.startSpan({ name: "retrieval" }); + * // ... work ... + * span.end({ status: "OK" }); + * return result; + * }, { name: "research_agent" }); + * ``` + */ + +import { + init, + trace as mlflowTrace, + startSpan as mlflowStartSpan, + updateCurrentTrace, + SpanType, + type TraceOptions as MlflowTraceOptions, + type SpanOptions as MlflowSpanOptions, +} from "mlflow-tracing"; + +// ── Constants ────────────────────────────────────────────────────────── + +const DEFAULT_BASE_URL = "https://use.kayba.ai"; +const MAX_FOLDER_LENGTH = 256; +const SAFE_FOLDER_RE = /[^a-zA-Z0-9 _\-/.]/g; +const HTML_TAG_RE = /<[^>]*>/g; + +// ── Module state ─────────────────────────────────────────────────────── + +let _folder: string | null = null; +let _sessionId: string | null = null; +let _userId: string | null = null; +let _configured = false; + +// ── Types ────────────────────────────────────────────────────────────── + +export interface ConfigureOptions { + /** Kayba API key. Falls back to `KAYBA_API_KEY` env var. */ + apiKey?: string; + /** Kayba API base URL. Falls back to `KAYBA_API_URL` env var, then `https://use.kayba.ai`. */ + baseUrl?: string; + /** Alias for `folder`. If both provided, `folder` takes precedence. */ + experiment?: string; + /** Folder name for organizing traces in the Kayba dashboard. */ + folder?: string; + /** MLflow experiment ID. Defaults to `"0"`. */ + experimentId?: string; +} + +export interface TraceOptions { + /** Custom span name. Defaults to the function name. */ + name?: string; + /** Span type (e.g. SpanType.LLM, SpanType.AGENT). */ + spanType?: SpanType; + /** Additional span attributes. */ + attributes?: Record; +} + +export interface StartSpanOptions { + /** Span name. */ + name: string; + /** Span type. */ + spanType?: SpanType; + /** Input data to attach to the span. */ + inputs?: Record; +} + +// ── Internal helpers ─────────────────────────────────────────────────── + +function sanitizeFolder(name: string): string { + let clean = name.replace(HTML_TAG_RE, ""); + clean = clean.replace(SAFE_FOLDER_RE, ""); + return clean.trim().slice(0, MAX_FOLDER_LENGTH); +} + +function resolveFolder( + folder: string | undefined, + experiment: string | undefined, +): string | null { + const raw = folder ?? experiment; + if (!raw) return null; + const sanitized = sanitizeFolder(raw); + return sanitized || null; +} + +function injectKaybaContext(): void { + const tags: Record = {}; + const metadata: Record = {}; + + if (_folder !== null) tags["kayba.folder"] = _folder; + if (_sessionId !== null) metadata["mlflow.trace.session"] = _sessionId; + if (_userId !== null) metadata["mlflow.trace.user"] = _userId; + + const hasTags = Object.keys(tags).length > 0; + const hasMetadata = Object.keys(metadata).length > 0; + if (!hasTags && !hasMetadata) return; + + try { + const update: { tags?: Record; metadata?: Record } = {}; + if (hasTags) update.tags = tags; + if (hasMetadata) update.metadata = metadata; + updateCurrentTrace(update); + } catch { + // Silently ignore if no active trace context. + } +} + +// ── Public API ───────────────────────────────────────────────────────── + +/** + * Configure Kayba tracing. + * + * Sets the MLflow tracking URI and authentication so that all + * subsequent `trace` / `startSpan` calls export to Kayba. + */ +export function configure(options: ConfigureOptions = {}): void { + const apiKey = options.apiKey || process.env.KAYBA_API_KEY || ""; + + if (!apiKey) { + throw new Error( + "No API key provided. Pass apiKey or set the KAYBA_API_KEY environment variable.", + ); + } + + const baseUrl = + options.baseUrl || process.env.KAYBA_API_URL || DEFAULT_BASE_URL; + + const trackingUri = baseUrl.replace(/\/+$/, "") + "/api/mlflow"; + + // Configure MLflow under the hood. + process.env.MLFLOW_TRACKING_TOKEN = apiKey; + process.env.MLFLOW_TRACKING_URI = trackingUri; + + const experimentId = + options.experimentId || process.env.MLFLOW_EXPERIMENT_ID || "0"; + + init({ trackingUri, experimentId }); + + _folder = resolveFolder(options.folder, options.experiment); + _configured = true; +} + +/** + * Change the target folder for subsequent traces. + * + * @param folder - Folder name, or `null` to clear (traces go to Unfiled). + */ +export function setFolder(folder: string | null): void { + if (folder === null) { + _folder = null; + } else { + const sanitized = sanitizeFolder(folder); + _folder = sanitized || null; + } +} + +/** Return the currently configured folder, or `null`. */ +export function getFolder(): string | null { + return _folder; +} + +/** + * Set the session id auto-injected as `mlflow.trace.session` metadata on + * every subsequent trace. Use this to group multiple traces (e.g. each + * tool call producing its own trace) into a single agent run / conversation. + * + * @param sessionId - Session id, or `null` to clear. + */ +export function setSession(sessionId: string | null): void { + _sessionId = sessionId && sessionId.length > 0 ? sessionId : null; +} + +/** Return the currently configured session id, or `null`. */ +export function getSession(): string | null { + return _sessionId; +} + +/** + * Set the user id auto-injected as `mlflow.trace.user` metadata on every + * subsequent trace. + * + * @param userId - User id, or `null` to clear. + */ +export function setUser(userId: string | null): void { + _userId = userId && userId.length > 0 ? userId : null; +} + +/** Return the currently configured user id, or `null`. */ +export function getUser(): string | null { + return _userId; +} + +/** + * Attach tags and/or metadata to the currently active trace. Use this for + * per-call values like a tool call id. Folder, session, and user are + * auto-injected — call this only for additional fields. + * + * Silently no-ops when called outside an active trace context. + */ +export function updateTrace(update: { + tags?: Record; + metadata?: Record; +}): void { + try { + updateCurrentTrace(update); + } catch { + // Silently ignore if no active trace context. + } +} + +/** + * Wrap a function with tracing. The returned function generates a span + * each time it is called, with automatic folder tagging. + * + * @example + * ```ts + * const myFunc = trace(async (input: string) => { + * return doWork(input); + * }, { name: "my_func", spanType: SpanType.LLM }); + * ``` + */ +export function trace any>( + fn: T, + options: TraceOptions = {}, +): T { + // Wrap the function to inject the folder tag after execution. + const fnWithTag = (...args: Parameters): ReturnType => { + const result = fn(...args); + + // Handle async functions: inject tag after the promise resolves. + if (result instanceof Promise) { + return result.then((value: unknown) => { + injectKaybaContext(); + return value; + }) as ReturnType; + } + + injectKaybaContext(); + return result; + }; + + const mlflowOptions: MlflowTraceOptions = {}; + if (options.name) mlflowOptions.name = options.name; + if (options.spanType) mlflowOptions.spanType = options.spanType; + if (options.attributes) + mlflowOptions.attributes = options.attributes as Record; + + return mlflowTrace(fnWithTag as T, mlflowOptions); +} + +/** + * Create a span manually. Call `.end()` when done. + * + * @example + * ```ts + * const span = startSpan({ name: "retrieval", spanType: SpanType.TOOL }); + * span.setInputs({ query }); + * // ... work ... + * span.end({ outputs: { result }, status: "OK" }); + * ``` + */ +export function startSpan(options: StartSpanOptions) { + const mlflowOptions: MlflowSpanOptions = { + name: options.name, + }; + if (options.spanType) mlflowOptions.spanType = options.spanType; + if (options.inputs) mlflowOptions.inputs = options.inputs; + + const span = mlflowStartSpan(mlflowOptions); + + // Wrap .end() to inject the folder tag before closing. + const originalEnd = span.end.bind(span); + span.end = (endOptions?: Parameters[0]) => { + injectKaybaContext(); + return originalEnd(endOptions); + }; + + return span; +} + +/** Returns whether `configure()` has been called. */ +export function isConfigured(): boolean { + return _configured; +} + +// ── Re-export MLflow types for convenience ───────────────────────────── + +export { SpanType } from "mlflow-tracing"; +export type { LiveSpan, Span } from "mlflow-tracing"; + +// ── Default export ───────────────────────────────────────────────────── + +const kayba = { + configure, + trace, + startSpan, + setFolder, + getFolder, + setSession, + getSession, + setUser, + getUser, + updateTrace, + isConfigured, + SpanType, +}; + +export default kayba; diff --git a/sdk/typescript/tsconfig.json b/sdk/typescript/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..4290de1c4b4ee2f8c8b2d5c51d38b3956c4fa3e3 --- /dev/null +++ b/sdk/typescript/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "outDir": "./dist", + "rootDir": "./src", + "experimentalDecorators": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/sdk/typescript/tsup.config.ts b/sdk/typescript/tsup.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..30511427233907f498e5a6878715aa8881324dae --- /dev/null +++ b/sdk/typescript/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + sourcemap: true, + clean: true, +}); diff --git a/specs/001-openclaw-integration/contracts/cli.md b/specs/001-openclaw-integration/contracts/cli.md new file mode 100644 index 0000000000000000000000000000000000000000..6e9d9b35e91804189dc66c37cde7759293be6a66 --- /dev/null +++ b/specs/001-openclaw-integration/contracts/cli.md @@ -0,0 +1,108 @@ +# CLI Contract: learn_from_traces.py + +**Feature**: 001-openclaw-integration | **Date**: 2026-02-27 + +## Command + +```bash +uv run python examples/openclaw/learn_from_traces.py [OPTIONS] +``` + +## Arguments + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--dry-run` | bool | `false` | Parse sessions and report findings without running the learning pipeline or modifying any files | +| `--reprocess` | bool | `false` | Ignore the processed log and reprocess all sessions from scratch | + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `ANTHROPIC_API_KEY` | *(required)* | API key for LLM provider | +| `ACE_MODEL` | `"anthropic/claude-sonnet-4-20250514"` | LLM model for reflection and skill extraction | +| `OPENCLAW_AGENT_ID` | `"main"` | OpenClaw agent identifier | +| `OPENCLAW_HOME` | `~/.openclaw` | OpenClaw home directory | +| `OPENCLAW_WORKSPACE` | `~/.openclaw/workspace` | Path to OpenClaw workspace (where AGENTS.md lives) | + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | Success (including "nothing new to learn") | +| `1` | Error (missing sessions dir, API failure, corrupted skillbook) | + +## Output Format + +Console output with section headers: + +```text +============================================================ + Discovering sessions +============================================================ + Sessions dir: ~/.openclaw/agents/main/sessions + Total sessions: 12 + Already processed: 10 + New to process: 2 + +============================================================ + Parsing sessions +============================================================ + + b3db607f-....jsonl: Hello world + Parsed: 2, Skipped (empty): 0 + +============================================================ + Loading skillbook +============================================================ + Loaded 5 existing strategies from ~/.openclaw/ace_skillbook.json + +============================================================ + Learning from 2 traces +============================================================ + Processed: 2/2 + New strategies: 1 (total: 6) + + Latest strategies: + [session_mgmt-00006] Use greeting to establish session context... + +============================================================ + Saving +============================================================ + Skillbook: ~/.openclaw/ace_skillbook.json + Processed log: ~/.openclaw/ace_processed.txt + +============================================================ + Syncing to OpenClaw +============================================================ + Synced 6 strategies to ~/.openclaw/workspace/AGENTS.md + +============================================================ + Done +============================================================ +``` + +## Files Produced + +| File | Format | Description | +|------|--------|-------------| +| `~/.openclaw/ace_skillbook.json` | JSON | Persistent skillbook with all learned strategies | +| `~/.openclaw/ace_processed.txt` | Text | Newline-delimited list of processed session filenames | +| `~/.openclaw/workspace/AGENTS.md` | Markdown | Updated with strategies between `` markers | + +## AGENTS.md Marker Contract + +```markdown + +## Learned Strategies + +These strategies were learned from your past sessions. Use relevant +ones to improve your responses. Cite strategy IDs (e.g. +[web-scraping-00001]) when you apply them. + +{wrap_skillbook_context() output} + +``` + +- If markers exist: content between them is replaced +- If markers don't exist: section is appended to end of file +- Content outside markers is never modified diff --git a/specs/001-openclaw-integration/data-model.md b/specs/001-openclaw-integration/data-model.md new file mode 100644 index 0000000000000000000000000000000000000000..3aa3c5b3e9dc82467e02faf24174e2655acc7ab6 --- /dev/null +++ b/specs/001-openclaw-integration/data-model.md @@ -0,0 +1,161 @@ +# Data Model: OpenClaw Integration + +**Feature**: 001-openclaw-integration | **Date**: 2026-02-27 + +## Pipeline Steps + +### LoadTracesStep (`ace/steps/load_traces.py`) + +Generic step that reads a trace file from disk and puts raw content on `ctx.trace`. + +| Attribute | Value | +|-----------|-------| +| `requires` | `frozenset({"sample"})` — `sample` is the file path (`str \| Path`) | +| `provides` | `frozenset({"trace"})` — raw file content (type depends on file format) | + +**Behaviour**: Reads the file at `ctx.sample`, parses JSONL lines into `list[dict]`, places on `ctx.trace`. Skips unparseable lines gracefully. + +### OpenClawToTraceStep (`ace/integrations/openclaw/to_trace.py`) + +OpenClaw-specific step that converts raw JSONL events into a structured trace dict, preserving chronological order of queries, thinking, and tool uses. + +| Attribute | Value | +|-----------|-------| +| `requires` | `frozenset({"trace"})` — raw `list[dict]` from LoadTracesStep | +| `provides` | `frozenset({"trace"})` — structured trace dict for ReflectStep | + +**Behaviour**: Walks events in order, extracts message content items (text, thinking, toolCall, toolResult) preserving full content without truncation. Produces a trace dict with `{question, reasoning, answer, skill_ids, feedback, ground_truth}`. Transformation logic TBD (user will define separately). + +## Entities + +### SessionEvent + +A single line from an OpenClaw JSONL transcript file. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | `str` | yes | Event type: `"session"`, `"message"`, `"thinking_level_change"`, `"custom"` | +| `id` | `str` | yes | Unique event identifier | +| `parentId` | `str \| None` | no | Parent event ID for threading | +| `timestamp` | `str` | yes | ISO 8601 timestamp | +| `message` | `MessagePayload \| None` | no | Present when `type == "message"` | +| `version` | `int \| None` | no | Present when `type == "session"` | +| `cwd` | `str \| None` | no | Working directory (session events only) | +| `thinkingLevel` | `str \| None` | no | Present when `type == "thinking_level_change"` | +| `data` | `dict \| None` | no | Present when `type == "custom"` | + +### MessagePayload + +The `message` field within a `SessionEvent` of type `"message"`. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `role` | `str` | yes | `"user"`, `"assistant"`, or `"toolResult"` | +| `content` | `list[ContentItem]` | yes | Array of content items | +| `api` | `str \| None` | no | API used (e.g., `"openai-completions"`) | +| `provider` | `str \| None` | no | Provider name (e.g., `"litellm"`) | +| `model` | `str \| None` | no | Model identifier | +| `usage` | `UsageInfo \| None` | no | Token/cost tracking | +| `stopReason` | `str \| None` | no | Why generation stopped | +| `timestamp` | `int \| None` | no | Unix timestamp (ms) | + +### ContentItem + +An individual content block within a message's content array. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | `str` | yes | `"text"`, `"thinking"`, `"toolCall"`, `"toolResult"` | +| `text` | `str \| None` | no | Present when `type == "text"` | +| `thinking` | `str \| None` | no | Present when `type == "thinking"` | +| `id` | `str \| None` | no | Tool call ID (toolCall) | +| `name` | `str \| None` | no | Tool name (toolCall) | +| `arguments` | `dict \| None` | no | Tool arguments (toolCall) | +| `toolCallId` | `str \| None` | no | Matching call ID (toolResult) | +| `content` | `list[dict] \| None` | no | Result content (toolResult) | + +### Trace (dict) + +The structured representation placed on `ctx.trace` by `OpenClawToTraceStep`. This is a plain dict matching the TraceAnalyser's raw trace interface. + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `question` | `str` | yes | First user message in the session | +| `reasoning` | `str` | yes | Full chronological conversation: user messages, assistant responses, thinking (full), tool calls (full), tool results (full) | +| `answer` | `str` | yes | Last assistant text response | +| `skill_ids` | `list[str]` | yes | Always `[]` (no prior skills applied) | +| `feedback` | `str` | yes | Summary string (e.g., "Session completed with N tool calls") | +| `ground_truth` | `None` | yes | Always `None` (no ground truth for open-ended sessions) | + +### Skillbook (existing) + +Reused from `ace.core.skillbook.Skillbook`. No changes needed. + +| Field | Type | Description | +|-------|------|-------------| +| `skills` | `dict[str, Skill]` | ID → Skill mapping | +| `sections` | `dict[str, list[str]]` | Section → skill IDs | +| `next_id` | `int` | Counter for new skill IDs | +| `similarity_decisions` | `dict` | Deduplication cache | + +### Skill (existing) + +Reused from `ace.core.skillbook.Skill`. No changes needed. + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `str` | Format: `{section}-{5-digit-counter}` | +| `section` | `str` | Category/domain | +| `content` | `str` | Strategy description | +| `justification` | `str` | Why this strategy is valuable | +| `evidence` | `str` | Source session evidence | +| `helpful` | `int` | Positive vote count | +| `harmful` | `int` | Negative vote count | +| `neutral` | `int` | Neutral vote count | +| `created_at` | `str` | ISO 8601 timestamp | +| `updated_at` | `str` | ISO 8601 timestamp | +| `status` | `str` | `"active"` or `"invalid"` | + +### ProcessedLog + +Plain text file tracking which sessions have been processed. + +| Aspect | Detail | +|--------|--------| +| **Path** | `~/.openclaw/ace_processed.txt` | +| **Format** | Newline-delimited session filenames (sorted) | +| **Example** | `b3db607f-7ae8-4089-b806-44800e961672.jsonl\nc4ef912a-...jsonl\n` | + +## Relationships + +```text +LoadTracesStep: + ctx.sample (file path) → read JSONL → ctx.trace (list[dict] raw events) + +OpenClawToTraceStep: + ctx.trace (list[dict] raw events) → convert → ctx.trace (structured trace dict) + +Pipeline composition: + LoadTracesStep → OpenClawToTraceStep → ReflectStep → TagStep → UpdateStep → ApplyStep + +SessionEvent (JSONL line) + └── contains → MessagePayload + └── contains → ContentItem[] + ├── text → user messages / assistant responses (full) + ├── thinking → reasoning content (full, no truncation) + ├── toolCall → tool invocation data (full) + └── toolResult → tool output data (full) + +Skillbook → save_to_file() → ace_skillbook.json +Skillbook → wrap_skillbook_context() → sync_to_agents_md() → AGENTS.md +Session filenames → ProcessedLog (ace_processed.txt) +``` + +## Validation Rules + +1. **LoadTracesStep**: Skips unparseable JSONL lines. Returns empty list for empty/missing files. +2. **SessionEvent**: Events with `type != "message"` are skipped by OpenClawToTraceStep. +3. **MessagePayload**: Messages must have `role` in `{"user", "assistant"}` and non-empty `content` array. +4. **ContentItem**: Unknown `type` values are silently skipped. +5. **Trace**: Must have non-empty `question` (at least one user message). Sessions with no user messages produce `None` from OpenClawToTraceStep. +6. **No truncation**: Thinking content, tool call arguments, and tool results are all preserved in full per clarifications. diff --git a/specs/001-openclaw-integration/plan.md b/specs/001-openclaw-integration/plan.md new file mode 100644 index 0000000000000000000000000000000000000000..c6c4d921a3bf677c0105318c4409c10c59a49508 --- /dev/null +++ b/specs/001-openclaw-integration/plan.md @@ -0,0 +1,87 @@ +# Implementation Plan: OpenClaw Integration + +**Branch**: `001-openclaw-integration` | **Date**: 2026-02-27 | **Spec**: [spec.md](spec.md) +**Input**: Feature specification from `/specs/001-openclaw-integration/spec.md` + +## Summary + +Integrate ACE with OpenClaw to automatically learn from session transcripts (JSONL) and sync strategies back into the agent's workspace (AGENTS.md). Uses the existing `TraceAnalyser` pipeline to run Reflect → Tag → Update → Apply on parsed transcripts, with incremental processing and dry-run support. The implementation adds two new pipeline steps — a generic `LoadTracesStep` in `ace/steps/` and an OpenClaw-specific `OpenClawToTraceStep` in `ace/integrations/openclaw/` — composed with the learning tail in an example script (`examples/openclaw/learn_from_traces.py`). + +## Technical Context + +**Language/Version**: Python 3.12+ +**Primary Dependencies**: ace (Skillbook, Reflector, SkillManager, TraceAnalyser, LiteLLMClient, wrap_skillbook_context), pydantic >=2.0.0, litellm >=1.78.0 +**Storage**: JSON file (skillbook at `~/.openclaw/ace_skillbook.json`), plain text (processed log at `~/.openclaw/ace_processed.txt`), JSONL (OpenClaw session transcripts) +**Testing**: pytest with pytest-cov (coverage enforced `--cov-fail-under=25`), MockLLMClient pattern from existing tests +**Target Platform**: Linux/macOS (local development machines where OpenClaw runs) +**Project Type**: Example/integration script (shipped in `examples/openclaw/`, not in the core library) +**Performance Goals**: Incremental runs (no new sessions) complete in <5 seconds without LLM calls (SC-003); handle 500+ sessions in a single run (SC-005) +**Constraints**: No new core dependencies; uses only existing ACE pipeline components; environment variable configuration +**Scale/Scope**: Single CLI script + tests; targets individual developer workstations with 1-500+ session transcripts + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Principle | Status | Evidence | +|-----------|--------|----------| +| **I. Ease of Use First** | PASS | SC-001 requires <5 min setup with <=3 config steps. Single script entry point. Config via environment variables with sensible defaults. README with copy-pasteable examples. | +| **II. Practical Value** | PASS | Solves a concrete problem: extracting strategies from real OpenClaw sessions. Adds learning/skillbook evolution on top of OpenClaw (measurable value per constitution). | +| **III. Simplicity** | PASS | Single script in `examples/`, no new abstractions. Reuses existing TraceAnalyser, Skillbook, Reflector, SkillManager. No new dependencies. Plain-text processed log (not a DB). | +| **IV. Clean & Modular Code** | PASS | Parsing, learning, syncing, and tracking are separate functions. Uses existing ACE module boundaries. No circular dependencies introduced. | + +**Gate Result**: PASS — No violations. Proceed to Phase 0. + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-openclaw-integration/ +├── plan.md # This file +├── research.md # Phase 0 output +├── data-model.md # Phase 1 output +├── quickstart.md # Phase 1 output +├── contracts/ # Phase 1 output (CLI contract) +└── tasks.md # Phase 2 output (/speckit.tasks command) +``` + +### Source Code (repository root) + +```text +ace/steps/ +└── load_traces.py # LoadTracesStep — generic file→ctx.trace loader + +ace/integrations/openclaw/ +├── __init__.py # Exports OpenClawToTraceStep +└── to_trace.py # OpenClawToTraceStep — JSONL events→trace dict + +examples/openclaw/ +├── learn_from_traces.py # Main entry point (composes steps + learning tail) +├── README.md # Integration documentation (existing) +└── *.jsonl # Sample session transcripts + +tests/ +├── test_load_traces_step.py # Unit tests for LoadTracesStep +└── test_openclaw.py # Unit tests for OpenClawToTraceStep, end-to-end + +docs/integrations/ +└── openclaw.md # Integration guide (new) +``` + +**Structure Decision**: Two new pipeline steps following existing patterns. `LoadTracesStep` is generic (reads files, puts raw data on `ctx.trace`) and lives in `ace/steps/`. `OpenClawToTraceStep` is integration-specific (converts OpenClaw JSONL to trace dict) and lives in `ace/integrations/openclaw/`. The example script composes these steps with `learning_tail()`. No changes to existing core classes. + +## Constitution Re-Check (Post-Design) + +| Principle | Status | Post-Design Evidence | +|-----------|--------|---------------------| +| **I. Ease of Use First** | PASS | quickstart.md confirms 3-step setup. CLI contract shows clear flags and output. | +| **II. Practical Value** | PASS | R-001 confirmed real JSONL format parsing. Thinking content (R-003) and tool calls (R-004) provide rich learning signal. | +| **III. Simplicity** | PASS | No new entities beyond what spec defined. Reuses all existing ACE APIs. Plain dict traces, no new dataclasses. | +| **IV. Clean & Modular Code** | PASS | Data model shows clean separation: parsing (JSONL → Trace), learning (TraceAnalyser), persistence (Skillbook), sync (AGENTS.md). Each is a distinct function. | + +**Post-Design Gate Result**: PASS — No violations introduced during design. + +## Complexity Tracking + +> No constitution violations — this section is intentionally empty. diff --git a/specs/001-openclaw-integration/quickstart.md b/specs/001-openclaw-integration/quickstart.md new file mode 100644 index 0000000000000000000000000000000000000000..8796f88f66d93ce3cfca9db9503afb7c219f070d --- /dev/null +++ b/specs/001-openclaw-integration/quickstart.md @@ -0,0 +1,77 @@ +# Quick Start: OpenClaw Integration + +**Feature**: 001-openclaw-integration | **Date**: 2026-02-27 + +## Prerequisites + +- Python 3.12+ +- An OpenClaw agent that has completed at least one session +- An Anthropic API key (or any LiteLLM-supported provider) + +## Setup (3 steps) + +### 1. Install + +```bash +git clone https://github.com/kayba-ai/agentic-context-engine.git +cd agentic-context-engine +uv sync +``` + +### 2. Configure + +```bash +export ANTHROPIC_API_KEY="your-api-key" +``` + +Optional overrides: + +```bash +export OPENCLAW_AGENT_ID="main" # which agent to learn from +export OPENCLAW_HOME="~/.openclaw" # OpenClaw home directory +export ACE_MODEL="anthropic/claude-sonnet-4-20250514" # LLM model +``` + +### 3. Run + +```bash +# Learn from all past sessions +uv run python examples/openclaw/kayba-ace/learn_from_traces.py + +# Preview what would be processed (no LLM calls, no file changes) +uv run python examples/openclaw/kayba-ace/learn_from_traces.py --dry-run + +# Reprocess everything (ignore what's already been learned) +uv run python examples/openclaw/kayba-ace/learn_from_traces.py --reprocess +``` + +## What Happens + +1. **Discovers** session transcripts from `~/.openclaw/agents//sessions/` +2. **Parses** JSONL files into structured traces (user messages, reasoning, tool calls) +3. **Learns** by running ACE's Reflect → Tag → Update → Apply pipeline +4. **Saves** strategies to `~/.openclaw/ace_skillbook.json` +5. **Syncs** strategies into `~/.openclaw/workspace/AGENTS.md` +6. Your OpenClaw agent reads the updated AGENTS.md on its next session + +## Automate (optional) + +Run every 30 minutes via cron: + +```bash +crontab -e +# Add: +*/30 * * * * cd /path/to/agentic-context-engine && uv run python examples/openclaw/kayba-ace/learn_from_traces.py >> /tmp/ace-openclaw.log 2>&1 +``` + +## Verify + +After running, check the output: + +```bash +# View learned strategies +cat ~/.openclaw/ace_skillbook.json | python -m json.tool | head -50 + +# View what was injected into your agent +grep -A 20 "ACE:SKILLBOOK:START" ~/.openclaw/workspace/AGENTS.md +``` diff --git a/specs/001-openclaw-integration/research.md b/specs/001-openclaw-integration/research.md new file mode 100644 index 0000000000000000000000000000000000000000..7d05ef8d01351b8699417fa96f9c1537e107c610 --- /dev/null +++ b/specs/001-openclaw-integration/research.md @@ -0,0 +1,92 @@ +# Research: OpenClaw Integration + +**Feature**: 001-openclaw-integration | **Date**: 2026-02-27 + +## R-001: OpenClaw JSONL Transcript Format + +**Decision**: The parser must handle the nested OpenClaw JSONL event format, not flat role/content events. + +**Rationale**: Inspecting the sample transcript (`b3db607f-...jsonl`) reveals the format is significantly different from what the current parser assumes: + +- **Top-level fields**: `type`, `id`, `parentId`, `timestamp`, and optionally `message` +- **Event types**: `"session"`, `"message"`, `"thinking_level_change"`, `"custom"` +- **Message structure**: `event["message"]` contains `role`, `content` (array), `api`, `provider`, `model`, `usage` +- **Content array items**: Each has a `type` field — `"text"`, `"thinking"`, `"toolCall"`, `"toolResult"` + - Text: `{"type": "text", "text": "..."}` + - Thinking: `{"type": "thinking", "thinking": "...", "thinkingSignature": "reasoning_content"}` + - Tool call: `{"type": "toolCall", "id": "...", "name": "...", "arguments": {...}}` + - Tool result: `{"type": "toolResult", "toolCallId": "...", "content": [{"type": "text", "text": "..."}]}` + +**Current parser bug**: `parse_session_jsonl()` reads `event.get("role")` and `event.get("content")` at the top level. The actual data has `event["message"]["role"]` and `event["message"]["content"]` (an array, not a string). The current parser will always produce empty traces from real OpenClaw sessions. + +**Alternatives considered**: None — must match the actual format. + +## R-002: Trace Dict Structure for TraceAnalyser + +**Decision**: Keep the existing trace dict format `{question, reasoning, answer, skill_ids, feedback, ground_truth}` as TraceAnalyser accepts raw dicts placed on `ctx.trace`. + +**Rationale**: `TraceAnalyser` uses raw traces — any dict type is accepted and placed on `ctx.trace`. The Reflector then reads `ctx.trace` (or falls back to `ctx.agent_output`). The trace dict keys should map cleanly to what Reflector's prompt template expects. Existing test patterns confirm this structure works. + +**Alternatives considered**: Using `AgentOutput` dataclass — rejected because TraceAnalyser explicitly supports raw traces without wrapping. + +## R-003: Thinking Content Handling + +**Decision**: Preserve thinking content in full — no truncation or filtering. + +**Rationale**: Thinking traces are integral to how the OpenClaw agent works. They contain the richest reasoning data and provide essential context for the ACE learning pipeline to understand the agent's decision-making process. Truncation would lose critical information that the Reflector needs to extract meaningful strategies. + +**Clarification**: User explicitly confirmed thinking traces must be preserved in full (see spec Clarifications 2026-02-27). + +**Alternatives rejected**: +- Head+tail truncation (500/200 chars) — rejected per user requirement; loses critical reasoning context. +- Filtering thinking blocks entirely — rejected; removes the most valuable data for learning. + +## R-004: Tool Call/Result Pairing + +**Decision**: Extract tool calls with their names and arguments from `"toolCall"` content items, and pair with results from subsequent `"toolResult"` events. Preserve all data in full — no truncation of arguments or results. + +**Rationale**: Tool usage is a key signal for the Reflector. The JSONL format stores tool calls as content items within assistant messages, and tool results as separate content items (or messages) with matching `toolCallId`. Pairing them gives the Reflector a complete picture of what tools were used and what they returned. + +**Clarification**: User explicitly confirmed tool arguments and results must be preserved in full (see spec Clarifications 2026-02-27). + +**Alternatives rejected**: Ignoring tool calls — rejected because tool usage patterns are one of the most valuable things to learn from. Truncating tool results — rejected per user requirement. + +## R-005: Session Discovery Path + +**Decision**: Discover sessions from `~/.openclaw/agents//sessions/*.jsonl` with the session directory structure matching the JSONL sample. + +**Rationale**: The sample file's `"session"` event contains `"cwd": "/app"` and session metadata. OpenClaw stores sessions per-agent under the home directory. The `OPENCLAW_AGENT_ID` environment variable selects which agent's sessions to process. + +**Alternatives considered**: Recursive glob for all agents — rejected for simplicity (Principle III). Users can run the script multiple times with different `OPENCLAW_AGENT_ID` values. + +## R-006: Skillbook Persistence Format + +**Decision**: Use the existing `Skillbook.save_to_file()` / `Skillbook.load_from_file()` JSON format. No custom serialization needed. + +**Rationale**: The skillbook JSON format is well-defined with fields: `skills`, `sections`, `next_id`, `similarity_decisions`. Each skill has `id`, `section`, `content`, `justification`, `evidence`, `helpful/harmful/neutral`, timestamps, and `status`. The existing API handles all serialization. + +**Alternatives considered**: None — reusing existing infrastructure per Principle III. + +## R-007: AGENTS.md Sync Format + +**Decision**: Use `wrap_skillbook_context()` from `ace.integrations` to format the skillbook content between HTML comment markers ``. + +**Rationale**: `wrap_skillbook_context()` already produces a formatted string with skillbook strategies and usage instructions. The marker-based replacement pattern in `sync_to_agents_md()` is correct — preserves content outside markers, handles create/update cases. + +**Alternatives considered**: Custom formatting — rejected because `wrap_skillbook_context()` already exists and produces the right format. + +## R-008: Error Handling Strategy + +**Decision**: Graceful degradation — skip individual malformed sessions, report errors, continue processing. + +**Rationale**: Constitution Principle I (Ease of Use) requires that a single bad file doesn't crash the entire run. FR-010 explicitly requires skipping malformed files gracefully. The current try/except pattern in `parse_session_jsonl()` handles `JSONDecodeError` correctly. + +**Alternatives considered**: Strict mode that fails on first error — rejected as it violates FR-010 and Principle I. + +## R-009: Testing Strategy + +**Decision**: Unit tests with MockLLMClient, using sample JSONL fixtures. No live LLM calls in tests. + +**Rationale**: Existing test patterns in `tests/` use `MockLLMClient` that returns canned `ReflectorOutput` and `SkillManagerOutput`. This allows testing the full flow (parse → analyse → save → sync) without API costs or flakiness. The sample JSONL file can serve as a test fixture. + +**Alternatives considered**: Integration tests with real LLM — useful but should be `@pytest.mark.slow` and optional. diff --git a/specs/001-openclaw-integration/spec.md b/specs/001-openclaw-integration/spec.md new file mode 100644 index 0000000000000000000000000000000000000000..6f1a78646f1f97e5ea6314084e5ae47aa01e7f0d --- /dev/null +++ b/specs/001-openclaw-integration/spec.md @@ -0,0 +1,155 @@ +# Feature Specification: OpenClaw Integration + +**Feature Branch**: `001-openclaw-integration` +**Created**: 2026-02-27 +**Status**: Implemented +**Input**: User description: "Integrate ACE with OpenClaw to automatically learn from session transcripts and sync strategies back into the agent's workspace" + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - One-Off Learning from Past Sessions (Priority: P1) + +A developer has been running an OpenClaw agent that accumulates session transcripts over time. They install the ACE framework and want to immediately extract useful strategies from all existing sessions. They run a single command, and ACE parses the transcripts, identifies patterns and lessons, and produces a set of learned strategies. The developer can inspect what was learned before deciding to use it. + +**Why this priority**: This is the core value proposition — extracting actionable strategies from historical agent sessions. Without this, no other feature matters. It delivers immediate value from day one with zero ongoing configuration. + +**Independent Test**: Can be fully tested by providing sample session transcript files and verifying that strategies are extracted and saved to a persistent skillbook file. + +**Acceptance Scenarios**: + +1. **Given** an OpenClaw agent has completed at least one session with transcript files on disk, **When** the developer runs the learning process, **Then** the system discovers all session files, parses them into trace data, runs the learning pipeline, and saves learned strategies to a persistent skillbook file. +2. **Given** the learning process has completed, **When** the developer inspects the output, **Then** they see a summary of how many sessions were processed, how many strategies were extracted, and a preview of the latest strategies. +3. **Given** some session files contain no usable content (empty or malformed), **When** the learning process encounters them, **Then** they are skipped with a count of skipped sessions reported, and processing continues for remaining files. + +--- + +### User Story 2 - Strategy Sync to Agent Workspace (Priority: P2) + +After learning from sessions, the developer wants the learned strategies injected back into the OpenClaw agent's workspace so the agent can use them on its next run. The system writes strategies into a workspace file that OpenClaw reads at session start, formatted between clearly marked boundaries so other content in the file is preserved. + +**Why this priority**: Learning without application has no value. This closes the feedback loop — strategies extracted from past sessions directly improve future sessions. It's the second half of the core value proposition. + +**Independent Test**: Can be fully tested by running learning on sample sessions and verifying that the workspace file is created/updated with formatted strategies between marker boundaries, with any existing content outside the markers preserved. + +**Acceptance Scenarios**: + +1. **Given** the skillbook contains learned strategies, **When** the sync process runs, **Then** the strategies are written into the agent's workspace file between clearly defined marker comments. +2. **Given** the workspace file already contains content outside the marker comments, **When** the sync process runs, **Then** all existing content outside the markers is preserved unchanged. +3. **Given** the workspace file does not yet exist, **When** the sync process runs, **Then** the file is created with the strategies section. +4. **Given** the sync process has previously written strategies and new strategies have been learned, **When** the sync runs again, **Then** the marker section is replaced with the updated strategies. + +--- + +### User Story 3 - Incremental Processing of New Sessions (Priority: P3) + +A developer runs the OpenClaw agent regularly, generating new sessions over time. They want the learning process to only process new sessions each time it runs, avoiding redundant reprocessing of sessions already learned from. They also want the option to reprocess everything if needed (e.g., after resetting the skillbook). + +**Why this priority**: For ongoing use, incremental processing prevents wasted computation and LLM API calls. Without it, every run would reprocess the entire history, which is slow and expensive. The reprocess escape hatch ensures developers are never stuck. + +**Independent Test**: Can be fully tested by running the learning process twice — the second run should skip previously processed sessions and only process new ones. Running with a reprocess flag should process all sessions again. + +**Acceptance Scenarios**: + +1. **Given** the learning process has previously run and processed 5 sessions, **When** it runs again with 2 new sessions available, **Then** only the 2 new sessions are processed. +2. **Given** the processed session log tracks which sessions have been handled, **When** the developer requests a full reprocess, **Then** all sessions are processed regardless of the log. +3. **Given** the processed session log does not exist (first run), **When** the learning process runs, **Then** all available sessions are treated as new. + +--- + +### User Story 4 - Dry Run Preview (Priority: P4) + +A developer wants to see what sessions would be processed and what data would be extracted without actually running the learning pipeline or spending LLM API credits. They run the process in a preview mode that parses sessions and reports what it found, but does not call the learning pipeline or modify any files. + +**Why this priority**: Developers need to verify their setup and understand what data is available before committing to an LLM-powered learning run. This reduces waste and builds confidence in the integration. + +**Independent Test**: Can be fully tested by running in preview mode with sample sessions and verifying that session parsing results are displayed but no skillbook or workspace files are created or modified. + +**Acceptance Scenarios**: + +1. **Given** new session files exist, **When** the developer runs in preview mode, **Then** the system reports how many sessions were found, parses them, and displays a summary of extracted data without calling the learning pipeline. +2. **Given** the developer runs in preview mode, **When** the process completes, **Then** no skillbook file, workspace file, or processed log is created or modified. + +--- + +### User Story 5 - Docker Deployment (Priority: P1) + +A developer wants ACE baked into their OpenClaw Docker image so the agent can trigger learning itself at session start — zero host-side setup. They extend the OpenClaw image with a `Dockerfile.ace` that installs Python 3.12, uv, and the ACE framework, then add AGENTS.md instructions telling the agent to run `ace-learn` and read the skillbook. + +**Why this priority**: Most OpenClaw users run Docker. Without a Docker path, they must install Python/uv on the host, manage paths, and set up cron — friction that prevents adoption. Docker makes it zero-config after the initial image build. + +**Independent Test**: Build the extended image, run `ace-learn --dry-run` inside a container with mounted `.openclaw` volume, verify it discovers sessions and reports correctly. + +**Acceptance Scenarios**: + +1. **Given** the developer has a working OpenClaw Docker setup, **When** they build with `Dockerfile.ace` and pass their LLM API key through docker-compose, **Then** the `ace-learn` command is available inside the container. +2. **Given** the extended image is running, **When** the agent runs `ace-learn` at session start, **Then** it processes new sessions, updates the skillbook in the workspace volume, and reports results. +3. **Given** the AGENTS.md contains the auto-learning instructions, **When** the agent starts a new session, **Then** it runs `ace-learn`, reads `skills/kayba-ace/ace_skillbook.md` using file-reading tools, and applies relevant strategies. +4. **Given** the skillbook is written to the workspace volume, **When** the container restarts, **Then** the skillbook persists and is available for the next session. + +--- + +### Edge Cases + +- What happens when the session transcript directory does not exist (OpenClaw not installed or agent never run)? The system reports a clear error message indicating the expected directory and suggests verifying the installation. +- What happens when all discovered sessions have already been processed? The system reports "nothing new to learn from" and exits cleanly without errors. +- What happens when the LLM API key is missing or invalid? The system fails with a clear error before attempting any API calls, without corrupting existing skillbook or workspace files. +- What happens when the session transcript format changes between OpenClaw versions? The parser handles missing or unexpected fields gracefully, skipping unparseable sessions and reporting them as skipped. +- What happens when the skillbook file is corrupted or invalid? The system reports the issue and offers the option to start fresh with a new skillbook rather than crashing. +- What happens when the workspace file has been manually edited and the marker comments were removed? The system appends a new marker section to the end of the file rather than silently overwriting content. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST discover session transcript files from the configured OpenClaw agent's session directory. +- **FR-002**: System MUST parse session transcripts into structured trace data containing the original user request, agent reasoning (including full thinking traces without truncation), final answer, and tool usage information. +- **FR-003**: System MUST run the ACE learning pipeline (reflect, tag, update, apply) on parsed traces to extract strategies. +- **FR-004**: System MUST persist learned strategies to a skillbook file that survives across runs. +- **FR-005**: System MUST sync learned strategies into the OpenClaw agent's workspace file between clearly defined marker boundaries. +- **FR-006**: System MUST preserve all existing content in the workspace file outside of the marker boundaries during sync. +- **FR-007**: System MUST track which sessions have already been processed to enable incremental learning. +- **FR-008**: System MUST support a reprocess mode that ignores the processed log and reprocesses all sessions. +- **FR-009**: System MUST support a preview mode that parses sessions and reports findings without running the learning pipeline or modifying files. +- **FR-010**: System MUST skip malformed or empty session files gracefully and continue processing remaining files. +- **FR-011**: System MUST report a summary after each run including: sessions discovered, sessions processed, sessions skipped, strategies before and after, and new strategies added. +- **FR-012**: System MUST allow configuration of the OpenClaw home directory, agent identifier, workspace path, and LLM model through environment variables or explicit parameters. +- **FR-013**: System MUST be runnable as a standalone command for one-off use and be schedulable for recurring automated runs (e.g., via cron or task scheduler). + +### Key Entities + +- **Session Transcript**: A record of a single OpenClaw agent session containing the sequence of user messages, agent responses, and tool invocations. Stored as a file on disk in the OpenClaw sessions directory. +- **Trace**: A structured representation of a session transcript containing the original question, agent reasoning chain (including full, untruncated thinking blocks), final answer, tool call summary, and optional feedback. Used as input to the ACE learning pipeline. +- **Skillbook**: A persistent collection of learned strategies. Each strategy has an identifier, topic section, content description, and effectiveness scores (helpful/harmful/neutral). Survives across runs and grows over time. +- **Strategy**: A single learned lesson within the skillbook. Describes a specific technique, pattern, or approach that the agent found useful (or harmful). Includes evidence and justification from the session that produced it. +- **Workspace File**: The file in the OpenClaw agent's workspace where learned strategies are injected for the agent to read on its next session. Contains a marked section that is updated by the sync process while preserving all other content. +- **Processed Log**: A record of which session files have already been analyzed, enabling incremental processing. Prevents redundant reprocessing of historical sessions. + +## Clarifications + +### Session 2026-02-27 + +- Q: Should thinking traces be truncated or filtered from parsed traces? → A: No. Thinking traces must be preserved in full — they are integral to how the agent works and provide essential reasoning context for the learning pipeline. +- Q: Should tool call arguments and results be preserved in full or truncated? → A: Preserve in full. No truncation for tool arguments or tool results. +- Q: Where should trace loading and conversion logic live? → A: A generic `LoadTracesStep` in `ace/steps/` loads raw file contents onto `ctx.trace`. An OpenClaw-specific `OpenClawToTraceStep` in `ace/integrations/openclaw/` converts raw JSONL events into the structured trace dict, preserving chronological order of queries, thinking, and tool uses. The transformation logic will be defined separately. + +## Assumptions + +- OpenClaw stores session transcripts as individual files (one per session) in a predictable directory structure under the OpenClaw home directory. +- OpenClaw reads the workspace file (AGENTS.md) at the start of each session, making it the appropriate injection point for learned strategies. +- OpenClaw does NOT auto-inline markdown links in AGENTS.md — the agent must explicitly read files using its file-reading tools. AGENTS.md instructions must tell the agent to `read` the skillbook file, not just link to it. +- The LLM API key is provided through standard environment variable configuration. +- The default LLM model for reflection and skill extraction follows the project's standard configuration pattern. +- Session transcripts contain structured entries with role (user/assistant) and content fields, with optional tool invocation entries. +- A single run processes all new sessions in one batch — there is no need for streaming or real-time processing of in-progress sessions. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: A developer can go from installation to first learned strategies in under 5 minutes with no more than 3 configuration steps. +- **SC-002**: The system processes 100 session transcripts and produces at least 1 learned strategy per 10 sessions on average. +- **SC-003**: Incremental runs (no new sessions) complete in under 5 seconds without making any LLM calls. +- **SC-004**: After learning, the OpenClaw agent's workspace file contains all current strategies in a format the agent can read and apply on its next session. +- **SC-005**: The system handles 500+ historical sessions without failures or data loss in a single run. +- **SC-006**: 90% of developers can complete the full setup-learn-sync cycle on their first attempt following the documentation. +- **SC-007**: Strategies learned from OpenClaw sessions are cited by the agent in at least 20% of subsequent sessions where a relevant strategy exists. diff --git a/specs/001-openclaw-integration/tasks.md b/specs/001-openclaw-integration/tasks.md new file mode 100644 index 0000000000000000000000000000000000000000..d27a157e98203e34ec914ec323123c01634880e9 --- /dev/null +++ b/specs/001-openclaw-integration/tasks.md @@ -0,0 +1,202 @@ +# Tasks: OpenClaw Integration + +**Input**: Design documents from `/specs/001-openclaw-integration/` +**Prerequisites**: plan.md, spec.md, data-model.md, research.md, contracts/cli.md, quickstart.md + +**Tests**: Included — plan.md specifies test files and CLAUDE.md requires tests for new features (R-009). + +**Organization**: Tasks grouped by user story to enable independent implementation and testing. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3, US4) +- Include exact file paths in descriptions + +--- + +## Phase 1: Setup + +**Purpose**: Create package structure for the new OpenClaw integration + +- [X] T001 Create `ace/integrations/openclaw/` package directory with `__init__.py` + +--- + +## Phase 2: Foundational (Pipeline Steps) + +**Purpose**: Implement the two new pipeline steps that ALL user stories depend on + +**CRITICAL**: No user story work can begin until this phase is complete + +- [X] T002 [P] Implement `LoadTracesStep` in `ace/steps/load_traces.py` — generic step that reads JSONL file at `ctx.sample`, parses lines into `list[dict]`, places on `ctx.trace`; `requires={"sample"}`, `provides={"trace"}`; skip unparseable lines gracefully per FR-010 and data-model.md validation rules +- [X] T003 [P] Implement `OpenClawToTraceStep` (pass-through) in `ace/integrations/openclaw/to_trace.py` — `requires={"trace"}`, `provides={"trace"}`; for now returns `ctx` unchanged (transformation logic deferred per user decision); follow existing ToTrace step pattern from `ClaudeCodeToTrace`/`BrowserToTrace` +- [X] T004 Export `OpenClawToTraceStep` from `ace/integrations/openclaw/__init__.py` and add to `ace/integrations/__init__.py` exports; export `LoadTracesStep` from `ace/steps/__init__.py` + +**Checkpoint**: Pipeline steps ready — user story implementation can now begin + +--- + +## Phase 3: User Story 1 — One-Off Learning from Past Sessions (Priority: P1) MVP + +**Goal**: Developer runs a single command and ACE discovers session transcripts, parses them, runs the learning pipeline, and saves strategies to a persistent skillbook file. + +**Independent Test**: Provide sample JSONL files, run the script, verify strategies extracted and saved to `ace_skillbook.json`. + +**Acceptance**: FR-001, FR-002, FR-003, FR-004, FR-010, FR-011, FR-012, FR-013 + +### Implementation for User Story 1 + +- [X] T005 [US1] Rewrite `examples/openclaw/learn_from_traces.py`: remove broken `parse_session_jsonl()` function; implement `discover_sessions()` that globs `~/.openclaw/agents//sessions/*.jsonl` using env vars `OPENCLAW_HOME`, `OPENCLAW_AGENT_ID` per R-005 and contracts/cli.md; report clear error if directory missing per edge cases +- [X] T006 [US1] Implement pipeline composition in `examples/openclaw/learn_from_traces.py`: for each discovered session, run `LoadTracesStep → OpenClawToTraceStep → learning_tail()` using `TraceAnalyser.from_roles()` with `LiteLLMClient`, `Reflector`, `SkillManager`; configure model via `ACE_MODEL` env var per FR-012 +- [X] T007 [US1] Implement skillbook load/save in `examples/openclaw/learn_from_traces.py`: load from `~/.openclaw/ace_skillbook.json` via `Skillbook.load_from_file()` (create new if missing); save after learning via `Skillbook.save_to_file()` per FR-004 and R-006 +- [X] T008 [US1] Implement summary reporting in `examples/openclaw/learn_from_traces.py` per CLI contract output format: sessions discovered, already processed, new to process, parsed/skipped counts, strategies before/after/new, latest strategy preview per FR-011 +- [X] T009 [US1] Implement CLI entry point with `argparse` in `examples/openclaw/learn_from_traces.py`: `--dry-run` and `--reprocess` flags (wired in later phases), error handling for missing API key and corrupted skillbook per edge cases, `if __name__ == "__main__"` block per FR-013 + +**Checkpoint**: One-off learning fully functional — can discover, parse, learn, and save strategies + +--- + +## Phase 4: User Story 2 — Strategy Sync to Agent Workspace (Priority: P2) + +**Goal**: Learned strategies are injected into the OpenClaw agent's `AGENTS.md` between marker boundaries so the agent reads them on next session. + +**Independent Test**: After learning, verify `AGENTS.md` contains strategies between `` markers with existing content preserved. + +**Acceptance**: FR-005, FR-006 + +### Implementation for User Story 2 + +- [X] T010 [US2] Implement `sync_to_agents_md()` in `examples/openclaw/learn_from_traces.py`: use `wrap_skillbook_context()` to format strategies; write between `` / `` markers per contracts/cli.md AGENTS.md Marker Contract; replace if markers exist, append if not; preserve content outside markers per FR-006; create file if missing; use `OPENCLAW_WORKSPACE` env var +- [X] T011 [US2] Integrate sync into `main()` flow in `examples/openclaw/learn_from_traces.py`: call `sync_to_agents_md()` after skillbook save; skip sync in dry-run mode; report sync path in summary output + +**Checkpoint**: Full learn-and-sync cycle works — strategies extracted and injected into workspace + +--- + +## Phase 5: User Story 3 — Incremental Processing (Priority: P3) + +**Goal**: Only new sessions are processed on subsequent runs; `--reprocess` overrides to reprocess all. + +**Independent Test**: Run twice — second run skips already-processed sessions. Run with `--reprocess` — all sessions reprocessed. + +**Acceptance**: FR-007, FR-008 + +### Implementation for User Story 3 + +- [X] T012 [US3] Implement processed log read/write in `examples/openclaw/learn_from_traces.py`: read/write `~/.openclaw/ace_processed.txt` as newline-delimited sorted session filenames per data-model.md ProcessedLog; filter `discover_sessions()` output to exclude already-processed files per FR-007 +- [X] T013 [US3] Wire `--reprocess` flag in `examples/openclaw/learn_from_traces.py`: when set, ignore processed log and process all discovered sessions per FR-008; update processed log after successful processing regardless of flag + +**Checkpoint**: Incremental processing works — repeat runs skip processed sessions, `--reprocess` overrides + +--- + +## Phase 6: User Story 4 — Dry Run Preview (Priority: P4) + +**Goal**: `--dry-run` parses sessions and reports findings without running the learning pipeline or modifying any files. + +**Independent Test**: Run with `--dry-run`, verify no skillbook/workspace/processed-log files created or modified. + +**Acceptance**: FR-009 + +### Implementation for User Story 4 + +- [X] T014 [US4] Wire `--dry-run` flag in `examples/openclaw/learn_from_traces.py`: when set, discover and parse sessions, display summary of extracted data (session count, trace previews), but skip `TraceAnalyser.run()`, skip `Skillbook.save_to_file()`, skip `sync_to_agents_md()`, skip processed log write per FR-009 + +**Checkpoint**: All 4 user stories independently functional + +--- + +## Phase 7: Polish & Testing + +**Purpose**: Tests, documentation, and cross-cutting validation + +- [X] T015 [P] Unit tests for `LoadTracesStep` in `tests/test_load_traces_step.py`: test JSONL parsing, empty file, missing file, unparseable lines skipped, valid multi-line JSONL; use sample JSONL fixture from `examples/openclaw/b3db607f-*.jsonl` +- [X] T016 [P] Unit tests for `OpenClawToTraceStep` in `tests/test_openclaw.py`: test pass-through behavior, verify requires/provides contract, verify step returns context unchanged; use `MockLLMClient` pattern from existing tests per R-009 +- [X] T017 End-to-end test in `tests/test_openclaw.py`: test full pipeline `LoadTracesStep → OpenClawToTraceStep → learning_tail()` with `MockReflector` and `MockSkillManager`; verify skillbook receives new strategies; use sample JSONL fixture +- [X] T018 Update `examples/openclaw/README.md` with current usage matching quickstart.md and contracts/cli.md + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — start immediately +- **Foundational (Phase 2)**: Depends on Phase 1 — T002/T003 need the package directory from T001 +- **US1 (Phase 3)**: Depends on Phase 2 — needs LoadTracesStep and OpenClawToTraceStep +- **US2 (Phase 4)**: Depends on US1 — sync needs a working learning flow to produce strategies +- **US3 (Phase 5)**: Depends on US1 — incremental processing adds to the base learning flow +- **US4 (Phase 6)**: Depends on US1 — dry-run modifies the base learning flow +- **Polish (Phase 7)**: Depends on Phase 2 (tests for steps) and US4 (all features complete for e2e test) + +### User Story Dependencies + +- **US1 (P1)**: Requires Foundational (Phase 2) — core learning flow +- **US2 (P2)**: Requires US1 — needs working skillbook to sync +- **US3 (P3)**: Requires US1 — adds filtering on top of discovery +- **US4 (P4)**: Requires US1 — adds early-exit branch to main flow +- **US3 and US4 are independent of each other** — can be implemented in either order after US1 + +### Within Each Phase + +- T002 and T003 are parallel (different files) +- T005 → T006 → T007 → T008 → T009 are sequential (same file, building up) +- T010 → T011 are sequential (same file) +- T012 → T013 are sequential (same file) +- T015 and T016 are parallel (different test files) + +### Parallel Opportunities + +- **Phase 2**: T002 (LoadTracesStep) and T003 (OpenClawToTraceStep) — different files +- **Phase 7**: T015 (test_load_traces_step.py) and T016 (test_openclaw.py) — different files +- **Cross-phase**: T015 can start as soon as T002 completes; T016 can start as soon as T003 completes + +--- + +## Parallel Example: Foundational Phase + +```bash +# Launch both pipeline steps in parallel (different files): +Task: "Implement LoadTracesStep in ace/steps/load_traces.py" +Task: "Implement OpenClawToTraceStep in ace/integrations/openclaw/to_trace.py" +``` + +## Parallel Example: Testing Phase + +```bash +# Launch both test files in parallel: +Task: "Unit tests for LoadTracesStep in tests/test_load_traces_step.py" +Task: "Unit tests for OpenClawToTraceStep in tests/test_openclaw.py" +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Setup (T001) +2. Complete Phase 2: Foundational (T002–T004) +3. Complete Phase 3: User Story 1 (T005–T009) +4. **STOP and VALIDATE**: Run script against sample JSONL, verify strategies saved +5. If working → continue to US2–US4 + +### Incremental Delivery + +1. Setup + Foundational → Pipeline steps ready +2. Add US1 → Test with sample JSONL → Core learning works (MVP!) +3. Add US2 → Verify AGENTS.md updated → Full loop closed +4. Add US3 → Run twice, verify incremental → Production-ready +5. Add US4 → Verify dry-run → Developer-friendly +6. Polish → Tests + docs → Ship-ready + +--- + +## Notes + +- T003 (OpenClawToTraceStep) is a **pass-through** for now — transformation logic deferred per user decision +- The existing `learn_from_traces.py` (345 lines) will be **rewritten** starting at T005, not patched incrementally +- Sample JSONL fixture: `examples/openclaw/b3db607f-7ae8-4089-b806-44800e961672.jsonl` +- MockLLMClient, MockReflector, MockSkillManager patterns from `tests/conftest.py` and `tests/test_ace_steps.py` +- All env var defaults per contracts/cli.md: `OPENCLAW_HOME=~/.openclaw`, `OPENCLAW_AGENT_ID=main`, `ACE_MODEL=anthropic/claude-sonnet-4-20250514` diff --git a/specs/002-ace-mcp-server/contracts/tool-schemas.md b/specs/002-ace-mcp-server/contracts/tool-schemas.md new file mode 100644 index 0000000000000000000000000000000000000000..5384f72b95ac9b74aaadde2018baf96a65c70c97 --- /dev/null +++ b/specs/002-ace-mcp-server/contracts/tool-schemas.md @@ -0,0 +1,341 @@ +# MCP Tool Schemas: ACE MCP Server + +**Feature**: `002-ace-mcp-server` +**Date**: 2026-03-02 +**Status**: Draft Contract (MVP) + +This document defines the canonical request/response schemas for MCP tools exposed by ACE. + +## Shared Types + +### SessionConfig + +```json +{ + "type": "object", + "properties": { + "model": { "type": "string", "minLength": 1 }, + "temperature": { "type": "number", "minimum": 0, "maximum": 2 }, + "max_tokens": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false +} +``` + +### ErrorEnvelope + +```json +{ + "type": "object", + "required": ["code", "message"], + "properties": { + "code": { "type": "string" }, + "message": { "type": "string" }, + "details": { "type": ["object", "null"], "additionalProperties": true } + }, + "additionalProperties": false +} +``` + +--- + +## Tool: `ace.ask` + +### Request + +```json +{ + "type": "object", + "required": ["session_id", "question"], + "properties": { + "session_id": { "type": "string", "minLength": 1 }, + "question": { "type": "string", "minLength": 1, "maxLength": 100000 }, + "context": { "type": "string", "default": "" }, + "session_config": { "$ref": "#/definitions/SessionConfig" }, + "metadata": { "type": "object", "additionalProperties": true } + }, + "additionalProperties": false, + "definitions": { + "SessionConfig": { + "type": "object", + "properties": { + "model": { "type": "string", "minLength": 1 }, + "temperature": { "type": "number", "minimum": 0, "maximum": 2 }, + "max_tokens": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + } + } +} +``` + +### Response + +```json +{ + "type": "object", + "required": ["session_id", "answer", "skill_count"], + "properties": { + "session_id": { "type": "string" }, + "answer": { "type": "string" }, + "skill_count": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false +} +``` + +--- + +## Tool: `ace.learn.sample` + +### Request + +```json +{ + "type": "object", + "required": ["session_id", "samples"], + "properties": { + "session_id": { "type": "string", "minLength": 1 }, + "samples": { + "type": "array", + "minItems": 1, + "maxItems": 25, + "items": { + "type": "object", + "required": ["question"], + "properties": { + "question": { "type": "string", "minLength": 1 }, + "context": { "type": "string", "default": "" }, + "ground_truth": { "type": ["string", "null"], "default": null }, + "metadata": { "type": "object", "additionalProperties": true } + }, + "additionalProperties": false + } + }, + "epochs": { "type": "integer", "minimum": 1, "maximum": 20, "default": 1 }, + "session_config": { "$ref": "#/definitions/SessionConfig" } + }, + "additionalProperties": false, + "definitions": { + "SessionConfig": { + "type": "object", + "properties": { + "model": { "type": "string", "minLength": 1 }, + "temperature": { "type": "number", "minimum": 0, "maximum": 2 }, + "max_tokens": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + } + } +} +``` + +### Response + +```json +{ + "type": "object", + "required": ["session_id", "processed", "skill_count_before", "skill_count_after"], + "properties": { + "session_id": { "type": "string" }, + "processed": { "type": "integer", "minimum": 0 }, + "failed": { "type": "integer", "minimum": 0, "default": 0 }, + "skill_count_before": { "type": "integer", "minimum": 0 }, + "skill_count_after": { "type": "integer", "minimum": 0 }, + "new_skill_count": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false +} +``` + +--- + +## Tool: `ace.learn.feedback` + +### Request + +```json +{ + "type": "object", + "required": ["session_id", "question", "answer", "feedback"], + "properties": { + "session_id": { "type": "string", "minLength": 1 }, + "question": { "type": "string", "minLength": 1 }, + "answer": { "type": "string", "minLength": 1 }, + "feedback": { "type": "string", "minLength": 1 }, + "context": { "type": "string", "default": "" }, + "ground_truth": { "type": ["string", "null"], "default": null }, + "session_config": { "$ref": "#/definitions/SessionConfig" } + }, + "additionalProperties": false, + "definitions": { + "SessionConfig": { + "type": "object", + "properties": { + "model": { "type": "string", "minLength": 1 }, + "temperature": { "type": "number", "minimum": 0, "maximum": 2 }, + "max_tokens": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + } + } +} +``` + +### Response + +```json +{ + "type": "object", + "required": ["session_id", "learned", "skill_count_before", "skill_count_after"], + "properties": { + "session_id": { "type": "string" }, + "learned": { "type": "boolean" }, + "skill_count_before": { "type": "integer", "minimum": 0 }, + "skill_count_after": { "type": "integer", "minimum": 0 }, + "new_skill_count": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false +} +``` + +--- + +## Tool: `ace.skillbook.get` + +### Request + +```json +{ + "type": "object", + "required": ["session_id"], + "properties": { + "session_id": { "type": "string", "minLength": 1 }, + "limit": { "type": "integer", "minimum": 1, "maximum": 200, "default": 20 }, + "include_invalid": { "type": "boolean", "default": false } + }, + "additionalProperties": false +} +``` + +### Response + +```json +{ + "type": "object", + "required": ["session_id", "stats", "skills"], + "properties": { + "session_id": { "type": "string" }, + "stats": { "type": "object", "additionalProperties": true }, + "skills": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "content"], + "properties": { + "id": { "type": "string" }, + "content": { "type": "string" }, + "topic": { "type": ["string", "null"] }, + "helpful": { "type": ["integer", "null"] }, + "harmful": { "type": ["integer", "null"] }, + "neutral": { "type": ["integer", "null"] } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": false +} +``` + +--- + +## Tool: `ace.skillbook.save` + +### Request + +```json +{ + "type": "object", + "required": ["session_id", "path"], + "properties": { + "session_id": { "type": "string", "minLength": 1 }, + "path": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false +} +``` + +Path policy: the server resolves the user-provided `path` to a canonical absolute path (following symlinks, resolving `..`) before validation and file I/O. If `ACE_MCP_SKILLBOOK_ROOT` is set, the resolved path MUST be inside that directory; otherwise return `ACE_MCP_VALIDATION_ERROR`. + +### Response + +```json +{ + "type": "object", + "required": ["session_id", "path", "saved_skill_count"], + "properties": { + "session_id": { "type": "string" }, + "path": { "type": "string", "description": "Resolved absolute path where the skillbook was saved." }, + "saved_skill_count": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false +} +``` + +--- + +## Tool: `ace.skillbook.load` + +### Request + +```json +{ + "type": "object", + "required": ["session_id", "path"], + "properties": { + "session_id": { "type": "string", "minLength": 1 }, + "path": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false +} +``` + +Path policy: the server resolves the user-provided `path` to a canonical absolute path (following symlinks, resolving `..`) before validation and file I/O. If `ACE_MCP_SKILLBOOK_ROOT` is set, the resolved path MUST be inside that directory; otherwise return `ACE_MCP_VALIDATION_ERROR`. + +### Response + +```json +{ + "type": "object", + "required": ["session_id", "path", "skill_count"], + "properties": { + "session_id": { "type": "string" }, + "path": { "type": "string", "description": "Resolved absolute path the skillbook was loaded from." }, + "skill_count": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false +} +``` + +--- + +## Safe Mode Policy Matrix + +| Tool | Allowed in `safe_mode=true` | +|------|-----------------------------| +| `ace.ask` | ✅ | +| `ace.skillbook.get` | ✅ | +| `ace.learn.sample` | ❌ (`ACE_MCP_FORBIDDEN_IN_SAFE_MODE`) | +| `ace.learn.feedback` | ❌ (`ACE_MCP_FORBIDDEN_IN_SAFE_MODE`) | +| `ace.skillbook.save` | ❌ (`ACE_MCP_FORBIDDEN_IN_SAFE_MODE`) | +| `ace.skillbook.load` | ❌ (`ACE_MCP_FORBIDDEN_IN_SAFE_MODE`) | + +## Save/Load Policy + +When `allow_save_load=false` (independent of safe mode), `ace.skillbook.save` and `ace.skillbook.load` are blocked with `ACE_MCP_SAVE_LOAD_DISABLED`. + +| Tool | `safe_mode=false`, `allow_save_load=false` | +|------|---------------------------------------------| +| `ace.skillbook.save` | ❌ (`ACE_MCP_SAVE_LOAD_DISABLED`) | +| `ace.skillbook.load` | ❌ (`ACE_MCP_SAVE_LOAD_DISABLED`) | diff --git a/specs/002-ace-mcp-server/spec.md b/specs/002-ace-mcp-server/spec.md new file mode 100644 index 0000000000000000000000000000000000000000..88221484f24c032c8c2eecfb98c8c3a8330ef08c --- /dev/null +++ b/specs/002-ace-mcp-server/spec.md @@ -0,0 +1,270 @@ +# Feature Specification: ACE MCP Server (Optional) + +**Feature Branch**: `002-ace-mcp-server` +**Created**: 2026-03-02 +**Status**: Draft +**Input**: User description: "extend ACE to also work as an MCP server (optional but powerful)" + +## Summary + +Add an optional MCP server mode for ACE using the `ace` architecture. The server exposes ACE capabilities as MCP tools so external MCP clients (IDEs, copilots, orchestrators) can call ACE for inference and learning. + +The feature is **opt-in** via optional dependency extras and a dedicated CLI entrypoint. Existing ACE APIs and integrations remain unchanged when MCP is not enabled. + +## Scope + +### In Scope (MVP) + +- MCP server over stdio transport. +- Tool endpoints for ask/learn/skillbook operations. +- Per-session ACE runner lifecycle with in-memory state. +- Input/output validation with Pydantic models. +- Structured error mapping to MCP tool errors. +- Unit + integration tests for handler behavior and transport startup. +- Docs and runnable example client. + +### Out of Scope (Post-MVP) + +- HTTP/SSE MCP transport. +- Distributed or persistent multi-process session stores. +- AuthN/AuthZ and multi-tenant isolation beyond local process boundaries. +- Rich observability dashboards (basic logs only in MVP). + +## Design Constraints + +- Implement in `ace` integration layer, not in `pipeline/` or `ace/core/` for MVP. +- Preserve backward compatibility: no breaking changes to existing runners. +- Keep MCP dependency optional (no mandatory install-time dependency). +- Use existing ACE runner constructors (`from_model`, `from_roles`) and skillbook persistence APIs. + +## User Scenarios & Testing + +### User Story 1 — Ask Through MCP (P1) + +A client calls `ace.ask` through MCP and gets an answer from a session-scoped ACE instance. + +**Independent test**: start server, call `ace.ask`, assert valid answer payload and session state initialization. + +### User Story 2 — Learn Through MCP (P1) + +A client calls `ace.learn.sample` and `ace.learn.feedback`; ACE updates the skillbook for that session. + +**Independent test**: call learning tools, then `ace.skillbook.get`, assert strategy count changed. + +### User Story 3 — Persist/Restore Skillbook (P2) + +A client saves skillbook to disk and later reloads it into the same/new session. + +**Independent test**: `save` then `load` and assert stable strategy IDs/count. + +### User Story 4 — Safe Optionality (P1) + +Projects without MCP extras continue functioning exactly as before. + +**Independent test**: import and use non-MCP modules without MCP installed. + +## Functional Requirements + +- **FR-001**: System MUST provide a CLI entrypoint to run ACE as an MCP server over stdio. +- **FR-002**: System MUST expose MCP tools: `ace.ask`, `ace.learn.sample`, `ace.learn.feedback`, `ace.skillbook.get`, `ace.skillbook.save`, `ace.skillbook.load`. +- **FR-003**: System MUST validate every tool request and response via typed schemas. +- **FR-004**: System MUST isolate state by `session_id`. +- **FR-005**: System MUST lazily initialize session runners with model/provider config. +- **FR-006**: System MUST support disabling mutating tools (`save/load` and learn tools) in safe mode. +- **FR-007**: System MUST map internal exceptions to stable MCP error codes/messages. +- **FR-008**: System MUST cap request sizes (max samples per call, max payload size) to prevent runaway execution. +- **FR-009**: System MUST remain fully optional via `mcp` extra dependency. +- **FR-010**: System MUST include handler-level success tests for each tool and server startup/registration smoke tests when the `mcp` extra is installed. + +## Non-Functional Requirements + +- **NFR-001**: Tool roundtrip latency for `ace.ask` should be < 2s p50 excluding model latency. +- **NFR-002**: Session operations must be thread-safe within a process. +- **NFR-003**: Server startup failures must emit actionable install/config error text. +- **NFR-004**: Existing test suite behavior remains unchanged when MCP extras are absent. + +## Tool Contract Source of Truth + +Canonical tool schemas are defined in: `specs/002-ace-mcp-server/contracts/tool-schemas.md`. + +## Exact Module Tree (Implementation) + +```text +ace/ + integrations/ + mcp/ + __init__.py + server.py # create_server(), main(), startup wiring + registry.py # session registry + lifecycle (TTL, lazy init) + config.py # MCPServerConfig, limits, safe-mode flags + errors.py # domain errors + MCP error mapping + models.py # Pydantic request/response schemas + handlers.py # tool handler implementations + adapters.py # maps handlers <-> MCP SDK tool registration + +tests/ + test_ace_mcp_models.py # schema validation and serialization + test_ace_mcp_registry.py # session lifecycle and locking + test_ace_mcp_handlers.py # tool behavior with mocked runner + test_ace_mcp_server.py # server startup + tool registration smoke + +docs/ + integrations/ + mcp.md # install, run, tool catalog, examples + +examples/ + ace/ + mcp_client_demo.py # minimal MCP client invoking ace.ask +``` + +## Package and CLI Changes + +- `pyproject.toml` + - Add optional dependency group: + - `mcp = ["mcp>="]` (or official Python MCP SDK package used by maintainers) + - Add script entrypoint: + - `ace-mcp = "ace.integrations.mcp.server:main"` + +## Runtime Architecture + +1. MCP server starts and registers tool definitions from `models.py`. +2. Incoming request is validated into typed request model. +3. `registry.py` returns/create session runner (`ACELiteLLM` by default). +4. `handlers.py` executes operation against runner. +5. Response serialized to typed output model. +6. Exceptions mapped through `errors.py` into stable MCP error responses. + +## Session Model + +- Key: `session_id: str`. +- Value: session object containing runner, creation time, last access time, lock. +- Concurrency: per-session lock around mutating operations. +- Expiry: configurable TTL cleanup (lazy sweep on access in MVP). + +## Configuration Model + +`MCPServerConfig` fields (MVP): + +- `default_model: str = "gpt-4o-mini"` +- `safe_mode: bool = false` +- `max_samples_per_call: int = 25` +- `max_prompt_chars: int = 100_000` +- `session_ttl_seconds: int = 3600` +- `allow_save_load: bool = true` +- `learn_timeout_seconds: int = 300` +- `skillbook_root: str | None = null` +- `log_level: str = "INFO"` + +Environment variable mapping (MVP): + +- `ACE_MCP_DEFAULT_MODEL` +- `ACE_MCP_SAFE_MODE` +- `ACE_MCP_MAX_SAMPLES_PER_CALL` +- `ACE_MCP_MAX_PROMPT_CHARS` +- `ACE_MCP_SESSION_TTL_SECONDS` +- `ACE_MCP_ALLOW_SAVE_LOAD` +- `ACE_MCP_LEARN_TIMEOUT_SECONDS` +- `ACE_MCP_SKILLBOOK_ROOT` + +## Error Taxonomy + +- `ACE_MCP_VALIDATION_ERROR` +- `ACE_MCP_SESSION_NOT_FOUND` +- `ACE_MCP_FORBIDDEN_IN_SAFE_MODE` — mutating tool called with `safe_mode=true` +- `ACE_MCP_SAVE_LOAD_DISABLED` — save/load tool called with `allow_save_load=false` (independent of safe mode) +- `ACE_MCP_PROVIDER_ERROR` +- `ACE_MCP_TIMEOUT` +- `ACE_MCP_INTERNAL_ERROR` + +Each error must include: + +- `code` (stable string) +- `message` (human-readable) +- `details` (optional structured dict) + +## Design Notes + +### `ace.ask` response + +The `applied_skill_ids` field was removed from the `ace.ask` response. The underlying `ACELiteLLM.ask()` does not track which skills were applied during generation, so the field was always empty. Removed to avoid a misleading contract. + +### `ace.learn.feedback` response + +The `learned` field reports `True` when the feedback handler successfully executed the learning path (either direct feedback or trace-based fallback), regardless of whether new skills were created. This avoids false negatives when the reflector modifies existing skills without adding new ones. + +### `ace.learn.feedback` fallback trace + +When no prior `ask()` exists for the session, the handler builds a synthetic trace. The `context` field maps to `context` (not `reasoning`) in the trace dict, since context is background information and reasoning is the agent's chain of thought. + +### MCP learning limitations + +Learning through MCP (`ace.learn.sample`) does not provide a task environment. The pipeline's EvaluateStep uses `ground_truth` comparison only. Environment-based evaluation is not available through MCP in the MVP. + +### Learn timeout + +`ace.learn.sample` and `ace.learn.feedback` wrap their runner calls in `asyncio.wait_for` with `learn_timeout_seconds` (default 300s). When the timeout fires, the handler raises `ACE_MCP_TIMEOUT` instead of `ACE_MCP_INTERNAL_ERROR`. + +### Prompt limit fields + +The per-tool fields included in the `max_prompt_chars` check: + +- `ace.ask`: `question + context` +- `ace.learn.sample`: `question + context` per sample (0-indexed in error messages) +- `ace.learn.feedback`: `question + context + answer + feedback + ground_truth` + +### Save/load path resolution + +`ace.skillbook.save` and `ace.skillbook.load` resolve the user-provided path to an absolute canonical path (via `Path.resolve()`) before validation and before passing it to the runner. The response echoes the **resolved** path, not the raw input. This eliminates TOCTOU races with symlinks or `..` path components. + +## Security & Safety + +- Safe mode blocks mutating operations by policy. +- Path validation for save/load (reject paths outside `ACE_MCP_SKILLBOOK_ROOT` when set). Validation operates on the resolved canonical path. +- Request size limits enforced before runner call. +- No secret values logged in payload dumps. + +## Testing Plan + +### Unit + +- `models.py`: required fields, type coercion, limits. +- `registry.py`: session create/get/delete, TTL expiry, lock semantics. +- `handlers.py`: tool success paths + mapped failures. + +### Integration + +- Boot server with MCP SDK test harness when the `mcp` extra is installed. +- Validate tool registration/startup via smoke tests and cover tool success paths at the handler layer. +- Validate safe mode blocks expected tools. + +### Regression + +- Run existing `ace` tests ensuring no MCP dependency required unless explicitly installed. + +## Delivery Plan (PR Sequence) + +1. **PR-1**: skeleton package + config + models + tests. +2. **PR-2**: session registry + handlers for `ace.ask` and `ace.skillbook.get`. +3. **PR-3**: learning + save/load handlers, error mapping, limits. +4. **PR-4**: server bootstrap + CLI entrypoint + integration tests. +5. **PR-5**: docs + example client + changelog entry. + +## Acceptance Criteria + +- All implemented FR/NFR remain consistent with the optional dependency boundary. +- Tool schemas match contract doc exactly. +- `ace-mcp` starts successfully and serves all MVP tools. +- Existing non-MCP workflows are unaffected. + +## Ready for Merge Checklist + +- [x] Module tree implemented under `ace/integrations/mcp/` +- [x] Tool schemas implemented and contract-aligned (`ace.ask`, `ace.learn.sample`, `ace.learn.feedback`, `ace.skillbook.get/save/load`) +- [x] Optional dependency and CLI entrypoint wired (`mcp` extra, `ace-mcp`) +- [x] Safe mode policy enforced for mutating tools +- [x] Request-size limits enforced (`max_prompt_chars`, `max_samples_per_call`) +- [x] Optional root-bound path validation enforced for save/load (`ACE_MCP_SKILLBOOK_ROOT`) +- [x] MCP-focused tests passing (`test_ace_mcp_models/registry/handlers/server`) +- [x] Optional dependency boundary enforced with actionable install errors +- [x] Docs + example client updated (`docs/integrations/mcp.md`, `examples/ace/mcp_client_demo.py`) +- [x] Changelog updated for this feature diff --git a/test_rr_live.py b/test_rr_live.py new file mode 100644 index 0000000000000000000000000000000000000000..6d1f88eb210cf3f1aa09c2b9a91f8f7d9b0d68f4 --- /dev/null +++ b/test_rr_live.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""Live RR test — run RR against a real benchmark trace and observe behavior. + +Instruments the sandbox to print every code execution and its output, +giving full visibility into how the RR iterates. + +Usage: + uv run python test_rr_live.py + uv run python test_rr_live.py --task 12 + uv run python test_rr_live.py --all-failed +""" + +import argparse +import json +import logging +import os +import sys +import time +from pathlib import Path + +from dotenv import load_dotenv + +load_dotenv() + +from ace.steps.rr_step import RRStep, RRConfig, TraceSandbox +from ace.core.sandbox import ExecutionResult +from ace.core.context import ACEStepContext, SkillbookView +from ace.core.skillbook import Skillbook + +# ── Config ────────────────────────────────────────────────────────────── + +MODEL = os.getenv( + "ACE_MODEL", "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0" +) +TRACES_FILE = Path( + "ace-eval/results/e2e/run_7f757d765ba5/benchmark/traces.json" +) + +# Only show RR-level logs, not every HTTP request +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)-30s | %(message)s", + datefmt="%H:%M:%S", +) +for lib in ( + "LiteLLM", "litellm", "httpx", "httpcore", "urllib3", + "botocore", "boto3", "pydantic_ai", +): + logging.getLogger(lib).setLevel(logging.WARNING) + + +# ── Sandbox instrumentation ───────────────────────────────────────────── + +_orig_execute = TraceSandbox.execute +_iteration_counter = 0 + + +def _instrumented_execute(self, code: str, timeout: float = 30.0) -> ExecutionResult: + global _iteration_counter + _iteration_counter += 1 + n = _iteration_counter + + print(f"\n{'━' * 70}") + print(f" EXECUTE_CODE (iteration {n})") + print(f"{'━' * 70}") + for i, line in enumerate(code.strip().splitlines(), 1): + print(f" {i:3d} │ {line}") + print(f"{'─' * 70}") + + start = time.time() + result = _orig_execute(self, code, timeout) + elapsed = time.time() - start + + if result.stdout: + out = result.stdout.strip() + lines = out.splitlines() + if len(lines) > 40: + for line in lines[:30]: + print(f" out │ {line}") + print(f" out │ ... ({len(lines) - 30} more lines)") + else: + for line in lines: + print(f" out │ {line}") + if result.stderr: + for line in result.stderr.strip().splitlines(): + print(f" err │ {line}") + if result.exception: + print(f" EXC │ {type(result.exception).__name__}: {result.exception}") + if result.final_value is not None: + print(f" FIN │ FINAL() called with keys: {list(result.final_value.keys()) if isinstance(result.final_value, dict) else type(result.final_value).__name__}") + + print(f" [{elapsed:.2f}s]") + return result + + +TraceSandbox.execute = _instrumented_execute + + +# ── Helpers ────────────────────────────────────────────────────────────── + + +def load_traces() -> dict: + with open(TRACES_FILE) as f: + return json.load(f) + + +def get_failed_tasks(data: dict) -> list[tuple[str, dict]]: + failed = [] + for task_id, entry in data.items(): + trial = entry["trials"][0] + if trial["reward"] == 0.0: + failed.append((task_id, trial["trace"])) + return failed + + +def run_rr_on_trace(task_id: str, trace: dict) -> None: + global _iteration_counter + _iteration_counter = 0 + + print(f"\n{'=' * 70}") + print(f" Task {task_id} | reward=0.0 | {trace.get('outcome', '?')}") + print(f"{'=' * 70}") + print(f" Question: {trace['question'][:200]}") + print(f" Feedback: {trace['feedback'][:200]}") + print(f" Answer: {str(trace.get('answer', ''))[:200]}") + print(f" Reasoning: {len(trace.get('reasoning', ''))} chars") + print(f" Messages: {len(trace.get('messages', []))} entries") + print() + + rr = RRStep( + MODEL, + config=RRConfig( + max_requests=20, + max_output_chars=10_000, + ), + ) + + ctx = ACEStepContext( + trace=trace, + skillbook=SkillbookView(Skillbook()), + ) + + start = time.time() + result_ctx = rr(ctx) + elapsed = time.time() - start + + print(f"\n{'=' * 70}") + print(f" RESULT (task {task_id}, {elapsed:.1f}s, {_iteration_counter} code executions)") + print(f"{'=' * 70}") + + if not result_ctx.reflections: + print(" No reflections produced!") + return + + r = result_ctx.reflections[0] + + print(f"\n Reasoning:\n {r.reasoning[:600]}") + print(f"\n Key insight:\n {r.key_insight}") + if r.error_identification: + print(f"\n Error identification:\n {r.error_identification}") + if r.root_cause_analysis: + print(f"\n Root cause:\n {r.root_cause_analysis}") + if r.correct_approach: + print(f"\n Correct approach:\n {r.correct_approach[:400]}") + # Print raw metadata + raw = r.raw or {} + usage = raw.get("usage", {}) + rr_trace = raw.get("rr_trace", {}) + print(f"\n Metadata:") + print(f" Tokens: {usage.get('input_tokens', '?')} in / {usage.get('output_tokens', '?')} out") + print(f" LLM requests: {usage.get('requests', '?')}") + print(f" Tool iterations: {rr_trace.get('total_iterations', '?')}") + print(f" Timed out: {rr_trace.get('timed_out', '?')}") + print() + + +def main(): + parser = argparse.ArgumentParser(description="Live RR test") + parser.add_argument("--task", type=str, help="Specific task ID to analyze") + parser.add_argument( + "--all-failed", action="store_true", help="Run on all failed tasks" + ) + args = parser.parse_args() + + data = load_traces() + failed = get_failed_tasks(data) + print(f"Loaded {len(data)} tasks, {len(failed)} failed") + print(f"Failed task IDs: {[t[0] for t in failed]}") + print(f"Model: {MODEL}") + + if args.task: + if args.task not in data: + print(f"Task {args.task} not found. Available: {list(data.keys())}") + sys.exit(1) + trace = data[args.task]["trials"][0]["trace"] + run_rr_on_trace(args.task, trace) + elif args.all_failed: + for task_id, trace in failed: + run_rr_on_trace(task_id, trace) + else: + if not failed: + print("No failed tasks found!") + sys.exit(1) + task_id, trace = failed[0] + run_rr_on_trace(task_id, trace) + + +if __name__ == "__main__": + main() diff --git a/test_sm_e2e.py b/test_sm_e2e.py new file mode 100644 index 0000000000000000000000000000000000000000..258fcdd0261cb28cf69661d7c202a6fc1070e098 --- /dev/null +++ b/test_sm_e2e.py @@ -0,0 +1,424 @@ +"""Challenging end-to-end test for the agentic SkillManager. + +Runs the full ACE pipeline (Agent → Evaluate → Reflect → Update) over a +math-word-problem benchmark for multiple epochs. Measures whether the +skillbook actually improves the Agent's accuracy and whether the SM keeps +the skillbook hygienic across many mutations. + +Usage:: + + uv run python test_sm_e2e.py + LIVE_E2E_MODEL=bedrock/us.anthropic.claude-sonnet-4-6 uv run python test_sm_e2e.py + LIVE_E2E_EPOCHS=3 uv run python test_sm_e2e.py + +Default: haiku 4.5 on Bedrock, 2 epochs, 15 samples. ~60–90 LLM calls. +""" + +from __future__ import annotations + +import os +import statistics +import sys +import time +from collections import Counter +from typing import Any + +from dotenv import find_dotenv, load_dotenv + +load_dotenv(find_dotenv()) + +from ace import ( + ACE, + Agent, + Reflector, + Sample, + SimpleEnvironment, + SkillManager, + Skillbook, +) +from ace.core.recursive_agent import AgenticConfig +from ace.deduplication.detector import SimilarityDetector +from ace.protocols.deduplication import DeduplicationConfig + +MODEL = os.environ.get( + "LIVE_E2E_MODEL", "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0" +) +EPOCHS = int(os.environ.get("LIVE_E2E_EPOCHS", "2")) + +# Math-word-problem benchmark. Mix of one-step, multi-step, and subtle +# traps (percentage base, unit conversion, order of operations). Small +# enough to run in a few minutes; large enough that single-sample noise +# doesn't dominate the signal. +# Adversarial benchmark — hand-picked problems that frontier small models +# frequently miss (classic traps, subtle word-problem wording, counting that +# looks easy but rewards systematic care). Ground truths are concise so +# SimpleEnvironment's substring match is robust. +SAMPLES: list[Sample] = [ + # Classic "Boy-born-on-Tuesday" conditional-probability twist (without + # the day-of-week qualifier; the twist is recognising the 1/3 answer). + Sample( + question=( + "A family has two children. You learn that at least one of them is " + "a boy. What is the probability that both children are boys? " + "Assume boys and girls are equally likely and independent. " + "Answer as a fraction in lowest terms." + ), + ground_truth="1/3", + ), + # Monty Hall — frequently missed. + Sample( + question=( + "In the Monty Hall problem with three doors (one car, two goats), " + "you pick a door, the host opens a different door revealing a goat, " + "and offers to let you switch. What is the probability of winning " + "the car if you always switch? Answer as a fraction." + ), + ground_truth="2/3", + ), + # Birthday-ish + Sample( + question=( + "In a group of 23 people, what is the probability that at least two " + "share a birthday (ignoring leap years)? Round to 2 decimal places." + ), + ground_truth="0.51", + ), + # Percentage-base trap + Sample( + question=( + "A store raises a $50 item by 20%, then lowers the new price by 20%. " + "What is the final price in dollars?" + ), + ground_truth="48", + ), + # Reverse-percentage — the trap is confusing discount base. + Sample( + question=( + "An item is sold for $63 after a 30% discount. What was the original " + "price in dollars?" + ), + ground_truth="90", + ), + # Compound interest with trap. + Sample( + question=( + "You invest $1000 at 10% annual interest, compounded annually, for " + "3 years. What is the final value in dollars? Round to the nearest " + "whole dollar." + ), + ground_truth="1331", + ), + # Rate word problem — classic "work together" trap. + Sample( + question=( + "Alice can paint a room in 6 hours. Bob can paint the same room in " + "4 hours. How many hours would it take them working together? " + "Answer as a fraction in lowest terms." + ), + ground_truth="12/5", + ), + # Age problem — multi-step algebra with a slightly awkward wording. + Sample( + question=( + "Mary is twice as old as her brother. In 10 years, she will be 1.5 " + "times his age. How old is Mary now?" + ), + ground_truth="20", + ), + # Counting / combinatorics trap. + Sample( + question=( + "How many different ways can the letters of the word 'MISSISSIPPI' " + "be arranged?" + ), + ground_truth="34650", + ), + # Combinatorics — distinct-handshakes. + Sample( + question=( + "Ten people at a party each shake hands exactly once with every " + "other person. How many handshakes occur in total?" + ), + ground_truth="45", + ), + # Number-theory trap: trailing zeros in 100! + Sample( + question="How many trailing zeros are in 100 factorial (100!)?", + ground_truth="24", + ), + # Classic rate problem — mixture. + Sample( + question=( + "How many liters of pure water must be added to 30 liters of a 40% " + "salt solution to dilute it to a 25% salt solution?" + ), + ground_truth="18", + ), + # Rate-distance-time with unit trap. + Sample( + question=( + "A train travels 150 kilometers in 1 hour 15 minutes. What is its " + "speed in kilometers per hour?" + ), + ground_truth="120", + ), + # Averages trap. + Sample( + question=( + "A student's average on four tests is 85. What score on a fifth " + "test would raise her average to 87?" + ), + ground_truth="95", + ), + # Geometry — area of annulus. + Sample( + question=( + "A circular ring (annulus) has an outer radius of 10 and an inner " + "radius of 6. What is its area? Express in terms of pi." + ), + ground_truth="64pi", + ), + # Logic/wording trap — classic "how many X does each sibling have". + Sample( + question=( + "If a brother has as many sisters as brothers, and each of his " + "sisters has twice as many brothers as sisters, how many boys and " + "girls are in the family? Give the total number of children." + ), + ground_truth="7", + ), + # Rate problem with ratio twist. + Sample( + question=( + "If it takes 5 machines 5 minutes to make 5 widgets, how long would " + "it take 100 machines to make 100 widgets? Answer in minutes." + ), + ground_truth="5", + ), + # Lily-pad doubling trap. + Sample( + question=( + "A lily pad doubles in size every day. It takes 48 days to cover a " + "pond. On what day did it cover half the pond?" + ), + ground_truth="47", + ), + # Bat-and-ball cost — CRT classic. + Sample( + question=( + "A bat and a ball cost $1.10 in total. The bat costs $1.00 more " + "than the ball. How much does the ball cost in cents?" + ), + ground_truth="5", + ), + # Geometric — Pythagorean with subtle unit. + Sample( + question=( + "A 13-foot ladder leans against a wall. The bottom is 5 feet from " + "the wall. How high up the wall does the ladder reach, in feet?" + ), + ground_truth="12", + ), +] + + +def _accuracy(results: list[Any]) -> tuple[float, int, int]: + correct = 0 + total = 0 + for r in results: + ctx = getattr(r, "output", None) + if ctx is None or ctx.agent_output is None or ctx.sample is None: + continue + gt = (ctx.sample.ground_truth or "").strip().lower() + ans = (ctx.agent_output.final_answer or "").strip().lower() + if not gt: + continue + if gt in ans: + correct += 1 + total += 1 + return (correct / total if total else 0.0, correct, total) + + +def _operation_histogram(skillbook: Skillbook) -> Counter: + """Count operations implied by current skillbook state. + + We infer from the skills that exist: an ADD was performed for each + active skill. UPDATEs and TAGs aren't visible from the final state + alone — we track them separately via SM outputs.""" + return Counter({"skills_end": len(skillbook.skills())}) + + +def _counter_stats(skillbook: Skillbook) -> dict[str, float]: + skills = skillbook.skills() + if not skills: + return {} + return { + "skills_total": len(skills), + "used_sum": sum(s.used_count for s in skills), + "helpful_sum": sum(s.helpful_count for s in skills), + "harmful_sum": sum(s.harmful_count for s in skills), + "neutral_sum": sum(s.neutral_count for s in skills), + "used_mean": statistics.mean(s.used_count for s in skills), + "helpful_mean": statistics.mean(s.helpful_count for s in skills), + "harmful_mean": statistics.mean(s.harmful_count for s in skills), + } + + +def _near_duplicate_pairs(skillbook: Skillbook, threshold: float = 0.85) -> int: + """Count pairs of active skills with cosine similarity >= threshold.""" + detector = SimilarityDetector(DeduplicationConfig()) + detector.ensure_embeddings(skillbook) + skills = [s for s in skillbook.skills() if s.embedding is not None] + pairs = 0 + for i in range(len(skills)): + for j in range(i + 1, len(skills)): + sim = detector.cosine_similarity(skills[i].embedding, skills[j].embedding) + if sim >= threshold: + pairs += 1 + return pairs + + +def _print_header(msg: str) -> None: + bar = "=" * 72 + print(f"\n{bar}\n{msg}\n{bar}") + + +def _print_skills(skillbook: Skillbook, limit: int = 40) -> None: + for s in skillbook.skills()[:limit]: + counters = f"(u={s.used_count},+{s.helpful_count},-{s.harmful_count},={s.neutral_count})" + content = s.content[:90] + ("…" if len(s.content) > 90 else "") + print(f" [{s.id}] {counters} {content}") + + +def main() -> int: + _print_header(f"E2E SkillManager test — model={MODEL} epochs={EPOCHS} N={len(SAMPLES)}") + + skillbook = Skillbook() + ace = ACE.from_roles( + agent=Agent(MODEL), + reflector=Reflector(MODEL), + skill_manager=SkillManager( + MODEL, config=AgenticConfig(max_requests=12) + ), + environment=SimpleEnvironment(), + skillbook=skillbook, + ) + + per_epoch_accuracy: list[float] = [] + per_epoch_skill_count: list[int] = [] + per_epoch_stats: list[dict[str, float]] = [] + wall_times: list[float] = [] + + for epoch in range(1, EPOCHS + 1): + _print_header(f"Epoch {epoch}/{EPOCHS}") + t0 = time.time() + results = ace.run(SAMPLES, epochs=1) + wall = time.time() - t0 + wall_times.append(wall) + + acc, correct, total = _accuracy(results) + per_epoch_accuracy.append(acc) + per_epoch_skill_count.append(len(skillbook.skills())) + per_epoch_stats.append(_counter_stats(skillbook)) + + print(f" accuracy: {acc:.2%} ({correct}/{total})") + print(f" skillbook: {len(skillbook.skills())} skills") + print(f" wall: {wall:.1f}s") + _print_skills(skillbook, limit=8) + + # Final diagnostics + _print_header("Final skillbook") + _print_skills(skillbook, limit=40) + + _print_header("Hygiene metrics") + dup_pairs = _near_duplicate_pairs(skillbook) + stats = _counter_stats(skillbook) + print(f" near-duplicate pairs (cos>=0.85): {dup_pairs}") + print(f" counter stats: {stats}") + + _print_header("Summary") + print(f" model: {MODEL}") + print(f" samples: {len(SAMPLES)} epochs: {EPOCHS}") + for i, (acc, count, wall) in enumerate( + zip(per_epoch_accuracy, per_epoch_skill_count, wall_times), 1 + ): + print(f" epoch {i}: acc={acc:.2%} skills={count} wall={wall:.1f}s") + + # Pass criteria + print() + checks: list[tuple[str, bool, str]] = [] + + # 1. No regression: final epoch accuracy >= first epoch (allow a small slack for noise) + if len(per_epoch_accuracy) >= 2: + delta = per_epoch_accuracy[-1] - per_epoch_accuracy[0] + noise_slack = 1.0 / len(SAMPLES) # 1-sample worth of noise + no_regression = delta >= -noise_slack + checks.append( + ( + "no accuracy regression vs. epoch 1", + no_regression, + f"Δ = {delta:+.2%} (slack ±{noise_slack:.2%})", + ) + ) + + # 2. Skillbook is bounded — shouldn't blow up past 2 skills per sample processed + max_reasonable_skills = len(SAMPLES) * EPOCHS * 2 + bounded = len(skillbook.skills()) <= max_reasonable_skills + checks.append( + ( + "skillbook size bounded", + bounded, + f"{len(skillbook.skills())} skills / ceiling {max_reasonable_skills}", + ) + ) + + # 3. At least some skills were created — otherwise SM isn't working + any_skills = len(skillbook.skills()) >= 1 + checks.append( + ( + "SM created >=1 skill", + any_skills, + f"final skills = {len(skillbook.skills())}", + ) + ) + + # 4. Near-duplicate rate stays low + dup_rate_ok = len(skillbook.skills()) == 0 or dup_pairs / max(len(skillbook.skills()), 1) < 0.3 + checks.append( + ( + "near-duplicate rate <30% of skill count", + dup_rate_ok, + f"{dup_pairs} pairs / {len(skillbook.skills())} skills", + ) + ) + + # 5. Some skills got used (Agent step bumped used_count) — sanity on injection tracking + if len(skillbook.skills()) >= 1: + any_used = any(s.used_count > 0 for s in skillbook.skills()) + # used_count only ticks on skills that existed *before* the Agent runs, + # so epoch-1 skills will only be used in epoch 2. Only assert when epochs>=2. + if EPOCHS >= 2: + checks.append( + ( + "injected_skill_ids bumped used_count", + any_used, + f"{sum(1 for s in skillbook.skills() if s.used_count > 0)} skills have used_count>0", + ) + ) + + all_pass = all(passed for _, passed, _ in checks) + for name, passed, detail in checks: + mark = "PASS" if passed else "FAIL" + print(f" [{mark}] {name} — {detail}") + + print() + if all_pass: + print("All checks passed.") + return 0 + else: + print("One or more checks failed.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test_sm_live.py b/test_sm_live.py new file mode 100644 index 0000000000000000000000000000000000000000..f7aa48f59b5caeae9085a7a622d4f02765c5baa9 --- /dev/null +++ b/test_sm_live.py @@ -0,0 +1,630 @@ +"""Live SkillManager tests — exercise the agentic tool loop against a real model. + +Each scenario builds a real ``Skillbook``, constructs a real ``ReflectorOutput``, +runs the agentic ``SkillManager``, then asserts what the tools mutated. Nothing +here uses mocks for the LLM. + +Usage:: + + OPENAI_API_KEY=... uv run python test_sm_live.py + +Set ``LIVE_SM_MODEL`` to override the default model. +""" + +from __future__ import annotations + +import os +import sys +import traceback +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from dotenv import find_dotenv, load_dotenv + +load_dotenv(find_dotenv()) + +from ace.core.outputs import ReflectorOutput +from ace.core.recursive_agent import AgenticConfig +from ace.core.skillbook import Skillbook +from ace.implementations.skill_manager import SkillManager + +MODEL = os.environ.get( + "LIVE_SM_MODEL", "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0" +) + +# Bedrock model aliases the user asked us to exercise. Set LIVE_SM_MODELS="" +# to disable the cross-model matrix and use only LIVE_SM_MODEL. +DEFAULT_MODELS = [ + "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0", + "bedrock/us.anthropic.claude-sonnet-4-6", + "bedrock/openai.gpt-oss-120b-1:0", + "bedrock/minimax.minimax-m2.5", +] + + +# ---------------------------------------------------------------------- +# Helpers +# ---------------------------------------------------------------------- + + +@dataclass +class Result: + name: str + passed: bool + detail: str + ops: list[str] + + +def _fmt_op(op: Any) -> str: + bits = [op.type] + if op.skill_id: + bits.append(op.skill_id) + if op.content: + snippet = op.content[:60] + ("…" if len(op.content) > 60 else "") + bits.append(repr(snippet)) + if op.metadata: + bits.append(str(op.metadata)) + return " ".join(bits) + + +def _run_case( + name: str, + *, + skillbook_setup: Callable[[Skillbook], None], + reflection: ReflectorOutput, + injected_skill_ids: tuple[str, ...] = (), + progress: str = "1/1", + question_context: str = "", + config: AgenticConfig | None = None, + assertion: Callable[[Skillbook, list[Any]], str | None] = lambda sb, ops: None, +) -> Result: + sb = Skillbook() + skillbook_setup(sb) + + try: + sm = SkillManager(MODEL, config=config or AgenticConfig(max_requests=15)) + output = sm.update_skills( + reflections=(reflection,), + skillbook=sb, + question_context=question_context, + progress=progress, + injected_skill_ids=injected_skill_ids, + ) + except Exception as e: + return Result( + name=name, + passed=False, + detail=f"SM crashed: {type(e).__name__}: {e}\n{traceback.format_exc()}", + ops=[], + ) + + ops = list(output.update.operations) + error = assertion(sb, ops) + ops_fmt = [_fmt_op(op) for op in ops] + reason_preview = (output.update.reasoning or "")[:180] + detail = ( + f"{error} | reasoning={reason_preview!r}" + if error + else f"reasoning={reason_preview!r}" + ) + return Result( + name=name, + passed=error is None, + detail=detail, + ops=ops_fmt, + ) + + +def _print_result(r: Result) -> None: + mark = "PASS" if r.passed else "FAIL" + print(f"[{mark}] {r.name}") + for op in r.ops: + print(f" · {op}") + # Always show detail on failure so we can spot patterns across models. + if not r.passed or os.environ.get("LIVE_SM_VERBOSE"): + print(f" detail: {r.detail}") + print() + + +# ---------------------------------------------------------------------- +# Scenario builders +# ---------------------------------------------------------------------- + + +def failure_reflection(*, error: str, insight: str, reasoning: str = "") -> ReflectorOutput: + return ReflectorOutput( + reasoning=reasoning or f"Agent got the wrong answer. {error}", + error_identification=error, + root_cause_analysis="Agent applied an incorrect method / missed a step.", + correct_approach="Describe the correct method with a concrete example.", + key_insight=insight, + ) + + +def success_reflection(*, insight: str, reasoning: str = "") -> ReflectorOutput: + return ReflectorOutput( + reasoning=reasoning or "Agent answered correctly using a clean approach.", + error_identification="none", + root_cause_analysis="Clean execution; chosen strategy fit the problem.", + correct_approach="Strategy worked; record so future runs reuse it.", + key_insight=insight, + ) + + +# ---------------------------------------------------------------------- +# Cases +# ---------------------------------------------------------------------- + + +def case_1_empty_sb_failure() -> Result: + """Empty skillbook + failure → expect at least one ADD.""" + refl = failure_reflection( + error="Computed 15*24 as 310 instead of 360 via distributive property.", + insight="When decomposing a product like a*(b+c), verify a*b and a*c before summing.", + reasoning=( + "Agent attempted 15*24 using distributive: 15*(20+4). Wrote 15*20=310. " + "Should be 300. Off-by-ten arithmetic slip." + ), + ) + return _run_case( + "case_1_empty_sb_failure", + skillbook_setup=lambda sb: None, + reflection=refl, + progress="1/10 correct", + question_context="Mental arithmetic", + assertion=lambda sb, ops: ( + None + if any(op.type == "ADD" for op in ops) and len(sb.skills()) >= 1 + else f"expected >=1 ADD and >=1 skill in book; got ops={[op.type for op in ops]}, skills={len(sb.skills())}" + ), + ) + + +def case_2_empty_sb_success() -> Result: + """Empty skillbook + success with a specific insight → expect ADD.""" + refl = success_reflection( + insight=( + "For factual 'capital of X' questions, answer with the capital only, " + "no extra prose." + ), + reasoning=( + "Agent answered 'Paris' to 'Capital of France?'. Clean factual lookup, " + "no reasoning noise. Reusable pattern for factual single-entity questions." + ), + ) + return _run_case( + "case_2_empty_sb_success", + skillbook_setup=lambda sb: None, + reflection=refl, + progress="8/10 correct", + question_context="Factual trivia", + assertion=lambda sb, ops: ( + None + if any(op.type == "ADD" for op in ops) + else f"expected an ADD; got {[op.type for op in ops]}" + ), + ) + + +def case_3_tag_helpful_for_injected() -> Result: + """Injected skill + success → expect a tag_skill(+1) on the injected skill.""" + injected_id = None + + def _setup(sb: Skillbook) -> None: + nonlocal injected_id + s = sb.add_skill( + section="math", + content="Use distributive property for mental multiplication: a*(b+c) = a*b + a*c.", + ) + injected_id = s.id + + refl = success_reflection( + insight="Distributive decomposition works well for 2-digit multiplications.", + reasoning=( + f"Agent used the injected skill [{'INJECTED_ID'}] to solve 25*14 = " + "25*(10+4) = 250+100 = 350. Correct answer. Strategy was directly useful." + ), + ) + + # We can't know injected_id until setup; patch inside the case runner + sb = Skillbook() + _setup(sb) + assert injected_id is not None + refl = ReflectorOutput( + reasoning=( + f"Agent used the injected skill {injected_id} to solve 25*14 = " + "25*(10+4) = 250+100 = 350. Correct answer. Strategy was directly useful." + ), + error_identification="none", + root_cause_analysis="Skill directly contributed to correct decomposition.", + correct_approach="Reuse the distributive strategy for similar 2-digit tasks.", + key_insight="Distributive decomposition works well for 2-digit multiplications.", + ) + + try: + sm = SkillManager(MODEL, config=AgenticConfig(max_requests=15)) + output = sm.update_skills( + reflections=(refl,), + skillbook=sb, + question_context="Mental arithmetic", + progress="9/10 correct", + injected_skill_ids=(injected_id,), + ) + except Exception as e: + return Result( + "case_3_tag_helpful_for_injected", + False, + f"crash: {e}", + [], + ) + + ops = list(output.update.operations) + tagged_helpful = any( + op.type == "TAG" + and op.skill_id == injected_id + and op.metadata.get("delta", 0) >= 1 + for op in ops + ) + skill = sb.get_skill(injected_id) + counter_ok = skill is not None and skill.helpful_count >= 1 + passed = tagged_helpful and counter_ok + return Result( + "case_3_tag_helpful_for_injected", + passed, + f"tagged_helpful={tagged_helpful}, helpful_count={skill.helpful_count if skill else 'MISSING'}", + [_fmt_op(op) for op in ops], + ) + + +def case_4_tag_harmful_for_injected() -> Result: + """Injected skill misled the agent → expect tag_skill(-1) or REMOVE, and likely an UPDATE/ADD with a corrected rule.""" + sb = Skillbook() + bad = sb.add_skill( + section="math", + content="Always add numbers left-to-right without regrouping to save time.", + ) + refl = ReflectorOutput( + reasoning=( + f"Agent followed skill {bad.id} (left-to-right without regrouping) and got 48+37=75 " + "instead of 85. The skill ignored carry. Skill directly caused the error." + ), + error_identification="Ignored carry when adding units column (8+7=15).", + root_cause_analysis=( + f"Injected skill {bad.id} instructed left-to-right without regrouping, which " + "drops carries. Incorrect strategy." + ), + correct_approach=( + "Add columns right-to-left and carry over into the next column when the sum " + "exceeds 9." + ), + key_insight="Multi-digit addition requires carrying over units-column overflow.", + ) + + try: + sm = SkillManager(MODEL, config=AgenticConfig(max_requests=15)) + output = sm.update_skills( + reflections=(refl,), + skillbook=sb, + question_context="Mental arithmetic", + progress="3/10 correct", + injected_skill_ids=(bad.id,), + ) + except Exception as e: + return Result( + "case_4_tag_harmful_for_injected", + False, + f"crash: {e}", + [], + ) + + ops = list(output.update.operations) + tagged_harmful = any( + op.type == "TAG" and op.skill_id == bad.id and op.metadata.get("delta", 0) <= -1 + for op in ops + ) + removed = any(op.type == "REMOVE" and op.skill_id == bad.id for op in ops) + updated = any(op.type == "UPDATE" and op.skill_id == bad.id for op in ops) + skill = sb.get_skill(bad.id) + harmful_count = skill.harmful_count if skill is not None else -1 + passed = tagged_harmful or removed or updated + detail = ( + f"tagged_harmful={tagged_harmful}, removed={removed}, updated={updated}, " + f"harmful_count={harmful_count}" + ) + return Result( + "case_4_tag_harmful_for_injected", + passed, + detail, + [_fmt_op(op) for op in ops], + ) + + +def case_5_dedup_before_add() -> Result: + """Near-duplicate skill already exists → SM should UPDATE or tag, not ADD a paraphrase.""" + sb = Skillbook() + existing = sb.add_skill( + section="math", + content="Use distributive property when multiplying two-digit numbers mentally.", + ) + # Reflection pattern is nearly the same as the existing skill + refl = failure_reflection( + error="Agent didn't decompose 18*25 and made a multi-step arithmetic error.", + insight=( + "Decompose two-digit multiplications with distributive property rather than " + "computing directly: avoids long arithmetic mistakes." + ), + reasoning=( + "Pattern: agents repeatedly make errors computing 2-digit multiplications " + "directly. Decomposition prevents this." + ), + ) + try: + sm = SkillManager(MODEL, config=AgenticConfig(max_requests=15)) + output = sm.update_skills( + reflections=(refl,), + skillbook=sb, + question_context="Mental arithmetic", + progress="4/10 correct", + injected_skill_ids=(), + ) + except Exception as e: + return Result("case_5_dedup_before_add", False, f"crash: {e}", []) + + ops = list(output.update.operations) + added = [op for op in ops if op.type == "ADD"] + updated_existing = any( + op.type == "UPDATE" and op.skill_id == existing.id for op in ops + ) + # Pass if either (a) no ADD was created (dedup worked), or (b) the existing skill was UPDATEd + passed = (len(added) == 0) or updated_existing + detail = ( + f"added={len(added)}, updated_existing={updated_existing}, " + f"final_skills={len(sb.skills())}" + ) + return Result( + "case_5_dedup_before_add", + passed, + detail, + [_fmt_op(op) for op in ops], + ) + + +def case_6_remove_harmful_threshold() -> Result: + """Skill with harmful_count=2 + new harmful evidence → SM should REMOVE or tag again to push to 3.""" + sb = Skillbook() + bad = sb.add_skill( + section="api", + content="Always send requests without retry logic to keep latency low.", + ) + # Pre-seed harmful_count=2 to simulate prior observations + bad.harmful_count = 2 + + refl = ReflectorOutput( + reasoning=( + f"Skill {bad.id} caused a third transient-failure incident — request dropped on a " + "flaky network path because retries were disabled." + ), + error_identification=( + "Request dropped on transient network error; no retry, user saw a hard failure." + ), + root_cause_analysis=( + f"Skill {bad.id} forbids retries even for transient errors — bad default for " + "unreliable links." + ), + correct_approach=( + "Retry transient errors (timeouts, 5xx, connection reset) with exponential " + "backoff up to 3 attempts; only skip retry on idempotent-violating verbs." + ), + key_insight="Never disable retries unconditionally; distinguish transient vs terminal.", + ) + + try: + sm = SkillManager(MODEL, config=AgenticConfig(max_requests=15)) + output = sm.update_skills( + reflections=(refl,), + skillbook=sb, + question_context="API reliability", + progress="ongoing", + injected_skill_ids=(bad.id,), + ) + except Exception as e: + return Result("case_6_remove_harmful_threshold", False, f"crash: {e}", []) + + ops = list(output.update.operations) + removed = any(op.type == "REMOVE" and op.skill_id == bad.id for op in ops) + bumped_to_three = any( + op.type == "TAG" and op.skill_id == bad.id and op.metadata.get("delta", 0) <= -1 + for op in ops + ) + skill = sb.get_skill(bad.id) + final_harmful = skill.harmful_count if skill is not None else "(removed)" + passed = removed or bumped_to_three + detail = f"removed={removed}, bumped_harmful={bumped_to_three}, final_harmful_count={final_harmful}" + return Result( + "case_6_remove_harmful_threshold", + passed, + detail, + [_fmt_op(op) for op in ops], + ) + + +def case_7_max_requests_one_shot() -> Result: + """max_requests=1 degrades to one-shot — still produces a valid audit even if no mutations.""" + refl = failure_reflection( + error="Minor off-by-one in range loop.", + insight="When iterating [a, b], confirm whether b is inclusive before coding the loop.", + ) + + try: + sm = SkillManager(MODEL, config=AgenticConfig(max_requests=1)) + output = sm.update_skills( + reflections=(refl,), + skillbook=Skillbook(), + question_context="Python coding", + progress="1/1", + injected_skill_ids=(), + ) + except Exception as e: + return Result("case_7_max_requests_one_shot", False, f"crash: {e}", []) + + # Either: SM produced ops (succeeded in one shot) or returned an empty audit (budget exhausted gracefully). + detail = ( + f"reasoning={output.update.reasoning[:120]!r}, " + f"ops={len(output.update.operations)}, timeout={output.raw.get('timeout', False)}" + ) + return Result( + "case_7_max_requests_one_shot", + True, # pass as long as it doesn't crash + detail, + [_fmt_op(op) for op in output.update.operations], + ) + + +def case_8_batch_reflections() -> Result: + """Two reflections in one call — SM should process both.""" + sb = Skillbook() + r1 = failure_reflection( + error="Division by zero not guarded.", + insight="Always guard against zero divisor before dividing.", + ) + r2 = success_reflection( + insight=( + "Prefer collections.Counter over manual dict-increment for tallying " + "frequencies — avoids KeyError paths." + ), + ) + try: + sm = SkillManager(MODEL, config=AgenticConfig(max_requests=20)) + output = sm.update_skills( + reflections=(r1, r2), + skillbook=sb, + question_context="Python coding basics", + progress="5/10 correct", + injected_skill_ids=(), + ) + except Exception as e: + return Result("case_8_batch_reflections", False, f"crash: {e}", []) + + ops = list(output.update.operations) + added = [op for op in ops if op.type == "ADD"] + passed = len(added) >= 1 + detail = f"adds={len(added)}, total_ops={len(ops)}, final_skills={len(sb.skills())}" + return Result( + "case_8_batch_reflections", + passed, + detail, + [_fmt_op(op) for op in ops], + ) + + +def case_9_vague_reflection_no_op() -> Result: + """Purely meta-commentary reflection → SM should produce no ADD (or degrade gracefully).""" + refl = ReflectorOutput( + reasoning="Agent should be more careful and think about things deeply.", + error_identification="Not careful enough.", + root_cause_analysis="Did not consider options.", + correct_approach="Be careful. Remember to think about things.", + key_insight="Consider carefully.", + ) + try: + sm = SkillManager(MODEL, config=AgenticConfig(max_requests=15)) + output = sm.update_skills( + reflections=(refl,), + skillbook=Skillbook(), + question_context="General", + progress="1/1", + injected_skill_ids=(), + ) + except Exception as e: + return Result("case_9_vague_reflection_no_op", False, f"crash: {e}", []) + + ops = list(output.update.operations) + adds = [op for op in ops if op.type == "ADD"] + # Pass if the SM declined to add vague skills. If it did ADD, inspect whether content is sharp — allow one if it rewrote it. + detail = f"adds={len(adds)}, total_ops={len(ops)}, reasoning={output.update.reasoning[:140]!r}" + passed = len(adds) == 0 + return Result("case_9_vague_reflection_no_op", passed, detail, [_fmt_op(op) for op in ops]) + + +# ---------------------------------------------------------------------- +# Main +# ---------------------------------------------------------------------- + + +CASES: list[Callable[[], Result]] = [ + case_1_empty_sb_failure, + case_2_empty_sb_success, + case_3_tag_helpful_for_injected, + case_4_tag_harmful_for_injected, + case_5_dedup_before_add, + case_6_remove_harmful_threshold, + case_7_max_requests_one_shot, + case_8_batch_reflections, + case_9_vague_reflection_no_op, +] + + +def main() -> int: + global MODEL + models_env = os.environ.get("LIVE_SM_MODELS") + if models_env is not None: + models = [m.strip() for m in models_env.split(",") if m.strip()] + elif os.environ.get("LIVE_SM_MODEL"): + models = [MODEL] + else: + models = DEFAULT_MODELS + + matrix: dict[str, list[Result]] = {} + + for model_id in models: + MODEL = model_id + short = model_id.split("/")[-1] + print(f"\n{'=' * 72}") + print(f"Model: {short}") + print("=" * 72 + "\n") + results: list[Result] = [] + for case in CASES: + try: + r = case() + except Exception as e: + r = Result( + case.__name__, + False, + f"runner crash: {e}\n{traceback.format_exc()}", + [], + ) + results.append(r) + _print_result(r) + matrix[short] = results + + # Summary matrix + print("\n" + "=" * 72) + print("SUMMARY") + print("=" * 72) + case_names = [c.__name__ for c in CASES] + model_names = list(matrix.keys()) + width = max((len(nm) for nm in case_names), default=10) + print(f"{'case'.ljust(width)} " + " ".join(f"{m[:22]:<22}" for m in model_names)) + for i, case_name in enumerate(case_names): + row = [case_name.ljust(width)] + for m in model_names: + r = matrix[m][i] + row.append(("PASS" if r.passed else "FAIL").ljust(22)) + print(" ".join(row)) + + # Totals + print() + for m in model_names: + p = sum(1 for r in matrix[m] if r.passed) + print(f" {m[:50]:<50} {p}/{len(CASES)} passed") + total_passed = sum( + sum(1 for r in matrix[m] if r.passed) for m in model_names + ) + total = len(CASES) * len(model_names) + print(f"\nTotal: {total_passed}/{total} across {len(model_names)} model(s)") + return 0 if total_passed == total else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test_sm_tau_retail.py b/test_sm_tau_retail.py new file mode 100644 index 0000000000000000000000000000000000000000..750f4ec6891944bfcf5fcace805c94dd8ec0ed64 --- /dev/null +++ b/test_sm_tau_retail.py @@ -0,0 +1,408 @@ +"""E2E tau-bench retail test with logfire tracing. + +Phases: + 1. Baseline: run N retail tasks with no skillbook -> collect traces, rewards. + 2. Learn: feed traces through the new agentic SkillManager -> build skillbook. + 3. Replay: run the same N tasks with the trained skillbook -> new rewards. + +The TauBenchRunner already emits rich logfire spans ("tau task run", +"tau task trace", "tau task outcome"). PydanticAI is auto-instrumented by +configure_logfire(), so every SkillManager tool call shows up in logfire +as a span. + +Usage:: + + uv run python test_sm_tau_retail.py + +Env: + LOGFIRE_TOKEN — write token (instrumented runs) + LOGFIRE_READ_TOKEN — read token (used by the script to fetch + spans for verification after the run) + AWS_BEARER_TOKEN_BEDROCK — Bedrock auth for haiku-4.5 + OPENAI_API_KEY — for tau-bench's mandatory gpt-4.1 user sim +""" + +from __future__ import annotations + +import logging +import os +import sys +import time +from types import MappingProxyType +from typing import Any + +from dotenv import find_dotenv, load_dotenv + +load_dotenv(find_dotenv()) + +# Tau2 hardcodes gpt-4.1 (via OpenAI) for the NL-assertion judge, the +# env interface, etc. Redirect to a Bedrock-hosted model before any +# tau2 modules import these constants so the Bedrock-only rule holds. +_BEDROCK_JUDGE = "bedrock/openai.gpt-oss-120b-1:0" +import tau2.config as _tau2_config # noqa: E402 +import tau2.evaluator.evaluator_nl_assertions as _nl_mod # noqa: E402 + +for _mod in (_tau2_config, _nl_mod): + for _attr in ( + "DEFAULT_LLM_AGENT", + "DEFAULT_LLM_USER", + "DEFAULT_LLM_NL_ASSERTIONS", + "DEFAULT_LLM_ENV_INTERFACE", + ): + if hasattr(_mod, _attr): + setattr(_mod, _attr, _BEDROCK_JUDGE) + +# Retail tasks' reward_basis includes NL_ASSERTION. tau2.run.run_task's +# default evaluation_type is ALL (no NL eval), which raises. Force +# ALL_WITH_NL_ASSERTIONS so the judge actually runs and a numeric reward +# is produced. +import tau2.run as _tau2_run # noqa: E402 +from tau2.evaluator.evaluator import EvaluationType as _EvalType # noqa: E402 + +_original_run_task = _tau2_run.run_task + + +def _run_task_with_nl(*args, **kwargs): + kwargs.setdefault("evaluation_type", _EvalType.ALL_WITH_NL_ASSERTIONS) + return _original_run_task(*args, **kwargs) + + +_tau2_run.run_task = _run_task_with_nl + +# Configure logfire BEFORE any pydantic_ai agents are built so +# instrumentation attaches cleanly. +from ace.observability import configure_logfire + +_LOGFIRE_OK = configure_logfire() + +import logfire # noqa: E402 + +RUN_TAG = f"sm_retail_e2e_{int(time.time())}" +logfire.info("test_sm_tau_retail.start", run_tag=RUN_TAG) + +# Emit a root span so we can query logfire for everything this run emitted. +_root_span = logfire.span("sm_retail_e2e_run", run_tag=RUN_TAG) +_root_span.__enter__() + +from ace.core.context import ACEStepContext # noqa: E402 +from ace.core.recursive_agent import AgenticConfig # noqa: E402 +from ace.core.skillbook import Skillbook # noqa: E402 +from ace.implementations.rr.config import RecursiveConfig # noqa: E402 +from ace.implementations.skill_manager import SkillManager # noqa: E402 +from ace.steps.rr_step import RRStep # noqa: E402 +from ace.steps.update import UpdateStep # noqa: E402 +from ace_eval.e2e.benchmarks.tau_bench import TauBenchRunner # noqa: E402 + +AGENT_MODEL = "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0" +SM_MODEL = "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0" +# Bedrock-only: user sim uses gpt-oss-120b on Bedrock instead of the +# tau-bench-canonical OpenAI gpt-4.1. Expect slightly lower rewards per +# tau-bench's guidance; acceptable for validating the SM loop. +USER_MODEL = "bedrock/openai.gpt-oss-120b-1:0" +TASK_INDICES = (0, 1, 2) # small slice for speed +MAX_STEPS = 50 + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", +) +# quieten noisy loggers +for name in ( + "httpx", + "LiteLLM", + "litellm", + "tau2", + "ace.core.recursive_agent", + "pipeline", +): + logging.getLogger(name).setLevel(logging.WARNING) +log = logging.getLogger("sm.tau_retail") + + +def _run_phase( + runner: TauBenchRunner, + task_indices: tuple[int, ...], + *, + phase: str, + skillbook_prompt: str | None, +) -> list[dict[str, Any]]: + """Run each task, return list of outcome dicts.""" + log.info("phase=%s indices=%s skillbook=%s", phase, task_indices, bool(skillbook_prompt)) + results = [] + with logfire.span( + "phase", phase=phase, run_tag=RUN_TAG, task_count=len(task_indices) + ): + for idx in task_indices: + out = runner.run_task( + idx, + skillbook_prompt=skillbook_prompt, + run_phase=phase, + trial=0, + ) + log.info( + " task %d: reward=%.2f outcome=%s wall=%.1fs", + idx, + out.reward, + out.outcome.value, + out.wall_clock_seconds or 0.0, + ) + results.append( + { + "task_index": idx, + "reward": out.reward, + "outcome": out.outcome.value, + "trace": dict(out.trace) if out.trace else None, + "wall": out.wall_clock_seconds or 0.0, + "error": out.error, + } + ) + return results + + +def _feed_trace_to_pipeline( + trace: dict[str, Any], + *, + reflect_step: RRStep, + update_step: UpdateStep, + skillbook: Skillbook, +) -> tuple[Any, Any]: + """Run RRStep → UpdateStep on a single trace.""" + from ace.core.context import SkillbookView + + ctx = ACEStepContext( + sample=None, + skillbook=SkillbookView(skillbook), + trace=MappingProxyType(trace), + injected_skill_ids=(), + ) + ctx1 = reflect_step(ctx) + ctx2 = update_step(ctx1) + return ctx1.reflections, ctx2.skill_manager_output + + +def _skill_summary(skillbook: Skillbook) -> str: + lines = [f"{len(skillbook.skills())} skills total:"] + for s in skillbook.skills(): + counters = f"u={s.used_count},+{s.helpful_count},-{s.harmful_count},={s.neutral_count}" + snippet = s.content[:100] + ("…" if len(s.content) > 100 else "") + lines.append(f" [{s.id}] ({counters}) {snippet}") + return "\n".join(lines) + + +def _fetch_logfire_spans(run_tag: str) -> dict[str, Any] | None: + """Pull back spans for this run via the Logfire read API. + + Returns a dict with counts and a few representative span names, or + None if read-token not configured / HTTP fails. + """ + token = os.environ.get("LOGFIRE_READ_TOKEN") + if not token: + log.warning("LOGFIRE_READ_TOKEN not set; skipping verification") + return None + + import httpx + + try: + # Give logfire a few seconds to flush + logfire.force_flush() + except Exception: + pass + + # Logfire read API: GET /v1/query with ?sql=... and Bearer auth. + # We tag only our top-level spans with run_tag. Broader view: recent + # spans that likely belong to this run — SM tool names + pydantic-ai + # chat/agent-run spans — so we can see whether SM tools actually fired. + url = "https://logfire-us.pydantic.dev/v1/query" + q = ( + "SELECT span_name, attributes " + "FROM records " + f"WHERE (attributes->>'run_tag' = '{run_tag}' " + " OR span_name IN ('rr.session','add_skill','update_skill','remove_skill'," + " 'tag_skill','search_skills','read_skill','execute_code'," + " 'agent run','chat','tau task run','tau task outcome')) " + " AND start_timestamp > now() - INTERVAL '30 minutes' " + "ORDER BY start_timestamp DESC LIMIT 500" + ) + r = None + for method in ("GET", "POST"): + try: + if method == "GET": + r = httpx.get( + url, + params={"sql": q}, + headers={"Authorization": f"Bearer {token}"}, + timeout=30.0, + ) + else: + r = httpx.post( + url, + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/sql", + }, + content=q, + timeout=30.0, + ) + if r.status_code == 200: + break + except Exception as e: + log.warning("logfire query (%s) crashed: %s", method, e) + continue + + if r is None or r.status_code != 200: + log.warning( + "logfire query failed: status=%s body=%s", + r.status_code if r is not None else "none", + (r.text[:400] if r is not None else ""), + ) + return None + + try: + data = r.json() + except Exception: + log.warning("logfire response parse failed; body=%s", r.text[:400]) + return None + + # Logfire returns column-oriented arrow-like payloads: {columns: [{name,values},...]} + name_counter: dict[str, int] = {} + total = 0 + if isinstance(data, dict) and isinstance(data.get("columns"), list): + span_col = next( + (c for c in data["columns"] if c.get("name") == "span_name"), None + ) + if span_col: + for name in span_col.get("values", []): + if isinstance(name, str): + name_counter[name] = name_counter.get(name, 0) + 1 + total += 1 + elif isinstance(data, list): + for row in data: + if isinstance(row, dict): + name = row.get("span_name") or row.get("name") + if isinstance(name, str): + name_counter[name] = name_counter.get(name, 0) + 1 + total += 1 + + return {"total": total, "span_counts": name_counter} + + +def main() -> int: + log.info("logfire=%s run_tag=%s", _LOGFIRE_OK, RUN_TAG) + log.info("agent=%s sm=%s user=%s", AGENT_MODEL, SM_MODEL, USER_MODEL) + + # --- Build roles & skillbook + skillbook = Skillbook() + # RR handles long multi-turn traces via execute_code; simple Reflector + # collapses them into a single LLM turn and gives up. + reflect_step = RRStep( + SM_MODEL, + config=RecursiveConfig(max_requests=15, max_tokens=200_000), + ) + skill_manager = SkillManager( + SM_MODEL, + config=AgenticConfig(max_requests=15), + ) + update_step = UpdateStep(skill_manager, skillbook) + + # --- Build tau runner + runner = TauBenchRunner( + domain="retail", + agent_model=AGENT_MODEL, + user_model=USER_MODEL, + user_strategy="llm", + max_num_steps=MAX_STEPS, + seed=300, + ) + log.info( + "retail total_tasks=%s picked=%s", runner.total_tasks, TASK_INDICES + ) + + # --- Phase 1: baseline + baseline = _run_phase(runner, TASK_INDICES, phase="baseline", skillbook_prompt=None) + baseline_reward = sum(r["reward"] for r in baseline) / len(baseline) + log.info("baseline mean reward: %.2f", baseline_reward) + + # --- Phase 2: learn from baseline traces + log.info("--- learning phase ---") + for b in baseline: + if b["trace"] is None: + continue + with logfire.span( + "learn_from_trace", + run_tag=RUN_TAG, + task_index=b["task_index"], + reward=b["reward"], + ): + reflections, sm_out = _feed_trace_to_pipeline( + b["trace"], + reflect_step=reflect_step, + update_step=update_step, + skillbook=skillbook, + ) + log.info( + " task %d: reflection key_insight=%r ops=%d", + b["task_index"], + (reflections[0].key_insight if reflections else "")[:120], + len(sm_out.operations) if sm_out else 0, + ) + + log.info("--- skillbook after learning ---\n%s", _skill_summary(skillbook)) + + # --- Phase 3: replay with trained skillbook + sb_prompt = skillbook.as_prompt() + log.info("skillbook prompt bytes: %d", len(sb_prompt)) + if not sb_prompt: + log.warning("empty skillbook — skipping replay phase") + replay_reward = None + replay = [] + else: + replay = _run_phase( + runner, TASK_INDICES, phase="replay", skillbook_prompt=sb_prompt + ) + replay_reward = sum(r["reward"] for r in replay) / len(replay) + log.info("replay mean reward: %.2f", replay_reward) + + # --- Verify: pull back logfire spans + log.info("--- verifying logfire spans ---") + span_report = _fetch_logfire_spans(RUN_TAG) + if span_report is None: + log.info("no logfire verification performed") + else: + log.info("logfire spans retrieved: %s", span_report) + + # --- Summary + print("\n" + "=" * 72) + print("SUMMARY") + print("=" * 72) + print(f"run_tag: {RUN_TAG}") + print(f"tasks: {TASK_INDICES}") + print(f"baseline reward: {baseline_reward:.2f} ({[r['reward'] for r in baseline]})") + if replay_reward is not None: + print(f"replay reward: {replay_reward:.2f} ({[r['reward'] for r in replay]})") + print(f"delta: {replay_reward - baseline_reward:+.2f}") + print(f"skills created: {len(skillbook.skills())}") + print(f"counters (sum): used={sum(s.used_count for s in skillbook.skills())} " + f"helpful={sum(s.helpful_count for s in skillbook.skills())} " + f"harmful={sum(s.harmful_count for s in skillbook.skills())} " + f"neutral={sum(s.neutral_count for s in skillbook.skills())}") + if span_report: + print(f"logfire spans: {span_report.get('total','?')} records") + # top 10 span names + counts = span_report.get("span_counts", {}) or {} + top = sorted(counts.items(), key=lambda kv: -kv[1])[:10] + for n, c in top: + print(f" {c:4d} {n}") + + return 0 + + +if __name__ == "__main__": + try: + rc = main() + finally: + try: + _root_span.__exit__(None, None, None) + logfire.force_flush() + except Exception: + pass + sys.exit(rc) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..6d4421081c2c094f9daed6b4c6d06c1bc1c82b57 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,31 @@ +"""Shared pytest fixtures for test suite.""" + +import pytest + + +@pytest.fixture(autouse=True) +def _suppress_opik(monkeypatch): + """Disable Opik connections during tests to avoid connection noise.""" + monkeypatch.setenv("OPIK_DISABLED", "true") + + +# Test markers configuration +pytest_configure_done = False + + +def pytest_configure(config): + """Register custom markers.""" + global pytest_configure_done + if not pytest_configure_done: + config.addinivalue_line( + "markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')" + ) + config.addinivalue_line( + "markers", "integration: marks tests as integration tests" + ) + config.addinivalue_line("markers", "unit: marks tests as unit tests") + config.addinivalue_line( + "markers", + "requires_api: marks tests requiring external API keys (skipped in CI)", + ) + pytest_configure_done = True diff --git a/tests/pipeline_engine/__init__.py b/tests/pipeline_engine/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/pipeline_engine/conftest.py b/tests/pipeline_engine/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..843b099f801ee17da0a9dade2fc901c065017666 --- /dev/null +++ b/tests/pipeline_engine/conftest.py @@ -0,0 +1,205 @@ +"""Shared fixtures and reusable dummy steps for pipeline engine tests. + +No ACE imports — every step here is a generic dummy that only uses the +pipeline primitives (StepContext, StepProtocol). +""" + +from __future__ import annotations + +import asyncio +import threading +import time +from types import MappingProxyType + +import pytest + +from pipeline import StepContext + +# --------------------------------------------------------------------------- +# Reusable dummy step classes (no ACE knowledge) +# --------------------------------------------------------------------------- + + +class Noop: + """Pass-through step — does not change context.""" + + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx + + +class SetA: + """Writes metadata['a'] = 1. No requirements.""" + + requires = frozenset() + provides = frozenset({"a"}) + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "a": 1})) + + +class SetB: + """Reads 'a', writes metadata['b'] = metadata['a'] + 1.""" + + requires = frozenset({"a"}) + provides = frozenset({"b"}) + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "b": ctx.metadata["a"] + 1}) + ) + + +class SetC: + """Reads 'b', writes metadata['c'] = metadata['b'] * 2.""" + + requires = frozenset({"b"}) + provides = frozenset({"c"}) + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "c": ctx.metadata["b"] * 2}) + ) + + +class Boom: + """Always raises RuntimeError.""" + + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + raise RuntimeError("boom") + + +class Slow: + """Sleeps for *delay* seconds then sets metadata['done'] = True.""" + + requires = frozenset() + provides = frozenset({"done"}) + + def __init__(self, delay: float = 0.05): + self.delay = delay + + def __call__(self, ctx: StepContext) -> StepContext: + time.sleep(self.delay) + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "done": True})) + + +class AsyncStep: + """Async step — sets metadata['async'] = True.""" + + requires = frozenset() + provides = frozenset({"async_done"}) + + async def __call__(self, ctx: StepContext) -> StepContext: + await asyncio.sleep(0) # yield to event loop + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "async_done": True}) + ) + + +class Recorder: + """Records every ctx it receives via call_log (thread-safe).""" + + requires = frozenset() + provides = frozenset() + + def __init__(self): + self.call_log: list[StepContext] = [] + self._lock = threading.Lock() + + def __call__(self, ctx: StepContext) -> StepContext: + with self._lock: + self.call_log.append(ctx) + return ctx + + +class BoundaryStep: + """Foreground step that marks the async_boundary handoff.""" + + requires = frozenset() + provides = frozenset({"bg_result"}) + async_boundary = True + max_workers = 2 + + def __call__(self, ctx: StepContext) -> StepContext: + time.sleep(0.01) # simulate background work + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "bg_result": True}) + ) + + +class SlowBoundaryStep: + """Slow boundary step for timeout testing.""" + + requires = frozenset() + provides = frozenset({"slow_bg"}) + async_boundary = True + max_workers = 1 + + def __call__(self, ctx: StepContext) -> StepContext: + time.sleep(2.0) # intentionally slow + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "slow_bg": True})) + + +class SerialStep: + """Background step that must serialize (max_workers=1). Appends to a shared log.""" + + requires = frozenset() + provides = frozenset({"serial_done"}) + max_workers = 1 + _log: list[str] = [] + _log_lock = threading.Lock() + + def __call__(self, ctx: StepContext) -> StepContext: + with self._log_lock: + SerialStep._log.append(f"start-{ctx.sample}") + time.sleep(0.02) # ensure ordering is visible if concurrent + SerialStep._log.append(f"end-{ctx.sample}") + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "serial_done": True}) + ) + + +# --------------------------------------------------------------------------- +# Pytest fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def noop(): + return Noop() + + +@pytest.fixture +def set_a(): + return SetA() + + +@pytest.fixture +def set_b(): + return SetB() + + +@pytest.fixture +def set_c(): + return SetC() + + +@pytest.fixture +def boom(): + return Boom() + + +@pytest.fixture +def recorder(): + return Recorder() + + +@pytest.fixture +def base_ctx(): + """A minimal StepContext with sample='test'.""" + return StepContext(sample="test") diff --git a/tests/pipeline_engine/test_branch.py b/tests/pipeline_engine/test_branch.py new file mode 100644 index 0000000000000000000000000000000000000000..39f5227bf12f879533fadccc5da3c331e73e702c --- /dev/null +++ b/tests/pipeline_engine/test_branch.py @@ -0,0 +1,1284 @@ +"""Comprehensive unit tests for Branch and MergeStrategy. + +Sections +-------- +1. TestBranchConstruction — creation, contract inference, merge selection +2. TestBranchSyncRaiseOnConflict — RAISE_ON_CONFLICT semantics +3. TestBranchSyncLastWriteWins — LAST_WRITE_WINS semantics +4. TestBranchSyncNamespaced — NAMESPACED semantics +5. TestBranchSyncCustomMerge — custom callable merge +6. TestBranchSyncFailures — all-branches-run / error collection +7. TestBranchSyncImmutability — frozen context shared safely +8. TestBranchAsyncParity — async path mirrors every sync behaviour +9. TestBranchAsyncNativeCoroutines — native async __call__ children +10. TestBranchViaRun — Branch embedded inside Pipeline.run() +""" + +from __future__ import annotations + +import asyncio +import threading +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any + +import pytest + +from pipeline import Branch, BranchError, MergeStrategy, Pipeline, StepContext +from pipeline.branch import ( + _merge_last_write_wins, + _merge_namespaced, + _merge_raise_on_conflict, +) + +# --------------------------------------------------------------------------- +# Test-local subclass with named fields for merge/conflict tests +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class TestContext(StepContext): + """Subclass with domain fields used by branch merge tests.""" + + agent_output: Any = None + reflection: Any = None + + +# --------------------------------------------------------------------------- +# Shared helpers +# --------------------------------------------------------------------------- + + +class WriteX: + requires = frozenset() + provides = frozenset({"x"}) + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "x": "from_x"})) + + +class WriteY: + requires = frozenset() + provides = frozenset({"y"}) + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "y": "from_y"})) + + +class WriteZ: + requires = frozenset() + provides = frozenset({"z"}) + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "z": "from_z"})) + + +class WriteN: + """Writes metadata['n'] = n. Used in custom-merge arithmetic tests.""" + + requires = frozenset() + provides = frozenset({"n"}) + + def __init__(self, n: int): + self.n = n + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "n": self.n})) + + +class WriteAgent: + """Writes to the named field ctx.agent_output (not metadata).""" + + requires = frozenset() + provides = frozenset({"agent_output"}) + + def __init__(self, value: str): + self.value = value + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx.replace(agent_output=self.value) + + +class WriteReflection: + """Writes to the named field ctx.reflection — second named-field for conflict tests.""" + + requires = frozenset() + provides = frozenset({"reflection"}) + + def __init__(self, value: str): + self.value = value + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx.replace(reflection=self.value) + + +class Explode: + """Always raises RuntimeError with a configurable message.""" + + requires = frozenset() + provides = frozenset() + + def __init__(self, msg: str = "boom"): + self.msg = msg + + def __call__(self, ctx: StepContext) -> StepContext: + raise RuntimeError(self.msg) + + +class Log: + """Side-effect step: appends its name to a shared log (thread-safe).""" + + requires = frozenset() + provides = frozenset() + + def __init__(self, name: str, log: list[str], lock: threading.Lock): + self.name = name + self.log = log + self.lock = lock + + def __call__(self, ctx: StepContext) -> StepContext: + with self.lock: + self.log.append(self.name) + return ctx + + +# --- native async helpers --- + + +class AsyncWriteX: + requires = frozenset() + provides = frozenset({"x"}) + + async def __call__(self, ctx: StepContext) -> StepContext: + await asyncio.sleep(0) + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "x": "async_x"})) + + +class AsyncWriteY: + requires = frozenset() + provides = frozenset({"y"}) + + async def __call__(self, ctx: StepContext) -> StepContext: + await asyncio.sleep(0) + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "y": "async_y"})) + + +class AsyncExplode: + requires = frozenset() + provides = frozenset() + + def __init__(self, msg: str = "async_boom"): + self.msg = msg + + async def __call__(self, ctx: StepContext) -> StepContext: + await asyncio.sleep(0) + raise RuntimeError(self.msg) + + +# --------------------------------------------------------------------------- +# 1. Construction +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestBranchConstruction: + + def test_no_children_raises_value_error(self): + with pytest.raises(ValueError): + Branch() + + def test_single_child_accepted(self): + b = Branch(Pipeline().then(WriteX())) + assert len(b.pipelines) == 1 + + def test_two_children(self): + b = Branch(Pipeline().then(WriteX()), Pipeline().then(WriteY())) + assert len(b.pipelines) == 2 + + def test_three_children(self): + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + Pipeline().then(WriteZ()), + ) + assert len(b.pipelines) == 3 + + def test_provides_is_union_of_all_children(self): + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + Pipeline().then(WriteZ()), + ) + assert {"x", "y", "z"} <= b.provides + + def test_provides_with_overlapping_children(self): + """Two children both providing 'x' → 'x' still appears once in provides.""" + b = Branch(Pipeline().then(WriteX()), Pipeline().then(WriteX())) + assert "x" in b.provides + assert b.provides == frozenset({"x"}) + + def test_requires_is_union_of_all_children(self): + class NeedsA: + requires = frozenset({"a"}) + provides = frozenset({"b"}) + + def __call__(self, ctx): + return ctx + + class NeedsC: + requires = frozenset({"c"}) + provides = frozenset({"d"}) + + def __call__(self, ctx): + return ctx + + b = Branch(NeedsA(), NeedsC()) + assert "a" in b.requires + assert "c" in b.requires + + def test_empty_pipeline_children_accepted(self): + b = Branch(Pipeline(), Pipeline()) + assert b.requires == frozenset() + assert b.provides == frozenset() + + def test_raw_step_as_child_not_only_pipeline(self): + """Branch accepts any callable with requires/provides, not only Pipeline.""" + b = Branch(WriteX(), WriteY()) + assert "x" in b.provides + assert "y" in b.provides + + # merge selection ------------------------------------------------------- + + def test_default_merge_is_raise_on_conflict(self): + b = Branch(Pipeline().then(WriteX())) + assert b._merge_fn is _merge_raise_on_conflict + + def test_last_write_wins_merge_selected(self): + b = Branch(Pipeline().then(WriteX()), merge=MergeStrategy.LAST_WRITE_WINS) + assert b._merge_fn is _merge_last_write_wins + + def test_namespaced_merge_selected(self): + b = Branch(Pipeline().then(WriteX()), merge=MergeStrategy.NAMESPACED) + assert b._merge_fn is _merge_namespaced + + def test_custom_callable_stored_directly(self): + fn = lambda ctxs: ctxs[0] + b = Branch(Pipeline().then(WriteX()), merge=fn) + assert b._merge_fn is fn + + def test_custom_callable_not_confused_with_enum(self): + """A callable that happens to equal a string must not be mistaken for an enum.""" + fn = lambda ctxs: ctxs[0] + b = Branch(Pipeline().then(WriteX()), merge=fn) + assert b._merge_fn is not _merge_raise_on_conflict + + +# --------------------------------------------------------------------------- +# 2. Sync — RAISE_ON_CONFLICT +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestBranchSyncRaiseOnConflict: + + def _ctx(self) -> TestContext: + return TestContext(sample="s") + + def test_disjoint_metadata_merged(self): + b = Branch(Pipeline().then(WriteX()), Pipeline().then(WriteY())) + out = b(self._ctx()) + assert out.metadata["x"] == "from_x" + assert out.metadata["y"] == "from_y" + + def test_named_field_conflict_raises_value_error(self): + b = Branch( + Pipeline().then(WriteAgent("v1")), + Pipeline().then(WriteAgent("v2")), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + with pytest.raises(ValueError, match="conflict"): + b(self._ctx()) + + def test_error_message_names_the_conflicting_field(self): + b = Branch( + Pipeline().then(WriteAgent("v1")), + Pipeline().then(WriteAgent("v2")), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + with pytest.raises(ValueError, match="agent_output"): + b(self._ctx()) + + def test_same_named_field_value_does_not_raise(self): + b = Branch( + Pipeline().then(WriteAgent("same")), + Pipeline().then(WriteAgent("same")), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + out = b(self._ctx()) + assert out.agent_output == "same" + + def test_three_branches_same_value_does_not_raise(self): + b = Branch( + Pipeline().then(WriteAgent("same")), + Pipeline().then(WriteAgent("same")), + Pipeline().then(WriteAgent("same")), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + out = b(self._ctx()) + assert out.agent_output == "same" + + def test_three_branches_conflict_raises(self): + b = Branch( + Pipeline().then(WriteAgent("a")), + Pipeline().then(WriteAgent("b")), + Pipeline().then(WriteAgent("c")), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + with pytest.raises(ValueError, match="conflict"): + b(self._ctx()) + + def test_two_named_field_conflicts_reported(self): + """If two named fields both conflict, the error mentions both.""" + b = Branch( + Pipeline().then(WriteAgent("a")).then(WriteReflection("r1")), + Pipeline().then(WriteAgent("b")).then(WriteReflection("r2")), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + with pytest.raises(ValueError): + b(self._ctx()) + + def test_metadata_conflict_does_not_raise(self): + """Metadata keys use last-writer-wins even with RAISE_ON_CONFLICT. + The conflict check applies only to named StepContext fields.""" + + class MetaV1: + requires = frozenset() + provides = frozenset({"x"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "x": "v1"}) + ) + + class MetaV2: + requires = frozenset() + provides = frozenset({"x"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "x": "v2"}) + ) + + b = Branch( + Pipeline().then(MetaV1()), + Pipeline().then(MetaV2()), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + out = b(self._ctx()) # must NOT raise + assert "x" in out.metadata + + def test_metadata_always_unioned(self): + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + out = b(self._ctx()) + assert out.metadata["x"] == "from_x" + assert out.metadata["y"] == "from_y" + + def test_preserves_sample_field(self): + b = Branch(Pipeline().then(WriteX()), Pipeline().then(WriteY())) + out = b(TestContext(sample="hello")) + assert out.sample == "hello" + + +# --------------------------------------------------------------------------- +# 3. Sync — LAST_WRITE_WINS +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestBranchSyncLastWriteWins: + + def _ctx(self) -> TestContext: + return TestContext(sample="s") + + def test_second_branch_wins_on_named_field(self): + b = Branch( + Pipeline().then(WriteAgent("first")), + Pipeline().then(WriteAgent("second")), + merge=MergeStrategy.LAST_WRITE_WINS, + ) + out = b(self._ctx()) + assert out.agent_output == "second" + + def test_third_branch_wins_on_named_field(self): + b = Branch( + Pipeline().then(WriteAgent("first")), + Pipeline().then(WriteAgent("second")), + Pipeline().then(WriteAgent("third")), + merge=MergeStrategy.LAST_WRITE_WINS, + ) + out = b(self._ctx()) + assert out.agent_output == "third" + + def test_metadata_is_union_of_all_branches(self): + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + merge=MergeStrategy.LAST_WRITE_WINS, + ) + out = b(self._ctx()) + assert out.metadata["x"] == "from_x" + assert out.metadata["y"] == "from_y" + + def test_sole_writer_last_retains_value(self): + """Only one branch writes a field; it wins because it's the last writer.""" + b = Branch( + Pipeline(), # empty — agent_output stays None + Pipeline().then(WriteAgent("only")), + merge=MergeStrategy.LAST_WRITE_WINS, + ) + out = b(self._ctx()) + assert out.agent_output == "only" + + def test_empty_last_branch_overwrites_with_default(self): + """An empty last branch's None overwrites the first branch's value.""" + b = Branch( + Pipeline().then(WriteAgent("first")), + Pipeline(), # agent_output=None here — last writer wins + merge=MergeStrategy.LAST_WRITE_WINS, + ) + out = b(self._ctx()) + assert out.agent_output is None # None overwrites "first" + + def test_does_not_raise_on_any_conflict(self): + b = Branch( + Pipeline().then(WriteAgent("a")), + Pipeline().then(WriteAgent("b")), + merge=MergeStrategy.LAST_WRITE_WINS, + ) + out = b(self._ctx()) # must not raise + assert out.agent_output == "b" + + def test_preserves_sample_field(self): + b = Branch( + Pipeline().then(WriteAgent("v")), + merge=MergeStrategy.LAST_WRITE_WINS, + ) + out = b(TestContext(sample="test")) + assert out.sample == "test" + + +# --------------------------------------------------------------------------- +# 4. Sync — NAMESPACED +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestBranchSyncNamespaced: + + def _ctx(self) -> TestContext: + return TestContext(sample="s") + + def test_two_branches_keyed_branch_0_and_1(self): + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + merge=MergeStrategy.NAMESPACED, + ) + out = b(self._ctx()) + assert "branch_0" in out.metadata + assert "branch_1" in out.metadata + + def test_each_key_holds_full_child_context(self): + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + merge=MergeStrategy.NAMESPACED, + ) + out = b(self._ctx()) + assert out.metadata["branch_0"].metadata["x"] == "from_x" + assert out.metadata["branch_1"].metadata["y"] == "from_y" + + def test_three_branches_all_present(self): + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + Pipeline().then(WriteZ()), + merge=MergeStrategy.NAMESPACED, + ) + out = b(self._ctx()) + assert {"branch_0", "branch_1", "branch_2"} <= set(out.metadata.keys()) + + def test_named_fields_taken_from_first_branch(self): + """Named dataclass fields on the result come from branch_0's output.""" + b = Branch( + Pipeline().then(WriteAgent("from_first")), + Pipeline().then(WriteAgent("from_second")), + merge=MergeStrategy.NAMESPACED, + ) + out = b(self._ctx()) + assert out.agent_output == "from_first" + + def test_both_branch_outputs_accessible_via_namespace(self): + b = Branch( + Pipeline().then(WriteAgent("a")), + Pipeline().then(WriteAgent("b")), + merge=MergeStrategy.NAMESPACED, + ) + out = b(self._ctx()) + assert out.metadata["branch_0"].agent_output == "a" + assert out.metadata["branch_1"].agent_output == "b" + + def test_conflicting_named_fields_do_not_raise(self): + """NAMESPACED never raises — outputs are fully isolated in metadata keys.""" + b = Branch( + Pipeline().then(WriteAgent("v1")), + Pipeline().then(WriteAgent("v2")), + merge=MergeStrategy.NAMESPACED, + ) + out = b(self._ctx()) # must not raise + assert out is not None + + def test_base_metadata_preserved(self): + """Existing metadata on the input context is preserved in the output.""" + ctx = TestContext(sample="s", metadata={"existing": 99}) + b = Branch(Pipeline().then(WriteX()), merge=MergeStrategy.NAMESPACED) + out = b(ctx) + assert out.metadata["existing"] == 99 + + def test_preserves_sample_field(self): + b = Branch(Pipeline().then(WriteX()), merge=MergeStrategy.NAMESPACED) + out = b(TestContext(sample="orig")) + assert out.sample == "orig" + + +# --------------------------------------------------------------------------- +# 5. Sync — custom merge +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestBranchSyncCustomMerge: + + def _ctx(self) -> TestContext: + return TestContext(sample="s") + + def test_fn_receives_all_outputs(self): + received: list = [] + + def capture(ctxs): + received.extend(ctxs) + return ctxs[0] + + b = Branch(Pipeline().then(WriteX()), Pipeline().then(WriteY()), merge=capture) + b(self._ctx()) + assert len(received) == 2 + + def test_fn_receives_outputs_not_inputs(self): + """Each element passed to the merge fn must be a result, not the input ctx.""" + received: list = [] + + def capture(ctxs): + received.extend(ctxs) + return ctxs[0] + + orig = self._ctx() + b = Branch(Pipeline().then(WriteX()), merge=capture) + b(orig) + # The received ctx should have 'x' set (it is the output, not the input) + assert received[0].metadata.get("x") == "from_x" + + def test_fn_can_compute_aggregate(self): + def merge_sum(ctxs): + total = sum(ctx.metadata.get("n", 0) for ctx in ctxs) + return ctxs[0].replace(metadata=MappingProxyType({"total": total})) + + b = Branch( + Pipeline().then(WriteN(3)), Pipeline().then(WriteN(7)), merge=merge_sum + ) + out = b(self._ctx()) + assert out.metadata["total"] == 10 + + def test_fn_can_select_last_output(self): + last = lambda ctxs: ctxs[-1] + b = Branch(Pipeline().then(WriteX()), Pipeline().then(WriteY()), merge=last) + out = b(self._ctx()) + assert "y" in out.metadata + assert "x" not in out.metadata + + def test_fn_accesses_subclass_named_fields(self): + """Custom merge function that reads subclass fields to pick a winner.""" + + def pick_best(ctxs): + # Select the branch whose agent_output is longest + return max(ctxs, key=lambda c: len(str(c.agent_output or ""))) + + b = Branch( + Pipeline().then(WriteAgent("short")), + Pipeline().then(WriteAgent("much_longer_answer")), + merge=pick_best, + ) + out = b(self._ctx()) + assert out.agent_output == "much_longer_answer" + + def test_fn_combines_subclass_fields_from_branches(self): + """Custom merge that combines named fields from different branches.""" + + def combine(ctxs): + # Take agent_output from first, reflection from second + return ctxs[0].replace(reflection=ctxs[1].reflection) + + b = Branch( + Pipeline().then(WriteAgent("answer")), + Pipeline().then(WriteReflection("insight")), + merge=combine, + ) + out = b(self._ctx()) + assert out.agent_output == "answer" + assert out.reflection == "insight" + + def test_fn_exception_propagates_directly(self): + def bad_merge(ctxs): + raise ValueError("merge exploded") + + b = Branch( + Pipeline().then(WriteX()), Pipeline().then(WriteY()), merge=bad_merge + ) + with pytest.raises(ValueError, match="merge exploded"): + b(self._ctx()) + + +# --------------------------------------------------------------------------- +# 6. Sync — failure semantics +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestBranchSyncFailures: + + def _ctx(self) -> TestContext: + return TestContext(sample="s") + + def test_single_failure_raises_branch_error(self): + b = Branch(Pipeline().then(Explode("err1"))) + with pytest.raises(BranchError): + b(self._ctx()) + + def test_two_failures_both_collected(self): + b = Branch( + Pipeline().then(Explode("err1")), + Pipeline().then(Explode("err2")), + ) + with pytest.raises(BranchError) as exc_info: + b(self._ctx()) + assert len(exc_info.value.failures) == 2 + + def test_three_failures_all_collected(self): + b = Branch( + Pipeline().then(Explode("e1")), + Pipeline().then(Explode("e2")), + Pipeline().then(Explode("e3")), + ) + with pytest.raises(BranchError) as exc_info: + b(self._ctx()) + assert len(exc_info.value.failures) == 3 + + def test_partial_failure_remaining_branches_still_run(self): + """All branches must complete even when one fails early.""" + log: list[str] = [] + lock = threading.Lock() + + b = Branch( + Pipeline().then(Explode("fail")), + Pipeline().then(Log("b", log, lock)), + Pipeline().then(Log("c", log, lock)), + ) + with pytest.raises(BranchError): + b(self._ctx()) + + assert "b" in log + assert "c" in log + + def test_one_failure_one_success_raises_branch_error(self): + b = Branch(Pipeline().then(WriteX()), Pipeline().then(Explode())) + with pytest.raises(BranchError): + b(self._ctx()) + + def test_branch_error_message_includes_failure_count(self): + b = Branch(Pipeline().then(Explode("e1")), Pipeline().then(Explode("e2"))) + with pytest.raises(BranchError) as exc_info: + b(self._ctx()) + assert "2" in str(exc_info.value) + + def test_branch_error_failures_are_original_exceptions(self): + b = Branch(Pipeline().then(Explode("specific_msg"))) + with pytest.raises(BranchError) as exc_info: + b(self._ctx()) + inner = exc_info.value.failures[0] + assert isinstance(inner, RuntimeError) + assert "specific_msg" in str(inner) + + def test_merge_fn_not_called_on_failure(self): + """If any branch fails, the merge function must never be invoked.""" + called: list = [] + + def should_not_run(ctxs): + called.append(True) + return ctxs[0] + + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(Explode()), + merge=should_not_run, + ) + with pytest.raises(BranchError): + b(self._ctx()) + + assert called == [] + + def test_branch_error_is_not_value_error(self): + b = Branch(Pipeline().then(Explode())) + with pytest.raises(BranchError) as exc_info: + b(self._ctx()) + assert not isinstance(exc_info.value, ValueError) + + +# --------------------------------------------------------------------------- +# 7. Sync — immutability / isolation +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestBranchSyncImmutability: + + def _ctx(self) -> TestContext: + return TestContext(sample="s") + + def test_all_branches_receive_the_same_frozen_context(self): + """All branches get the identical input object — frozen so sharing is safe.""" + received: list[StepContext] = [] + lock = threading.Lock() + + class Capture: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx): + with lock: + received.append(ctx) + return ctx + + orig = self._ctx() + b = Branch(Pipeline().then(Capture()), Pipeline().then(Capture())) + b(orig) + assert all(c is orig for c in received) + + def test_branch_outputs_are_independent(self): + """Writes in one branch must not appear in another branch's output.""" + + class WriteXv1: + requires = frozenset() + provides = frozenset({"x"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "x": "branch_a"}) + ) + + class WriteXv2: + requires = frozenset() + provides = frozenset({"x"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "x": "branch_b"}) + ) + + b = Branch( + Pipeline().then(WriteXv1()), + Pipeline().then(WriteXv2()), + merge=MergeStrategy.NAMESPACED, + ) + out = b(self._ctx()) + assert out.metadata["branch_0"].metadata["x"] == "branch_a" + assert out.metadata["branch_1"].metadata["x"] == "branch_b" + + def test_original_context_unchanged_after_branch(self): + orig = TestContext(sample="frozen", agent_output=None) + b = Branch(Pipeline().then(WriteAgent("mutated"))) + b(orig) + assert orig.agent_output is None # frozen — input is untouched + + +# --------------------------------------------------------------------------- +# 8. Async — parity with sync +# +# Every sync behaviour exercised above must hold in __call_async__. +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestBranchAsyncParity: + """Mirrors every sync test class for the async path.""" + + def _ctx(self) -> TestContext: + return TestContext(sample="s") + + # RAISE_ON_CONFLICT ------------------------------------------------------- + + def test_disjoint_metadata_merged(self): + b = Branch(Pipeline().then(WriteX()), Pipeline().then(WriteY())) + out = asyncio.run(b.__call_async__(self._ctx())) + assert out.metadata["x"] == "from_x" + assert out.metadata["y"] == "from_y" + + def test_named_field_conflict_raises_value_error(self): + """The merge fn raises ValueError; it propagates from __call_async__.""" + b = Branch( + Pipeline().then(WriteAgent("v1")), + Pipeline().then(WriteAgent("v2")), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + with pytest.raises(ValueError, match="conflict"): + asyncio.run(b.__call_async__(self._ctx())) + + def test_same_value_no_conflict_no_raise(self): + b = Branch( + Pipeline().then(WriteAgent("same")), + Pipeline().then(WriteAgent("same")), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert out.agent_output == "same" + + def test_metadata_conflict_does_not_raise(self): + class MetaV1: + requires = frozenset() + provides = frozenset({"x"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "x": "v1"}) + ) + + class MetaV2: + requires = frozenset() + provides = frozenset({"x"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "x": "v2"}) + ) + + b = Branch( + Pipeline().then(MetaV1()), + Pipeline().then(MetaV2()), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert "x" in out.metadata + + # LAST_WRITE_WINS --------------------------------------------------------- + + def test_last_write_wins_second_branch(self): + b = Branch( + Pipeline().then(WriteAgent("first")), + Pipeline().then(WriteAgent("second")), + merge=MergeStrategy.LAST_WRITE_WINS, + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert out.agent_output == "second" + + def test_last_write_wins_third_branch(self): + b = Branch( + Pipeline().then(WriteAgent("a")), + Pipeline().then(WriteAgent("b")), + Pipeline().then(WriteAgent("c")), + merge=MergeStrategy.LAST_WRITE_WINS, + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert out.agent_output == "c" + + def test_last_write_wins_metadata_union(self): + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + merge=MergeStrategy.LAST_WRITE_WINS, + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert out.metadata["x"] == "from_x" + assert out.metadata["y"] == "from_y" + + # NAMESPACED -------------------------------------------------------------- + + def test_namespaced_two_branches(self): + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + merge=MergeStrategy.NAMESPACED, + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert "branch_0" in out.metadata + assert "branch_1" in out.metadata + + def test_namespaced_child_contexts_accessible(self): + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + merge=MergeStrategy.NAMESPACED, + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert out.metadata["branch_0"].metadata["x"] == "from_x" + assert out.metadata["branch_1"].metadata["y"] == "from_y" + + def test_namespaced_three_branches(self): + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + Pipeline().then(WriteZ()), + merge=MergeStrategy.NAMESPACED, + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert {"branch_0", "branch_1", "branch_2"} <= set(out.metadata.keys()) + + def test_namespaced_named_fields_from_first_branch(self): + b = Branch( + Pipeline().then(WriteAgent("from_first")), + Pipeline().then(WriteAgent("from_second")), + merge=MergeStrategy.NAMESPACED, + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert out.agent_output == "from_first" + + # custom merge ------------------------------------------------------------ + + def test_custom_merge_receives_all_outputs(self): + received: list = [] + + def capture(ctxs): + received.extend(ctxs) + return ctxs[0] + + b = Branch(Pipeline().then(WriteX()), Pipeline().then(WriteY()), merge=capture) + asyncio.run(b.__call_async__(self._ctx())) + assert len(received) == 2 + + def test_custom_merge_can_compute_aggregate(self): + def merge_sum(ctxs): + total = sum(ctx.metadata.get("n", 0) for ctx in ctxs) + return ctxs[0].replace(metadata=MappingProxyType({"total": total})) + + b = Branch( + Pipeline().then(WriteN(4)), Pipeline().then(WriteN(6)), merge=merge_sum + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert out.metadata["total"] == 10 + + # failure semantics ------------------------------------------------------- + + def test_all_branch_failures_collected(self): + b = Branch( + Pipeline().then(Explode("err1")), + Pipeline().then(Explode("err2")), + ) + with pytest.raises(BranchError) as exc_info: + asyncio.run(b.__call_async__(self._ctx())) + assert len(exc_info.value.failures) == 2 + + def test_three_failures_all_collected(self): + b = Branch( + Pipeline().then(Explode("e1")), + Pipeline().then(Explode("e2")), + Pipeline().then(Explode("e3")), + ) + with pytest.raises(BranchError) as exc_info: + asyncio.run(b.__call_async__(self._ctx())) + assert len(exc_info.value.failures) == 3 + + def test_one_failure_one_success_raises(self): + b = Branch(Pipeline().then(WriteX()), Pipeline().then(Explode())) + with pytest.raises(BranchError): + asyncio.run(b.__call_async__(self._ctx())) + + def test_merge_fn_not_called_on_failure(self): + called: list = [] + + def should_not_run(ctxs): + called.append(True) + return ctxs[0] + + b = Branch( + Pipeline().then(WriteX()), + Pipeline().then(Explode()), + merge=should_not_run, + ) + with pytest.raises(BranchError): + asyncio.run(b.__call_async__(self._ctx())) + + assert called == [] + + # immutability ------------------------------------------------------------ + + def test_frozen_context_not_mutated(self): + """All branches receive the same frozen context object.""" + received: list[StepContext] = [] + + class AsyncCapture: + requires = frozenset() + provides = frozenset() + + async def __call__(self, ctx): + received.append(ctx) + return ctx + + orig = self._ctx() + b = Branch(Pipeline().then(AsyncCapture()), Pipeline().then(AsyncCapture())) + asyncio.run(b.__call_async__(orig)) + assert all(c is orig for c in received) + + # sample field preserved -------------------------------------------------- + + def test_preserves_sample_field(self): + b = Branch(Pipeline().then(WriteX()), Pipeline().then(WriteY())) + out = asyncio.run(b.__call_async__(TestContext(sample="preserved"))) + assert out.sample == "preserved" + + +# --------------------------------------------------------------------------- +# 9. Async — native async children (coroutine __call__) +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestBranchAsyncNativeCoroutines: + """Branch.__call_async__ detects native coroutine children and awaits directly.""" + + def _ctx(self) -> TestContext: + return TestContext(sample="s") + + def test_two_async_children_execute(self): + b = Branch(Pipeline().then(AsyncWriteX()), Pipeline().then(AsyncWriteY())) + out = asyncio.run(b.__call_async__(self._ctx())) + assert out.metadata["x"] == "async_x" + assert out.metadata["y"] == "async_y" + + def test_async_failure_collected(self): + b = Branch( + Pipeline().then(AsyncWriteX()), + Pipeline().then(AsyncExplode("native_fail")), + ) + with pytest.raises(BranchError) as exc_info: + asyncio.run(b.__call_async__(self._ctx())) + assert len(exc_info.value.failures) == 1 + assert "native_fail" in str(exc_info.value.failures[0]) + + def test_all_async_failures_collected(self): + b = Branch( + Pipeline().then(AsyncExplode("e1")), + Pipeline().then(AsyncExplode("e2")), + Pipeline().then(AsyncExplode("e3")), + ) + with pytest.raises(BranchError) as exc_info: + asyncio.run(b.__call_async__(self._ctx())) + assert len(exc_info.value.failures) == 3 + + def test_mixed_sync_and_native_async_children(self): + """Branch can fan out over a mix of sync (to_thread) and native async steps.""" + b = Branch( + Pipeline().then(WriteX()), # sync → asyncio.to_thread + Pipeline().then(AsyncWriteY()), # async → direct await + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert out.metadata["x"] == "from_x" + assert out.metadata["y"] == "async_y" + + def test_three_children_mixed(self): + b = Branch( + Pipeline().then(AsyncWriteX()), + Pipeline().then(AsyncWriteY()), + Pipeline().then(WriteZ()), # sync + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + out = asyncio.run(b.__call_async__(self._ctx())) + assert out.metadata["x"] == "async_x" + assert out.metadata["y"] == "async_y" + assert out.metadata["z"] == "from_z" + + +# --------------------------------------------------------------------------- +# 10. Integration — Branch via Pipeline.run() / run_async() +# +# When Branch is a step inside a Pipeline, the async code path in run_async() +# detects __call_async__ and calls it — Branch always runs async here. +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestBranchViaRun: + + def test_disjoint_metadata_through_run(self): + pipe = Pipeline().branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + ) + results = pipe.run([TestContext(sample="s")]) + out = results[0].output + assert out.metadata["x"] == "from_x" + assert out.metadata["y"] == "from_y" + + def test_failure_captured_in_sample_result(self): + pipe = Pipeline().branch( + Pipeline().then(WriteX()), + Pipeline().then(Explode()), + ) + results = pipe.run([TestContext(sample="s")]) + assert results[0].error is not None + assert results[0].failed_at == "Branch" + + def test_branch_error_preserved_in_sample_result(self): + pipe = Pipeline().branch( + Pipeline().then(Explode("e1")), + Pipeline().then(Explode("e2")), + ) + results = pipe.run([TestContext(sample="s")]) + assert isinstance(results[0].error, BranchError) + assert len(results[0].error.failures) == 2 + + def test_pre_branch_data_visible_inside_branch(self): + """Steps run before Branch must be visible in ctx received by branches.""" + + class SetPre: + requires = frozenset() + provides = frozenset({"pre"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "pre": 42}) + ) + + class ReadPre: + requires = frozenset({"pre"}) + provides = frozenset({"read"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType( + {**ctx.metadata, "read": ctx.metadata["pre"]} + ) + ) + + pipe = ( + Pipeline() + .then(SetPre()) + .branch( + Pipeline().then(ReadPre()), + Pipeline().then(WriteY()), + merge=MergeStrategy.LAST_WRITE_WINS, + ) + ) + results = pipe.run([TestContext(sample="s")]) + out = results[0].output + assert out.metadata.get("read") == 42 + assert out.metadata.get("y") == "from_y" + + def test_step_after_branch_receives_merged_context(self): + class After: + requires = frozenset() + provides = frozenset({"after"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "after": True}) + ) + + pipe = ( + Pipeline() + .branch(Pipeline().then(WriteX()), Pipeline().then(WriteY())) + .then(After()) + ) + results = pipe.run([TestContext(sample="s")]) + out = results[0].output + assert out.metadata["x"] == "from_x" + assert out.metadata["y"] == "from_y" + assert out.metadata["after"] is True + + def test_multiple_samples_all_succeed(self): + pipe = Pipeline().branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + ) + results = pipe.run([TestContext(sample=s) for s in ("s1", "s2", "s3")]) + assert len(results) == 3 + assert all(r.error is None for r in results) + assert all(r.output.metadata.get("x") == "from_x" for r in results) + + def test_last_write_wins_through_run(self): + pipe = Pipeline().branch( + Pipeline().then(WriteAgent("first")), + Pipeline().then(WriteAgent("second")), + merge=MergeStrategy.LAST_WRITE_WINS, + ) + results = pipe.run([TestContext(sample="s")]) + assert results[0].output.agent_output == "second" + + def test_namespaced_through_run(self): + pipe = Pipeline().branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + merge=MergeStrategy.NAMESPACED, + ) + results = pipe.run([TestContext(sample="s")]) + out = results[0].output + assert "branch_0" in out.metadata + assert "branch_1" in out.metadata + + def test_nested_branch_inside_pipeline_step(self): + """Pipeline-as-step containing a Branch, nested inside another Pipeline.""" + inner = Pipeline().branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + ) + outer = Pipeline().then(inner).then(WriteZ()) + results = outer.run([TestContext(sample="s")]) + out = results[0].output + assert out.metadata["x"] == "from_x" + assert out.metadata["y"] == "from_y" + assert out.metadata["z"] == "from_z" + + def test_run_async_entry_point_with_branch(self): + pipe = Pipeline().branch( + Pipeline().then(WriteX()), + Pipeline().then(WriteY()), + ) + results = asyncio.run( + pipe.run_async([TestContext(sample="s1"), TestContext(sample="s2")]) + ) + assert len(results) == 2 + assert all(r.error is None for r in results) + + def test_branch_runs_children_in_parallel(self): + """Fan-out branches must execute concurrently, not sequentially. + + Uses a threading.Barrier that requires both branches to arrive before + either can proceed. If branches ran sequentially the first would + block at the barrier and the test would fail with a BranchError. + """ + barrier = threading.Barrier(2, timeout=5) + + class BarrierStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + barrier.wait() # blocks until the other branch also arrives + return ctx + + pipe = Pipeline().branch( + Pipeline().then(BarrierStep()), + Pipeline().then(BarrierStep()), + ) + results = pipe.run([TestContext(sample="s")]) + assert results[0].error is None diff --git a/tests/pipeline_engine/test_context.py b/tests/pipeline_engine/test_context.py new file mode 100644 index 0000000000000000000000000000000000000000..e6d9b69e7b2ae43a5d80cc704e6333d850bcf6f2 --- /dev/null +++ b/tests/pipeline_engine/test_context.py @@ -0,0 +1,353 @@ +"""Unit tests for pipeline.StepContext. + +Tests cover the generic base class (sample + metadata) and the subclassing +pattern that consuming applications use to add domain fields. +""" + +from __future__ import annotations + +import dataclasses +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any + +import pytest + +from pipeline import StepContext + +# --------------------------------------------------------------------------- +# Test-local subclass — validates the subclassing pattern +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class DomainContext(StepContext): + """Minimal subclass used only in these tests.""" + + output: Any = None + score: float = 0.0 + + +# --------------------------------------------------------------------------- +# Base class defaults +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestStepContextDefaults: + def test_sample_defaults_to_none(self): + ctx = StepContext() + assert ctx.sample is None + + def test_metadata_defaults_to_empty_mappingproxy(self): + ctx = StepContext(sample="s") + assert ctx.metadata == MappingProxyType({}) + assert isinstance(ctx.metadata, MappingProxyType) + + def test_only_two_fields_on_base_class(self): + field_names = {f.name for f in dataclasses.fields(StepContext)} + assert field_names == {"sample", "metadata"} + + +# --------------------------------------------------------------------------- +# Immutability +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestStepContextImmutability: + def test_setting_sample_raises(self): + ctx = StepContext(sample="s") + with pytest.raises( + (dataclasses.FrozenInstanceError, AttributeError, TypeError) + ): + ctx.sample = "other" # type: ignore[misc] + + def test_setting_metadata_raises(self): + ctx = StepContext(sample="s") + with pytest.raises( + (dataclasses.FrozenInstanceError, AttributeError, TypeError) + ): + ctx.metadata = MappingProxyType({"x": 1}) # type: ignore[misc] + + def test_metadata_mappingproxy_is_not_mutable(self): + ctx = StepContext(sample="s", metadata={"k": "v"}) + with pytest.raises(TypeError): + ctx.metadata["k"] = "overwrite" # type: ignore[index] + + +# --------------------------------------------------------------------------- +# Coercion +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestStepContextCoercion: + def test_plain_dict_metadata_coerced_to_mappingproxy(self): + ctx = StepContext(sample="s", metadata={"x": 1}) + assert isinstance(ctx.metadata, MappingProxyType) + assert ctx.metadata["x"] == 1 + + def test_existing_mappingproxy_not_double_wrapped(self): + mp = MappingProxyType({"x": 1}) + ctx = StepContext(sample="s", metadata=mp) + assert ctx.metadata is mp + + +# --------------------------------------------------------------------------- +# replace() +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestStepContextReplace: + def test_replace_returns_new_object(self): + ctx = StepContext(sample="s") + ctx2 = ctx.replace(sample="t") + assert ctx2 is not ctx + + def test_replace_does_not_mutate_original(self): + ctx = StepContext(sample="s") + ctx.replace(sample="t") + assert ctx.sample == "s" + + def test_replace_updates_target_field(self): + ctx = StepContext(sample="s") + ctx2 = ctx.replace(sample="t") + assert ctx2.sample == "t" + + def test_replace_preserves_other_fields(self): + ctx = StepContext(sample="s", metadata={"k": 1}) + ctx2 = ctx.replace(sample="t") + assert ctx2.metadata["k"] == 1 + + def test_replace_metadata_immutable_pattern(self): + ctx = StepContext(sample="s", metadata={"x": 1}) + ctx2 = ctx.replace(metadata=MappingProxyType({**ctx.metadata, "y": 2})) + assert ctx2.metadata["x"] == 1 + assert ctx2.metadata["y"] == 2 + assert "y" not in ctx.metadata # original unchanged + + +# --------------------------------------------------------------------------- +# Equality +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestStepContextEquality: + def test_equal_contexts(self): + ctx1 = StepContext(sample="s") + ctx2 = StepContext(sample="s") + assert ctx1 == ctx2 + + def test_different_sample_not_equal(self): + assert StepContext(sample="a") != StepContext(sample="b") + + def test_context_not_hashable(self): + """StepContext is frozen but NOT hashable: MappingProxyType wraps a dict, + which is unhashable, so Python cannot derive a hash for the dataclass.""" + ctx = StepContext(sample="s") + with pytest.raises(TypeError): + hash(ctx) + + +# --------------------------------------------------------------------------- +# Subclassing pattern +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestStepContextSubclassing: + def test_subclass_has_base_fields(self): + ctx = DomainContext(sample="s") + assert ctx.sample == "s" + assert ctx.metadata == MappingProxyType({}) + + def test_subclass_has_domain_fields(self): + ctx = DomainContext(sample="s", output="answer", score=0.95) + assert ctx.output == "answer" + assert ctx.score == 0.95 + + def test_subclass_defaults(self): + ctx = DomainContext(sample="s") + assert ctx.output is None + assert ctx.score == 0.0 + + def test_subclass_is_frozen(self): + ctx = DomainContext(sample="s", output="x") + with pytest.raises( + (dataclasses.FrozenInstanceError, AttributeError, TypeError) + ): + ctx.output = "y" # type: ignore[misc] + + def test_subclass_replace_returns_same_type(self): + ctx = DomainContext(sample="s") + ctx2 = ctx.replace(output="answer") + assert isinstance(ctx2, DomainContext) + assert ctx2.output == "answer" + + def test_subclass_replace_preserves_base_fields(self): + ctx = DomainContext(sample="s", metadata={"k": 1}) + ctx2 = ctx.replace(output="x") + assert ctx2.sample == "s" + assert ctx2.metadata["k"] == 1 + + def test_subclass_replace_preserves_domain_fields(self): + ctx = DomainContext(sample="s", output="a", score=0.9) + ctx2 = ctx.replace(sample="t") + assert ctx2.output == "a" + assert ctx2.score == 0.9 + + def test_subclass_metadata_coercion(self): + ctx = DomainContext(sample="s", metadata={"x": 1}) + assert isinstance(ctx.metadata, MappingProxyType) + + def test_subclass_isinstance_of_step_context(self): + ctx = DomainContext(sample="s") + assert isinstance(ctx, StepContext) + + def test_subclass_equality(self): + a = DomainContext(sample="s", output="x") + b = DomainContext(sample="s", output="x") + assert a == b + + def test_subclass_inequality_on_domain_field(self): + a = DomainContext(sample="s", output="x") + b = DomainContext(sample="s", output="y") + assert a != b + + +# --------------------------------------------------------------------------- +# Multi-level subclassing (SubSubContext) +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class ExtendedContext(DomainContext): + """Two-level subclass: StepContext → DomainContext → ExtendedContext.""" + + label: str = "" + + +@pytest.mark.unit +class TestMultiLevelSubclassing: + def test_has_all_ancestor_fields(self): + ctx = ExtendedContext(sample="s", output="x", score=0.5, label="L") + assert ctx.sample == "s" + assert ctx.metadata == MappingProxyType({}) + assert ctx.output == "x" + assert ctx.score == 0.5 + assert ctx.label == "L" + + def test_defaults_from_all_levels(self): + ctx = ExtendedContext(sample="s") + assert ctx.output is None # from DomainContext + assert ctx.score == 0.0 # from DomainContext + assert ctx.label == "" # from ExtendedContext + + def test_replace_returns_correct_type(self): + ctx = ExtendedContext(sample="s") + ctx2 = ctx.replace(label="new") + assert isinstance(ctx2, ExtendedContext) + assert ctx2.label == "new" + + def test_replace_preserves_all_levels(self): + ctx = ExtendedContext(sample="s", output="x", score=0.9, label="L") + ctx2 = ctx.replace(sample="t") + assert ctx2.output == "x" + assert ctx2.score == 0.9 + assert ctx2.label == "L" + + def test_isinstance_chain(self): + ctx = ExtendedContext(sample="s") + assert isinstance(ctx, StepContext) + assert isinstance(ctx, DomainContext) + assert isinstance(ctx, ExtendedContext) + + def test_is_frozen(self): + ctx = ExtendedContext(sample="s", label="L") + with pytest.raises( + (dataclasses.FrozenInstanceError, AttributeError, TypeError) + ): + ctx.label = "new" # type: ignore[misc] + + def test_metadata_coercion(self): + ctx = ExtendedContext(sample="s", metadata={"k": 1}) + assert isinstance(ctx.metadata, MappingProxyType) + + def test_field_count(self): + field_names = {f.name for f in dataclasses.fields(ExtendedContext)} + assert field_names == {"sample", "metadata", "output", "score", "label"} + + +# --------------------------------------------------------------------------- +# Multi-field replace +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestMultiFieldReplace: + def test_replace_multiple_fields_at_once(self): + ctx = DomainContext(sample="s", output="a", score=0.1) + ctx2 = ctx.replace(sample="t", output="b", score=0.9) + assert ctx2.sample == "t" + assert ctx2.output == "b" + assert ctx2.score == 0.9 + + def test_replace_base_and_domain_fields_together(self): + ctx = DomainContext(sample="s", metadata={"k": 1}, output="a") + ctx2 = ctx.replace( + sample="t", + metadata=MappingProxyType({"k": 2}), + output="b", + ) + assert ctx2.sample == "t" + assert ctx2.metadata["k"] == 2 + assert ctx2.output == "b" + + def test_replace_all_fields_on_multi_level_subclass(self): + ctx = ExtendedContext(sample="s", output="a", score=0.1, label="L") + ctx2 = ctx.replace(sample="t", output="b", score=0.9, label="M") + assert isinstance(ctx2, ExtendedContext) + assert ctx2.sample == "t" + assert ctx2.output == "b" + assert ctx2.score == 0.9 + assert ctx2.label == "M" + + def test_original_unchanged_after_multi_field_replace(self): + ctx = DomainContext(sample="s", output="a", score=0.1) + ctx.replace(sample="t", output="b", score=0.9) + assert ctx.sample == "s" + assert ctx.output == "a" + assert ctx.score == 0.1 + + +# --------------------------------------------------------------------------- +# Cross-type equality +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class OtherContext(StepContext): + """A different subclass with the same field name as DomainContext.""" + + output: Any = None + + +@pytest.mark.unit +class TestCrossTypeEquality: + def test_different_subclass_types_not_equal(self): + a = DomainContext(sample="s", output="x") + b = OtherContext(sample="s", output="x") + assert a != b + + def test_base_not_equal_to_subclass(self): + base = StepContext(sample="s") + sub = DomainContext(sample="s") + assert base != sub + + def test_subclass_not_equal_to_sub_subclass(self): + parent = DomainContext(sample="s") + child = ExtendedContext(sample="s") + assert parent != child diff --git a/tests/pipeline_engine/test_errors.py b/tests/pipeline_engine/test_errors.py new file mode 100644 index 0000000000000000000000000000000000000000..f373fc230a5c9b8dc18f27ddb69d32267987df49 --- /dev/null +++ b/tests/pipeline_engine/test_errors.py @@ -0,0 +1,56 @@ +"""Unit tests for pipeline error types.""" + +from __future__ import annotations + +import pytest + +from pipeline import BranchError, PipelineConfigError, PipelineOrderError + + +@pytest.mark.unit +class TestErrorHierarchy: + def test_pipeline_order_error_is_exception(self): + err = PipelineOrderError("bad order") + assert isinstance(err, Exception) + + def test_pipeline_config_error_is_exception(self): + err = PipelineConfigError("bad config") + assert isinstance(err, Exception) + + def test_branch_error_is_exception(self): + err = BranchError([RuntimeError("x")]) + assert isinstance(err, Exception) + + +@pytest.mark.unit +class TestBranchError: + def test_stores_failures_list(self): + failures = [RuntimeError("a"), ValueError("b")] + err = BranchError(failures) + assert err.failures is failures + + def test_message_includes_failure_count(self): + err = BranchError([RuntimeError("x"), OSError("y")]) + assert "2" in str(err) + + def test_message_includes_type_names(self): + err = BranchError([RuntimeError("x"), ValueError("y")]) + msg = str(err) + assert "RuntimeError" in msg + assert "ValueError" in msg + + def test_single_failure(self): + inner = RuntimeError("boom") + err = BranchError([inner]) + assert err.failures == [inner] + assert "1" in str(err) + + def test_empty_failures_list(self): + err = BranchError([]) + assert err.failures == [] + assert "0" in str(err) + + def test_can_be_raised_and_caught(self): + with pytest.raises(BranchError) as exc_info: + raise BranchError([RuntimeError("boom")]) + assert len(exc_info.value.failures) == 1 diff --git a/tests/pipeline_engine/test_pipeline.py b/tests/pipeline_engine/test_pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..b10bbf44fa609ecb5523c684c45a24143fc816c1 --- /dev/null +++ b/tests/pipeline_engine/test_pipeline.py @@ -0,0 +1,621 @@ +"""Unit tests for Pipeline — construction, validation, and execution.""" + +from __future__ import annotations + +import asyncio +import time +import warnings +from types import MappingProxyType + +import pytest + +from pipeline import ( + Branch, + MergeStrategy, + Pipeline, + PipelineConfigError, + PipelineOrderError, + SampleResult, + StepContext, + StepProtocol, +) +from .conftest import ( + Boom, + BoundaryStep, + Noop, + Recorder, + SetA, + SetB, + SetC, + SlowBoundaryStep, + Slow, +) + +# --------------------------------------------------------------------------- +# Construction & contract inference +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestPipelineConstruction: + def test_empty_pipeline_has_empty_contracts(self): + p = Pipeline() + assert p.requires == frozenset() + assert p.provides == frozenset() + + def test_list_constructor_accepted(self): + p = Pipeline([SetA(), SetB()]) + assert "a" in p.provides + assert "b" in p.provides + + def test_then_returns_self(self): + p = Pipeline() + result = p.then(Noop()) + assert result is p + + def test_then_updates_provides(self): + p = Pipeline().then(SetA()) + assert "a" in p.provides + + def test_then_chain_updates_provides_cumulatively(self): + p = Pipeline().then(SetA()).then(SetB()).then(SetC()) + assert {"a", "b", "c"} <= p.provides + + def test_external_requires_inferred(self): + """Fields needed by the first step that no prior step provides.""" + p = Pipeline().then(SetB()) # SetB.requires = {"a"}, nothing provides "a" + assert "a" in p.requires + + def test_internally_satisfied_requires_not_in_external(self): + """When A→B, 'a' is provided internally so not in pipeline.requires.""" + p = Pipeline().then(SetA()).then(SetB()) + assert "a" not in p.requires + + def test_branch_method_appends_branch(self): + # Use two independent branches (no cross-dependency) + p = Pipeline().then(SetA()) + p2 = p.branch(Pipeline().then(Noop()), Pipeline().then(Noop())) + # branch() returns self; a Branch is the last step + assert isinstance(p2._steps[-1], Branch) + + def test_pipeline_satisfies_step_protocol(self): + p = Pipeline().then(SetA()) + assert isinstance(p, StepProtocol) + + +# --------------------------------------------------------------------------- +# Validation +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestPipelineValidation: + def test_order_error_when_b_before_a(self): + with pytest.raises(PipelineOrderError, match="a"): + Pipeline().then(SetB()).then(SetA()) + + def test_order_error_at_construction_with_list(self): + with pytest.raises(PipelineOrderError): + Pipeline([SetB(), SetA()]) + + def test_no_order_error_for_external_input(self): + """SetB requires 'a', but 'a' is not provided by any step → external input. + This is valid — the caller is expected to put 'a' in the initial context.""" + p = Pipeline().then(SetB()) # should NOT raise + assert "a" in p.requires + + def test_config_error_duplicate_async_boundary(self): + class B1: + requires = frozenset() + provides = frozenset({"p"}) + async_boundary = True + + def __call__(self, ctx): + return ctx + + class B2: + requires = frozenset() + provides = frozenset({"q"}) + async_boundary = True + + def __call__(self, ctx): + return ctx + + with pytest.raises(PipelineConfigError, match="duplicate"): + Pipeline().then(B1()).then(B2()) + + def test_config_error_boundary_inside_branch(self): + with pytest.raises(PipelineConfigError, match="Branch"): + Pipeline().branch(Pipeline().then(BoundaryStep())) + + def test_warning_for_boundary_on_nested_pipeline(self): + """async_boundary on a Pipeline-as-step must emit a warning (not error).""" + inner = Pipeline().then(SetA()) + inner.async_boundary = True # manually set + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + Pipeline().then(inner) + + assert any( + "async_boundary" in str(w.message) for w in caught + ), "Expected a warning about async_boundary on nested pipeline" + + def test_validation_runs_on_each_then_call(self): + # SetA provides "a"; SetB provides "b"; SetC requires "b". + # Adding SetC before SetB (so "b" is internally provided but out of order) + # must raise PipelineOrderError. + p = Pipeline().then(SetA()).then(SetB()) # "a" → "b" in order + with pytest.raises(PipelineOrderError): + # Now add a step that requires "a" again, but "a" was already consumed; + # add SetB *again* before SetC would work, but adding SetC before SetB + # in a fresh pipeline is the right test: + Pipeline().then(SetA()).then(SetC()).then( + SetB() + ) # "c" needs "b", "b" comes after + + +# --------------------------------------------------------------------------- +# __call__ (nested step mode) +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestPipelineCall: + def test_call_runs_all_steps(self): + p = Pipeline().then(SetA()).then(SetB()) + ctx = p(StepContext(sample="s")) + assert ctx.metadata["a"] == 1 + assert ctx.metadata["b"] == 2 + + def test_call_ignores_async_boundary(self): + """When used as nested step, async_boundary should not split execution.""" + p = Pipeline().then(SetA()).then(BoundaryStep()).then(SetB()) + + # BoundaryStep requires nothing, provides "bg_result" + # SetC would need "b", so we use SetA (provides "a"), then BoundaryStep, + # then Noop — all should run. + class AfterBoundary: + requires = frozenset() + provides = frozenset({"after"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "after": True}) + ) + + p2 = Pipeline().then(SetA()).then(BoundaryStep()).then(AfterBoundary()) + ctx = p2(StepContext(sample="s")) + assert ctx.metadata.get("a") == 1 + assert ctx.metadata.get("bg_result") is True + assert ctx.metadata.get("after") is True + + def test_empty_pipeline_call_passthrough(self): + ctx = StepContext(sample="original") + out = Pipeline()(ctx) + assert out == ctx + + +# --------------------------------------------------------------------------- +# run() — basic +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestPipelineRun: + def test_single_step_single_sample(self): + results = Pipeline().then(SetA()).run([StepContext(sample="s")]) + assert len(results) == 1 + assert results[0].output.metadata["a"] == 1 + assert results[0].error is None + + def test_multi_step_chain(self): + results = ( + Pipeline() + .then(SetA()) + .then(SetB()) + .then(SetC()) + .run([StepContext(sample="s")]) + ) + out = results[0].output + assert out.metadata["a"] == 1 + assert out.metadata["b"] == 2 + assert out.metadata["c"] == 4 + + def test_multiple_samples(self): + results = ( + Pipeline() + .then(SetA()) + .run([StepContext(sample=s) for s in ("s1", "s2", "s3")]) + ) + assert len(results) == 3 + assert all(r.output.metadata["a"] == 1 for r in results) + + def test_sample_value_in_result(self): + results = Pipeline().then(Noop()).run([StepContext(sample="hello")]) + assert results[0].sample == "hello" + + def test_empty_pipeline_passes_context_through(self): + results = Pipeline().run([StepContext(sample="s")]) + assert results[0].output is not None + assert results[0].output.sample == "s" + + def test_run_returns_sample_result_list(self): + results = ( + Pipeline() + .then(Noop()) + .run([StepContext(sample="a"), StepContext(sample="b")]) + ) + assert all(isinstance(r, SampleResult) for r in results) + + @pytest.mark.slow + def test_workers_N_runs_faster_than_sequential(self): + delay = 0.2 + contexts = [StepContext(sample=i) for i in range(4)] + pipe = Pipeline().then(Slow(delay)) + + t0 = time.monotonic() + pipe.run(contexts, workers=1) + seq_time = time.monotonic() - t0 + + t0 = time.monotonic() + pipe.run(contexts, workers=4) + par_time = time.monotonic() - t0 + + # Parallel should be at least 2× faster + assert ( + par_time < seq_time / 2 + ), f"workers=4 ({par_time:.2f}s) not faster than workers=1 ({seq_time:.2f}s)" + + +# --------------------------------------------------------------------------- +# run() — error handling +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestPipelineRunErrors: + def test_step_failure_sets_error(self): + results = Pipeline().then(Boom()).run([StepContext(sample="s")]) + assert results[0].error is not None + assert isinstance(results[0].error, RuntimeError) + + def test_step_failure_sets_failed_at_name(self): + results = Pipeline().then(Boom()).run([StepContext(sample="s")]) + assert results[0].failed_at == "Boom" + + def test_step_failure_output_is_none(self): + results = Pipeline().then(Boom()).run([StepContext(sample="s")]) + assert results[0].output is None + + def test_other_samples_continue_after_one_failure(self): + """A failing sample must not prevent other samples from being processed.""" + + class FailFirst: + requires = frozenset() + provides = frozenset() + call_count = 0 + + def __call__(self, ctx): + FailFirst.call_count += 1 + if ctx.sample == "bad": + raise RuntimeError("bad sample") + return ctx + + FailFirst.call_count = 0 + results = ( + Pipeline() + .then(FailFirst()) + .run([StepContext(sample=s) for s in ("ok1", "bad", "ok2")]) + ) + assert len(results) == 3 + errors = [r for r in results if r.error is not None] + successes = [r for r in results if r.error is None] + assert len(errors) == 1 + assert len(successes) == 2 + assert errors[0].sample == "bad" + + def test_failed_at_is_correct_step_name(self): + class FirstStep: + requires = frozenset() + provides = frozenset({"p"}) + + def __call__(self, ctx): + return ctx.replace(metadata={**ctx.metadata}) + + class FailingStep: + requires = frozenset({"p"}) + provides = frozenset() + + def __call__(self, ctx): + raise ValueError("fail") + + results = ( + Pipeline() + .then(FirstStep()) + .then(FailingStep()) + .run([StepContext(sample="s")]) + ) + assert results[0].failed_at == "FailingStep" + + +# --------------------------------------------------------------------------- +# run() — async_boundary + background +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestPipelineAsyncBoundary: + def test_foreground_returns_before_background_completes(self): + """run() must return while the background tail is still executing. + + We verify this by using a slow background step (0.3 s sleep) and a + threading.Event: if run() blocks until the background finishes, the + Event will already be set when we check it — which would fail the test. + """ + import threading as _threading + from types import MappingProxyType as _MappingProxyType + + bg_completed = _threading.Event() + + class SlowBg: + requires = frozenset() + provides = frozenset({"bg"}) + async_boundary = True + max_workers = 1 + + def __call__(self, ctx): + time.sleep(2.0) + bg_completed.set() + return ctx.replace( + metadata=_MappingProxyType({**ctx.metadata, "bg": True}) + ) + + pipe = Pipeline().then(SlowBg()) + results = pipe.run([StepContext(sample="s")]) + + # run() returned — background must NOT have finished yet + assert ( + not bg_completed.is_set() + ), "run() blocked until background completed; it should return immediately" + assert len(results) == 1 + + pipe.wait_for_background(timeout=5.0) + assert bg_completed.is_set() + + def test_wait_for_background_completes_output(self): + pipe = Pipeline().then(SetA()).then(BoundaryStep()) + results = pipe.run([StepContext(sample="s")]) + pipe.wait_for_background(timeout=5.0) + assert results[0].output is not None + assert results[0].output.metadata.get("bg_result") is True + assert results[0].error is None + + def test_background_failure_captured_in_result(self): + class BGBoom: + requires = frozenset() + provides = frozenset({"bg"}) + async_boundary = True + max_workers = 1 + + def __call__(self, ctx): + raise RuntimeError("bg_boom") + + pipe = Pipeline().then(BGBoom()) + results = pipe.run([StepContext(sample="s")]) + pipe.wait_for_background(timeout=5.0) + assert results[0].error is not None + assert results[0].failed_at == "BGBoom" + assert results[0].output is None + + def test_wait_for_background_no_threads_is_noop(self): + """wait_for_background() on a pipeline with no async_boundary must not raise.""" + pipe = Pipeline().then(SetA()) # no boundary → no background threads + pipe.run([StepContext(sample="s")]) + pipe.wait_for_background(timeout=1.0) # must be a silent no-op + + def test_wait_for_background_timeout_raises(self): + pipe = Pipeline().then(SlowBoundaryStep()) + pipe.run([StepContext(sample="s")]) + with pytest.raises(TimeoutError): + pipe.wait_for_background(timeout=0.05) + + def test_multiple_samples_all_get_background_result(self): + pipe = Pipeline().then(BoundaryStep()) + results = pipe.run([StepContext(sample=s) for s in ("a", "b", "c")]) + pipe.wait_for_background(timeout=5.0) + assert all(r.output is not None for r in results) + assert all(r.output.metadata.get("bg_result") is True for r in results) + + @pytest.mark.slow + def test_background_max_workers_1_serializes_execution(self): + """With max_workers=1, background steps cannot interleave.""" + from .conftest import SerialStep + + SerialStep._log.clear() + + class TriggerBoundary: + requires = frozenset() + provides = frozenset({"trigger"}) + async_boundary = True + max_workers = 3 # multiple samples can start background at once + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "trigger": True}) + ) + + # SerialStep has max_workers=1; two samples must not interleave + pipe = Pipeline().then(TriggerBoundary()).then(SerialStep()) + results = pipe.run( + [StepContext(sample="x"), StepContext(sample="y")], workers=2 + ) + pipe.wait_for_background(timeout=5.0) + + log = SerialStep._log + # For correct serialization: start-X must be immediately followed by end-X + for i in range(0, len(log), 2): + assert log[i].startswith("start"), f"log[{i}] = {log[i]}" + sample = log[i].split("-")[1] + assert log[i + 1] == f"end-{sample}", f"Interleaved: {log}" + + +# --------------------------------------------------------------------------- +# run_async() +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestPipelineRunAsync: + def test_run_async_same_results_as_run(self): + pipe = Pipeline().then(SetA()).then(SetB()) + contexts = [StepContext(sample="s1"), StepContext(sample="s2")] + sync_results = pipe.run(contexts) + async_results = asyncio.run(pipe.run_async(contexts)) + assert len(async_results) == 2 + for s, a in zip(sync_results, async_results): + assert s.output == a.output + + def test_run_async_handles_step_failure(self): + pipe = Pipeline().then(Boom()) + results = asyncio.run(pipe.run_async([StepContext(sample="s")])) + assert results[0].error is not None + + def test_run_async_workers_respected(self): + """Multiple samples run concurrently with workers>1.""" + delay = 0.2 + pipe = Pipeline().then(Slow(delay)) + contexts = [StepContext(sample=i) for i in range(4)] + t0 = time.monotonic() + asyncio.run(pipe.run_async(contexts, workers=4)) + elapsed = time.monotonic() - t0 + assert elapsed < delay * 3, f"Expected concurrency, took {elapsed:.2f}s" + + +# --------------------------------------------------------------------------- +# Nesting +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestPipelineNesting: + def test_inner_pipeline_used_as_step(self): + inner = Pipeline().then(SetA()).then(SetB()) + outer = Pipeline().then(inner).then(SetC()) + results = outer.run([StepContext(sample="s")]) + out = results[0].output + assert out.metadata["a"] == 1 + assert out.metadata["b"] == 2 + assert out.metadata["c"] == 4 + + def test_nested_pipeline_contracts_inferred(self): + inner = Pipeline().then(SetA()).then(SetB()) + assert "a" in inner.provides + assert "b" in inner.provides + # Inner pipeline doesn't need anything external + assert inner.requires == frozenset() + + def test_nested_pipeline_satisfies_step_protocol(self): + inner = Pipeline().then(SetA()) + assert isinstance(inner, StepProtocol) + + +# --------------------------------------------------------------------------- +# Contract validation with subclass fields +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestPipelineContractWithSubclass: + """Validate that requires/provides work correctly with subclass named fields.""" + + def test_order_error_when_subclass_field_required_before_provided(self): + """Step B requires a subclass field that Step A provides, but B comes first.""" + from dataclasses import dataclass + from typing import Any + + @dataclass(frozen=True) + class SubCtx(StepContext): + agent_output: Any = None + + class WriteOutput: + requires = frozenset() + provides = frozenset({"agent_output"}) + + def __call__(self, ctx): + return ctx.replace(agent_output="answer") + + class ReadOutput: + requires = frozenset({"agent_output"}) + provides = frozenset({"result"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType( + {**ctx.metadata, "result": ctx.agent_output} + ) + ) + + # Correct order works + p = Pipeline().then(WriteOutput()).then(ReadOutput()) + assert "agent_output" not in p.requires # internally satisfied + + # Wrong order raises + with pytest.raises(PipelineOrderError, match="agent_output"): + Pipeline().then(ReadOutput()).then(WriteOutput()) + + def test_subclass_field_as_external_input(self): + """A step requires a subclass field not provided by any step → external input.""" + + class NeedsOutput: + requires = frozenset({"agent_output"}) + provides = frozenset({"score"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "score": 1.0}) + ) + + p = Pipeline().then(NeedsOutput()) + assert "agent_output" in p.requires # external — caller must provide + + def test_subclass_context_flows_through_pipeline_run(self): + """Pipeline.run() with subclass contexts preserves subclass type.""" + from dataclasses import dataclass + from typing import Any + + @dataclass(frozen=True) + class RunCtx(StepContext): + answer: Any = None + + class SetAnswer: + requires = frozenset() + provides = frozenset({"answer"}) + + def __call__(self, ctx): + return ctx.replace(answer=f"solved_{ctx.sample}") + + results = Pipeline().then(SetAnswer()).run([RunCtx(sample="q1")]) + out = results[0].output + assert isinstance(out, RunCtx) + assert out.answer == "solved_q1" + + def test_subclass_context_with_call_mode(self): + """Pipeline.__call__ with subclass context preserves subclass type.""" + from dataclasses import dataclass + from typing import Any + + @dataclass(frozen=True) + class CallCtx(StepContext): + tag: str = "" + + class SetTag: + requires = frozenset() + provides = frozenset({"tag"}) + + def __call__(self, ctx): + return ctx.replace(tag="tagged") + + out = Pipeline().then(SetTag())(CallCtx(sample="s")) + assert isinstance(out, CallCtx) + assert out.tag == "tagged" diff --git a/tests/pipeline_engine/test_pipeline_e2e.py b/tests/pipeline_engine/test_pipeline_e2e.py new file mode 100644 index 0000000000000000000000000000000000000000..d53babc8c9d8e149f0483e83c67a4296720dfd79 --- /dev/null +++ b/tests/pipeline_engine/test_pipeline_e2e.py @@ -0,0 +1,504 @@ +"""End-to-end tests for the pipeline engine. + +These tests exercise realistic multi-step scenarios — no ACE imports. +Dummy steps simulate the Agent → Evaluate → Reflect → Update pattern using +a test-local StepContext subclass, so the full plumbing is exercised. +""" + +from __future__ import annotations + +import asyncio +import threading +import time +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any + +import pytest + +from pipeline import ( + Branch, + MergeStrategy, + Pipeline, + SampleResult, + StepContext, +) + +# --------------------------------------------------------------------------- +# Test-local context subclass (simulates ACE-style domain fields) +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class E2EContext(StepContext): + """Domain context with named fields for the Agent → Evaluate → Reflect → Update pattern.""" + + agent_output: Any = None + environment_result: Any = None + reflection: Any = None + skill_manager_output: Any = None + + +# --------------------------------------------------------------------------- +# Domain-agnostic dummy steps (ACE-shaped but no ACE imports) +# --------------------------------------------------------------------------- + + +class AgentStep: + """Reads ctx.sample (a named field, not metadata), writes agent_output.""" + + requires = frozenset() + provides = frozenset({"agent_output"}) + + def __call__(self, ctx: E2EContext) -> E2EContext: + return ctx.replace(agent_output=f"answer_for_{ctx.sample}") + + +class EvaluateStep: + """Reads agent_output + environment from metadata, writes environment_result.""" + + requires = frozenset({"agent_output"}) + provides = frozenset({"environment_result"}) + + def __call__(self, ctx: E2EContext) -> E2EContext: + correct = ctx.agent_output == ctx.metadata.get("expected") + return ctx.replace( + environment_result={ + "correct": correct, + "feedback": "ok" if correct else "wrong", + } + ) + + +class ReflectStep: + """Background step: reads agent_output + environment_result, writes reflection.""" + + requires = frozenset({"agent_output", "environment_result"}) + provides = frozenset({"reflection"}) + async_boundary = True + max_workers = 3 + + def __call__(self, ctx: E2EContext) -> E2EContext: + time.sleep(0.01) # simulate LLM latency + return ctx.replace( + reflection={ + "insight": "reflected", + "correct": ctx.environment_result["correct"], + } + ) + + +class UpdateStep: + """Background step: reads reflection, writes skill_manager_output. Serialized.""" + + requires = frozenset({"reflection"}) + provides = frozenset({"skill_manager_output"}) + max_workers = 1 + _updates: list = [] + _lock = threading.Lock() + + def __call__(self, ctx: E2EContext) -> E2EContext: + time.sleep(0.005) + with self._lock: + UpdateStep._updates.append(ctx.reflection) + return ctx.replace(skill_manager_output={"updated": True}) + + +class LogStep: + """Side-effect step that records sample name (for Branch tests).""" + + requires = frozenset() + provides = frozenset() + + def __init__(self): + self.log: list[str] = [] + self._lock = threading.Lock() + + def __call__(self, ctx: StepContext) -> StepContext: + with self._lock: + self.log.append(ctx.sample) + return ctx.replace(metadata=MappingProxyType({**ctx.metadata, "logged": True})) + + +class MetricStep: + """Writes a metric to metadata (for Branch tests alongside ReflectStep).""" + + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "metric": len(str(ctx.sample))}) + ) + + +# --------------------------------------------------------------------------- +# E2E: full 4-step pipeline (no async_boundary first) +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +class TestFullPipelineChain: + def _pipe(self) -> Pipeline: + return Pipeline().then(AgentStep()).then(EvaluateStep()) + + def test_single_sample_correct_answer(self): + sample_ctx_kwargs = {"metadata": {"expected": "answer_for_q1"}} + ctx = E2EContext(sample="q1", **sample_ctx_kwargs) + # Run via __call__ (nested mode) + out = self._pipe()(ctx) + assert out.agent_output == "answer_for_q1" + assert out.environment_result["correct"] is True + + def test_multiple_samples_run(self): + # Samples without expected = all "wrong" + results = self._pipe().run([E2EContext(sample=s) for s in ("q1", "q2", "q3")]) + assert len(results) == 3 + assert all(r.error is None for r in results) + assert all(r.output.agent_output.startswith("answer_for_") for r in results) + + def test_data_flows_correctly_through_chain(self): + results = self._pipe().run([E2EContext(sample="hello")]) + out = results[0].output + assert out.sample == "hello" + assert out.agent_output == "answer_for_hello" + assert out.environment_result is not None + assert "correct" in out.environment_result + + +# --------------------------------------------------------------------------- +# E2E: async_boundary — fire-and-forget pipeline +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +@pytest.mark.slow +class TestAsyncBoundaryPipeline: + def _pipe(self) -> Pipeline: + UpdateStep._updates.clear() + return ( + Pipeline() + .then(AgentStep()) + .then(EvaluateStep()) + .then(ReflectStep()) # async_boundary = True + .then(UpdateStep()) + ) + + def test_run_returns_before_background_finishes(self): + pipe = self._pipe() + t0 = time.monotonic() + results = pipe.run([E2EContext(sample="q1")], workers=1) + foreground_time = time.monotonic() - t0 + # Should return quickly (foreground only: agent + evaluate) + # Background runs Reflect (0.01s) + Update (0.005s) asynchronously + assert len(results) == 1 + pipe.wait_for_background(timeout=5.0) + + def test_all_steps_complete_after_wait(self): + pipe = self._pipe() + results = pipe.run([E2EContext(sample="q1"), E2EContext(sample="q2")]) + pipe.wait_for_background(timeout=5.0) + assert all(r.output is not None for r in results) + assert all(r.output.skill_manager_output == {"updated": True} for r in results) + + def test_update_serialized_across_samples(self): + """UpdateStep.max_workers=1 — updates must not interleave.""" + UpdateStep._updates.clear() + pipe = self._pipe() + contexts = [E2EContext(sample=f"s{i}") for i in range(5)] + pipe.run(contexts, workers=3) + pipe.wait_for_background(timeout=10.0) + # All 5 samples must have triggered an update + assert len(UpdateStep._updates) == 5 + + def test_failed_foreground_not_sent_to_background(self): + class FailEval: + requires = frozenset({"agent_output"}) + provides = frozenset({"environment_result"}) + + def __call__(self, ctx): + raise RuntimeError("eval failed") + + UpdateStep._updates.clear() + pipe = ( + Pipeline() + .then(AgentStep()) + .then(FailEval()) + .then(ReflectStep()) + .then(UpdateStep()) + ) + results = pipe.run([E2EContext(sample="q1")]) + pipe.wait_for_background(timeout=2.0) + # Error in foreground → no background submission + assert results[0].error is not None + assert results[0].failed_at == "FailEval" + assert len(UpdateStep._updates) == 0 + + +# --------------------------------------------------------------------------- +# E2E: Branch inside a pipeline +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +class TestBranchInPipeline: + def test_branch_parallel_reflect_and_log(self): + log_step = LogStep() + pipe = ( + Pipeline() + .then(AgentStep()) + .then(EvaluateStep()) + .branch( + Pipeline().then(MetricStep()), + Pipeline().then(log_step), + merge=MergeStrategy.RAISE_ON_CONFLICT, + ) + ) + results = pipe.run([E2EContext(sample="hello"), E2EContext(sample="world")]) + assert len(results) == 2 + assert all(r.error is None for r in results) + # Both branches ran + assert all(r.output.metadata.get("metric") is not None for r in results) + assert sorted(log_step.log) == ["hello", "world"] + + def test_step_after_branch_receives_merged_context(self): + class Summarize: + requires = frozenset() + provides = frozenset({"summary"}) + + def __call__(self, ctx): + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "summary": "done"}) + ) + + pipe = ( + Pipeline() + .then(AgentStep()) + .branch( + Pipeline().then(MetricStep()), + Pipeline().then(LogStep()), + ) + .then(Summarize()) + ) + results = pipe.run([E2EContext(sample="test")]) + assert results[0].output.metadata.get("summary") == "done" + + def test_branch_failure_captured_in_sample_result(self): + class BranchBoom: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx): + raise RuntimeError("branch_fail") + + pipe = ( + Pipeline() + .then(AgentStep()) + .branch( + Pipeline().then(MetricStep()), + Pipeline().then(BranchBoom()), + ) + ) + results = pipe.run([E2EContext(sample="s")]) + assert results[0].error is not None + assert results[0].failed_at == "Branch" + + +# --------------------------------------------------------------------------- +# E2E: nested pipeline reuse +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +class TestNestedPipelineReuse: + def test_inner_pipeline_reused_in_two_outer_pipelines(self): + inner = Pipeline().then(AgentStep()).then(EvaluateStep()) + + outer_a = Pipeline().then(inner) + outer_b = Pipeline().then(inner).then(MetricStep()) + + r_a = outer_a.run([E2EContext(sample="q1")]) + r_b = outer_b.run([E2EContext(sample="q1")]) + + assert r_a[0].output.agent_output == "answer_for_q1" + assert r_b[0].output.metadata.get("metric") is not None + + def test_deeply_nested_pipelines(self): + level1 = Pipeline().then(AgentStep()) + level2 = Pipeline().then(level1).then(EvaluateStep()) + level3 = Pipeline().then(level2).then(MetricStep()) + + results = level3.run([E2EContext(sample="deep")]) + out = results[0].output + assert out.agent_output == "answer_for_deep" + assert out.environment_result is not None + assert out.metadata.get("metric") is not None + + +# --------------------------------------------------------------------------- +# E2E: multiple run() calls on same instance +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +class TestMultipleRunCalls: + def test_two_run_calls_accumulate_background_work(self): + UpdateStep._updates.clear() + pipe = ( + Pipeline() + .then(AgentStep()) + .then(EvaluateStep()) + .then(ReflectStep()) + .then(UpdateStep()) + ) + pipe.run([E2EContext(sample="a"), E2EContext(sample="b")]) + pipe.run([E2EContext(sample="c"), E2EContext(sample="d")]) + pipe.wait_for_background(timeout=10.0) + assert len(UpdateStep._updates) == 4 + + def test_pipeline_state_not_contaminated_between_runs(self): + pipe = Pipeline().then(AgentStep()).then(EvaluateStep()) + r1 = pipe.run([E2EContext(sample="sample_x")]) + r2 = pipe.run([E2EContext(sample="sample_y")]) + assert r1[0].output.sample == "sample_x" + assert r2[0].output.sample == "sample_y" + + +# --------------------------------------------------------------------------- +# E2E: async steps inside pipeline +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +class TestAsyncStepsInPipeline: + def test_async_step_runs_via_run(self): + class AsyncAgent: + requires = frozenset() + provides = frozenset({"agent_output"}) + + async def __call__(self, ctx: E2EContext) -> E2EContext: + await asyncio.sleep(0) + return ctx.replace(agent_output="async_answer") + + results = Pipeline().then(AsyncAgent()).run([E2EContext(sample="s")]) + assert results[0].output.agent_output == "async_answer" + + def test_mixed_sync_async_steps(self): + class AsyncAgent: + requires = frozenset() + provides = frozenset({"agent_output"}) + + async def __call__(self, ctx: E2EContext) -> E2EContext: + await asyncio.sleep(0) + return ctx.replace(agent_output="async") + + class SyncEval: + requires = frozenset({"agent_output"}) + provides = frozenset({"environment_result"}) + + def __call__(self, ctx: E2EContext) -> E2EContext: + return ctx.replace(environment_result={"score": 1.0}) + + results = ( + Pipeline().then(AsyncAgent()).then(SyncEval()).run([E2EContext(sample="s")]) + ) + assert results[0].output.agent_output == "async" + assert results[0].output.environment_result["score"] == 1.0 + + def test_run_async_entry_point(self): + contexts = [E2EContext(sample="q1"), E2EContext(sample="q2")] + results = asyncio.run( + Pipeline().then(AgentStep()).then(EvaluateStep()).run_async(contexts) + ) + assert len(results) == 2 + assert all(r.error is None for r in results) + + +# --------------------------------------------------------------------------- +# E2E: background executor shared across pipeline instances +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +@pytest.mark.slow +class TestSharedBackgroundExecutor: + def test_same_step_class_uses_same_executor(self): + """Two Pipeline instances sharing the same step class share the executor.""" + + class SharedBg: + requires = frozenset() + provides = frozenset({"done"}) + async_boundary = True + max_workers = 1 # single-threaded shared pool + call_count = 0 + _lock = threading.Lock() + + def __call__(self, ctx): + with SharedBg._lock: + SharedBg.call_count += 1 + time.sleep(0.01) + return ctx.replace( + metadata=MappingProxyType({**ctx.metadata, "done": True}) + ) + + SharedBg.call_count = 0 + # Reset class executor so the test is independent + if hasattr(SharedBg, "_executor") and SharedBg._executor is not None: + SharedBg._executor.shutdown(wait=False) + SharedBg._executor = None + + pipe_a = Pipeline().then(SharedBg()) + pipe_b = Pipeline().then(SharedBg()) + + results_a = pipe_a.run([E2EContext(sample="a1"), E2EContext(sample="a2")]) + results_b = pipe_b.run([E2EContext(sample="b1"), E2EContext(sample="b2")]) + + pipe_a.wait_for_background(timeout=5.0) + pipe_b.wait_for_background(timeout=5.0) + + assert SharedBg.call_count == 4 + assert all(r.output.metadata.get("done") for r in results_a + results_b) + + +# --------------------------------------------------------------------------- +# E2E: iterable (non-list) inputs +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +class TestIterableInputs: + def test_generator_input(self): + """Pipeline.run() accepts a generator, not only a list.""" + + def gen(): + for s in ("a", "b", "c"): + yield E2EContext(sample=s) + + results = Pipeline().then(AgentStep()).run(gen()) + assert len(results) == 3 + assert all(r.error is None for r in results) + assert {r.output.agent_output for r in results} == { + "answer_for_a", + "answer_for_b", + "answer_for_c", + } + + def test_tuple_input(self): + """Pipeline.run() accepts a tuple of contexts.""" + contexts = tuple(E2EContext(sample=s) for s in ("x", "y")) + results = Pipeline().then(AgentStep()).then(EvaluateStep()).run(contexts) + assert len(results) == 2 + assert all(r.error is None for r in results) + + def test_run_async_with_generator(self): + """Pipeline.run_async() also accepts a generator.""" + + def gen(): + for s in ("q1", "q2"): + yield E2EContext(sample=s) + + results = asyncio.run( + Pipeline().then(AgentStep()).then(EvaluateStep()).run_async(gen()) + ) + assert len(results) == 2 + assert all(r.error is None for r in results) diff --git a/tests/pipeline_engine/test_protocol.py b/tests/pipeline_engine/test_protocol.py new file mode 100644 index 0000000000000000000000000000000000000000..3286c96f3c3735e495ff6528ddf0f1114c71919a --- /dev/null +++ b/tests/pipeline_engine/test_protocol.py @@ -0,0 +1,135 @@ +"""Unit tests for StepProtocol and SampleResult.""" + +from __future__ import annotations + +import pytest + +from pipeline import SampleResult, StepContext, StepProtocol + +# --------------------------------------------------------------------------- +# Helper objects +# --------------------------------------------------------------------------- + + +class ValidStep: + requires = frozenset({"a"}) + provides = frozenset({"b"}) + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx + + +class ValidStepWithPlainSets: + requires = {"a"} # plain set, not frozenset + provides = {"b"} + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx + + +class MissingRequires: + provides = frozenset({"b"}) + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx + + +class MissingProvides: + requires = frozenset({"a"}) + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx + + +class MissingCall: + requires = frozenset({"a"}) + provides = frozenset({"b"}) + + +class EmptyStep: + """Valid step with empty requires/provides (e.g. a pure side-effect step).""" + + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx + + +# --------------------------------------------------------------------------- +# StepProtocol +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestStepProtocol: + def test_valid_step_passes_isinstance(self): + assert isinstance(ValidStep(), StepProtocol) + + def test_valid_step_with_plain_sets_passes_isinstance(self): + # AbstractSet[str] accepts both set and frozenset + assert isinstance(ValidStepWithPlainSets(), StepProtocol) + + def test_empty_requires_provides_is_valid(self): + assert isinstance(EmptyStep(), StepProtocol) + + def test_missing_requires_fails_isinstance(self): + assert not isinstance(MissingRequires(), StepProtocol) + + def test_missing_provides_fails_isinstance(self): + assert not isinstance(MissingProvides(), StepProtocol) + + def test_missing_call_fails_isinstance(self): + assert not isinstance(MissingCall(), StepProtocol) + + def test_plain_object_fails_isinstance(self): + assert not isinstance(object(), StepProtocol) + + def test_none_fails_isinstance(self): + assert not isinstance(None, StepProtocol) + + +# --------------------------------------------------------------------------- +# SampleResult +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestSampleResult: + def test_basic_construction(self): + ctx = StepContext(sample="s") + r = SampleResult(sample="s", output=ctx, error=None, failed_at=None) + assert r.sample == "s" + assert r.output is ctx + assert r.error is None + assert r.failed_at is None + + def test_cause_defaults_to_none(self): + r = SampleResult(sample="s", output=None, error=None, failed_at=None) + assert r.cause is None + + def test_is_mutable(self): + r = SampleResult(sample="s", output=None, error=None, failed_at=None) + exc = RuntimeError("oops") + r.error = exc + r.failed_at = "SomeStep" + assert r.error is exc + assert r.failed_at == "SomeStep" + + def test_failure_result(self): + exc = RuntimeError("boom") + r = SampleResult(sample="x", output=None, error=exc, failed_at="BoomStep") + assert r.output is None + assert r.error is exc + assert r.failed_at == "BoomStep" + + def test_branch_failure_has_cause(self): + inner = RuntimeError("inner") + from pipeline import BranchError + + outer = BranchError([inner]) + r = SampleResult( + sample="x", output=None, error=outer, failed_at="Branch", cause=inner + ) + assert r.cause is inner + assert r.failed_at == "Branch" diff --git a/tests/test_ace_core.py b/tests/test_ace_core.py new file mode 100644 index 0000000000000000000000000000000000000000..88c40da93ccf14d18b443f5f66c688df7556d31d --- /dev/null +++ b/tests/test_ace_core.py @@ -0,0 +1,556 @@ +"""Tests for ace core: Skillbook, SkillbookView, ACEStepContext.""" + +from __future__ import annotations + +import json +import threading +from dataclasses import FrozenInstanceError +from unittest.mock import patch + +import pytest + +from ace.core.context import ACEStepContext, SkillbookView +from ace.core.insight_source import InsightSource +from ace.core.outputs import AgentOutput, ReflectorOutput +from ace.core.skillbook import ( + Skill, + Skillbook, + UpdateBatch, + UpdateOperation, +) + +# ------------------------------------------------------------------ # +# Skillbook CRUD +# ------------------------------------------------------------------ # + + +class TestSkillbookCRUD: + def test_add_and_get_skill(self): + sb = Skillbook() + skill = sb.add_skill("math", "Use division for fractions") + assert skill.section == "context" + assert skill.keywords == ["math"] + assert skill.issue == "Use division for fractions" + assert skill.insight == "Use division for fractions" + assert sb.get_skill(skill.id) is skill + + def test_add_skill_custom_id(self): + sb = Skillbook() + skill = sb.add_skill("math", "issue", skill_id="custom-001") + assert skill.id == "custom-001" + assert sb.get_skill("custom-001") is skill + + def test_update_skill(self): + sb = Skillbook() + skill = sb.add_skill("math", "old content") + updated = sb.update_skill(skill.id, insight="new content") + assert updated is not None + assert updated.insight == "new content" + + def test_update_nonexistent_skill(self): + sb = Skillbook() + assert sb.update_skill("missing-id", insight="x") is None + + def test_remove_skill_hard(self): + sb = Skillbook() + skill = sb.add_skill("math", "issue") + sb.remove_skill(skill.id, soft=False) + assert sb.get_skill(skill.id) is None + assert len(sb.skills()) == 0 + + def test_remove_skill_soft(self): + sb = Skillbook() + skill = sb.add_skill("math", "issue") + sb.remove_skill(skill.id) + assert sb.get_skill(skill.id) is not None + assert skill.active is False + assert len(sb.skills()) == 0 # active only + assert len(sb.skills(include_invalid=True)) == 1 + + def test_remove_nonexistent_skill(self): + sb = Skillbook() + sb.remove_skill("missing-id") # should not raise + + def test_skills_list(self): + sb = Skillbook() + sb.add_skill("math", "a") + sb.add_skill("math", "b") + sb.add_skill("writing", "c") + assert len(sb.skills()) == 3 + + def test_generate_id_increments(self): + sb = Skillbook() + s1 = sb.add_skill("math", "a") + s2 = sb.add_skill("math", "b") + assert s1.id != s2.id + assert s1.id.startswith("context-") + assert s2.id.startswith("context-") + + +# ------------------------------------------------------------------ # +# Skillbook serialization +# ------------------------------------------------------------------ # + + +class TestSkillbookSerialization: + def test_round_trip(self): + sb = Skillbook() + sb.add_skill("math", "content A", skill_id="math-001") + sb.add_skill("writing", "content B", skill_id="writing-001") + + data = sb.to_dict() + restored = Skillbook.from_dict(data) + + assert len(restored.skills()) == 2 + assert restored.get_skill("math-001").issue == "content A" + assert restored.get_skill("writing-001").issue == "content B" + + def test_json_round_trip(self): + sb = Skillbook() + sb.add_skill("sec", "content", skill_id="sec-001") + json_str = sb.dumps() + restored = Skillbook.loads(json_str) + assert restored.get_skill("sec-001").issue == "content" + + def test_file_round_trip(self, tmp_path): + sb = Skillbook() + sb.add_skill("sec", "content", skill_id="sec-001") + path = str(tmp_path / "sb.json") + sb.save_to_file(path) + restored = Skillbook.load_from_file(path) + assert restored.get_skill("sec-001").issue == "content" + + def test_load_nonexistent_file(self): + with pytest.raises(FileNotFoundError): + Skillbook.load_from_file("/nonexistent/path.json") + + def test_loads_invalid_json(self): + with pytest.raises((json.JSONDecodeError, ValueError)): + Skillbook.loads("not json") + + def test_from_dict_malformed_sections(self): + """v2 loads require an explicit schema version.""" + payload = { + "skills": {}, + "sections": {"bad": "not-a-list"}, + "next_id": 0, + } + with pytest.raises(ValueError, match="Skillbook format v2 required"): + Skillbook.from_dict(payload) + + def test_from_dict_missing_fields(self): + """Missing optional fields should use defaults.""" + payload = { + "schema_version": "2", + "skills": { + "s1": { + "id": "s1", + "section": "context", + "keywords": ["math"], + "issue": "x", + "insight": "x", + "created_at": "2025-01-01T00:00:00", + "updated_at": "2025-01-01T00:00:00", + } + }, + "sections": {"context": ["s1"]}, + } + sb = Skillbook.from_dict(payload) + skill = sb.get_skill("s1") + assert skill is not None + assert skill.embedding is None + assert skill.active is True + assert skill.occurrences == [] + + def test_sources_round_trip(self): + sb = Skillbook() + sb.add_skill( + "api", + "Check for a next-page token before stopping.", + skill_id="api-001", + insight_source=InsightSource( + trace_uid="kayba-hosted:conv-123", + source_system="kayba-hosted", + trace_id="conv-123", + display_name="checkout-failure.md", + sample_question="Why did pagination stop early?", + epoch=1, + ), + ) + + restored = Skillbook.from_dict(sb.to_dict()) + skill = restored.get_skill("api-001") + + assert skill is not None + assert skill.occurrences[0].trace_id == "conv-123" + assert skill.occurrences[0].epoch == 1 + assert skill.occurrences[0].sample_question == "Why did pagination stop early?" + + def test_source_summary_and_filter_include_trace_identity(self): + sb = Skillbook() + sb.add_skill( + "api", + "Check for a next-page token before stopping.", + skill_id="api-001", + insight_source=InsightSource( + trace_uid="kayba-hosted:conv-123", + source_system="kayba-hosted", + trace_id="conv-123", + display_name="checkout-failure.md", + sample_question="Why did pagination stop early?", + epoch=2, + ), + ) + + summary = sb.source_summary() + filtered = sb.source_filter(trace_uid="kayba-hosted:conv-123") + + assert summary["source_systems"]["kayba-hosted"] == 1 + assert summary["trace_uids"]["kayba-hosted:conv-123"] == 1 + assert filtered["api-001"][0]["trace_id"] == "conv-123" + + def test_update_skill_dedupes_identical_sources(self): + sb = Skillbook() + source = InsightSource( + trace_uid="synthetic:trace-001", + source_system="synthetic", + trace_id="trace-001", + display_name="trace-001", + ) + + sb.add_skill( + "api", + "Always check the continuation token.", + skill_id="api-001", + insight_source=source, + ) + sb.update_skill("api-001", insight_source=source) + + skill = sb.get_skill("api-001") + assert skill is not None + assert len(skill.occurrences) == 1 + + def test_add_skill_accepts_multiple_sources(self): + sb = Skillbook() + sb.add_skill( + "api", + "Generalize pagination handling across traces.", + skill_id="api-001", + insight_source=[ + InsightSource( + trace_uid="synthetic:trace-001", + source_system="synthetic", + trace_id="trace-001", + display_name="trace-001", + ), + InsightSource( + trace_uid="synthetic:trace-002", + source_system="synthetic", + trace_id="trace-002", + display_name="trace-002", + relation="supporting", + ), + ], + ) + + skill = sb.get_skill("api-001") + assert skill is not None + assert len(skill.occurrences) == 2 + assert skill.occurrences[0].trace_id == "trace-001" + assert skill.occurrences[1].trace_id == "trace-002" + + +# ------------------------------------------------------------------ # +# Skillbook update operations +# ------------------------------------------------------------------ # + + +class TestSkillbookUpdates: + def test_apply_add(self): + sb = Skillbook() + batch = UpdateBatch( + reasoning="test", + operations=[UpdateOperation(type="ADD", section="math", issue="new skill")], + ) + sb.apply_update(batch) + assert len(sb.skills()) == 1 + assert sb.skills()[0].issue == "new skill" + + def test_apply_update(self): + sb = Skillbook() + skill = sb.add_skill("math", "old", skill_id="math-001") + batch = UpdateBatch( + reasoning="test", + operations=[ + UpdateOperation( + type="UPDATE", + section="math", + insight="new", + skill_id="math-001", + ) + ], + ) + sb.apply_update(batch) + assert skill.insight == "new" + + def test_apply_tag_is_noop(self): + """TAG operations are accepted but no longer modify skills.""" + sb = Skillbook() + sb.add_skill("math", "issue", skill_id="math-001") + batch = UpdateBatch( + reasoning="test", + operations=[ + UpdateOperation( + type="TAG", + section="math", + skill_id="math-001", + metadata={"helpful": 1}, + ) + ], + ) + sb.apply_update(batch) + assert sb.get_skill("math-001") is not None + + def test_apply_remove(self): + sb = Skillbook() + sb.add_skill("math", "issue", skill_id="math-001") + batch = UpdateBatch( + reasoning="test", + operations=[ + UpdateOperation(type="REMOVE", section="math", skill_id="math-001") + ], + ) + sb.apply_update(batch) + skill = sb.get_skill("math-001") + assert skill is not None + assert skill.active is False + + def test_apply_update_missing_skill_id(self): + """UPDATE/TAG/REMOVE without skill_id should be skipped silently.""" + sb = Skillbook() + batch = UpdateBatch( + reasoning="test", + operations=[ + UpdateOperation(type="UPDATE", section="math", insight="x"), + UpdateOperation(type="TAG", section="math", metadata={"helpful": 1}), + UpdateOperation(type="REMOVE", section="math"), + ], + ) + sb.apply_update(batch) # should not raise + assert len(sb.skills()) == 0 + + +# ------------------------------------------------------------------ # +# Skillbook thread safety +# ------------------------------------------------------------------ # + + +class TestSkillbookThreadSafety: + def test_concurrent_add_and_update(self): + """Concurrent add_skill and update_skill should not corrupt state.""" + sb = Skillbook() + errors = [] + n_add = 50 + n_update = 50 + + def adder(): + try: + for i in range(n_add): + sb.add_skill("concurrent", f"skill-{i}") + except Exception as e: + errors.append(e) + + def updater(): + try: + for _ in range(n_update): + skills = sb.skills() + if skills: + sb.update_skill(skills[0].id, insight="updated") + except Exception as e: + errors.append(e) + + threads = [ + threading.Thread(target=adder), + threading.Thread(target=updater), + threading.Thread(target=adder), + threading.Thread(target=updater), + ] + for t in threads: + t.start() + for t in threads: + t.join() + + assert errors == [], f"Thread safety errors: {errors}" + # All skills should be present (2 adders × 50 each) + assert len(sb.skills()) == n_add * 2 + + def test_lock_is_reentrant(self): + """apply_update calls add_skill internally — lock must be reentrant.""" + sb = Skillbook() + batch = UpdateBatch( + reasoning="test", + operations=[ + UpdateOperation(type="ADD", section="sec", issue="a"), + UpdateOperation(type="ADD", section="sec", issue="b"), + ], + ) + sb.apply_update(batch) + assert len(sb.skills()) == 2 + + +# ------------------------------------------------------------------ # +# SkillbookView +# ------------------------------------------------------------------ # + + +class TestSkillbookView: + def test_read_methods(self): + sb = Skillbook() + sb.add_skill("math", "content", skill_id="m-001") + view = SkillbookView(sb) + + assert len(view) == 1 + assert view.get_skill("m-001").issue == "content" + assert len(view.skills()) == 1 + assert "skills" in view.stats() + + def test_no_write_methods(self): + sb = Skillbook() + view = SkillbookView(sb) + + assert not hasattr(view, "add_skill") + assert not hasattr(view, "update_skill") + assert not hasattr(view, "remove_skill") + assert not hasattr(view, "apply_update") + + def test_iteration(self): + sb = Skillbook() + sb.add_skill("a", "x") + sb.add_skill("b", "y") + view = SkillbookView(sb) + + skills = list(view) + assert len(skills) == 2 + + def test_repr(self): + sb = Skillbook() + sb.add_skill("a", "x") + view = SkillbookView(sb) + assert "1 skills" in repr(view) + + +# ------------------------------------------------------------------ # +# ACEStepContext +# ------------------------------------------------------------------ # + + +class TestACEStepContext: + def test_frozen(self): + ctx = ACEStepContext(sample="test") + with pytest.raises(FrozenInstanceError): + ctx.sample = "other" + + def test_replace(self): + ctx = ACEStepContext(sample="test", epoch=1) + ctx2 = ctx.replace(epoch=2) + assert ctx.epoch == 1 + assert ctx2.epoch == 2 + + def test_defaults(self): + ctx = ACEStepContext() + assert ctx.sample is None + assert ctx.skillbook is None + assert ctx.trace is None + assert ctx.agent_output is None + assert ctx.reflections == () + assert ctx.skill_manager_output is None + assert ctx.epoch == 1 + assert ctx.total_epochs == 1 + assert ctx.step_index == 0 + + def test_replace_with_skillbook_view(self): + sb = Skillbook() + view = SkillbookView(sb) + ctx = ACEStepContext(skillbook=view) + assert ctx.skillbook is view + + def test_replace_with_outputs(self): + agent_out = AgentOutput(reasoning="r", final_answer="a") + ctx = ACEStepContext() + ctx2 = ctx.replace(agent_output=agent_out) + assert ctx2.agent_output is agent_out + assert ctx.agent_output is None # original unchanged + + +# ------------------------------------------------------------------ # +# UpdateOperation / UpdateBatch parsing +# ------------------------------------------------------------------ # + + +class TestUpdateOperationParsing: + def test_from_json_add(self): + op = UpdateOperation.from_json( + {"type": "ADD", "section": "math", "issue": "skill content"} + ) + assert op.type == "ADD" + assert op.section == "math" + assert op.issue == "skill content" + + def test_from_json_parses_reflection_index(self): + op = UpdateOperation.from_json( + { + "type": "ADD", + "section": "math", + "issue": "skill content", + "learning_index": 1, + "reflection_index": 2, + "reflection_indices": [0, 2], + } + ) + + assert op.learning_index == 1 + assert op.reflection_index == 2 + assert op.reflection_indices == [0, 2] + assert op.to_json()["reflection_index"] == 2 + assert op.to_json()["reflection_indices"] == [0, 2] + + def test_from_json_tag_accepted(self): + """TAG operations are parsed for backwards compatibility.""" + op = UpdateOperation.from_json( + { + "type": "TAG", + "section": "math", + "skill_id": "m-001", + "metadata": {"helpful": 1}, + } + ) + assert op.type == "TAG" + assert op.metadata == {"helpful": 1} + + def test_from_json_invalid_type(self): + with pytest.raises(ValueError, match="Invalid operation type"): + UpdateOperation.from_json({"type": "INVALID", "section": "x"}) + + def test_batch_from_json(self): + batch = UpdateBatch.from_json( + { + "reasoning": "test reasoning", + "operations": [ + {"type": "ADD", "section": "a", "issue": "x"}, + {"type": "ADD", "section": "b", "issue": "y"}, + ], + } + ) + assert batch.reasoning == "test reasoning" + assert len(batch.operations) == 2 + + def test_batch_round_trip(self): + batch = UpdateBatch( + reasoning="r", + operations=[UpdateOperation(type="ADD", section="s", issue="c")], + ) + data = batch.to_json() + restored = UpdateBatch.from_json(data) + assert restored.reasoning == "r" + assert len(restored.operations) == 1 + assert restored.operations[0].type == "ADD" diff --git a/tests/test_ace_mcp_compatibility.py b/tests/test_ace_mcp_compatibility.py new file mode 100644 index 0000000000000000000000000000000000000000..3389c0caf71d8f84a135ee65a8d98f73223156ff --- /dev/null +++ b/tests/test_ace_mcp_compatibility.py @@ -0,0 +1,171 @@ +"""Focused compatibility tests for ACE's generic MCP server surface.""" + +from __future__ import annotations + +import json +import re +from unittest.mock import MagicMock, patch + +import pytest + +from ace.integrations.mcp.adapters import _MCP_INSTALL_HINT as ADAPTERS_INSTALL_HINT +from ace.integrations.mcp.adapters import _mcp_schema, register_tools +from ace.integrations.mcp.config import MCPServerConfig +from ace.integrations.mcp.errors import ( + ForbiddenInSafeModeError, + SessionNotFoundError, + ValidationError as ACEValidationError, + map_error_to_mcp, +) +from ace.integrations.mcp.handlers import MCPHandlers +from ace.integrations.mcp.models import AskRequest, LearnSampleRequest +from ace.integrations.mcp.registry import SessionRegistry +from ace.integrations.mcp.server import _MCP_INSTALL_HINT as SERVER_INSTALL_HINT + +_CLIENT_PATTERN = re.compile( + r"(vs\s*code|vscode|visual\s*studio\s*code|cursor|windsurf)", + re.IGNORECASE, +) + + +def _require_mcp(): + pytest.importorskip("mcp.server") + pytest.importorskip("mcp.types") + + from mcp.server import Server + from mcp.types import CallToolRequest, ListToolsRequest + + return Server, CallToolRequest, ListToolsRequest + + +def test_ask_request_schema_is_inlined(): + schema = _mcp_schema(AskRequest) + schema_str = json.dumps(schema) + + assert "$ref" not in schema_str + assert "$defs" not in schema_str + assert "session_id" in schema.get("properties", {}) + assert "question" in schema.get("properties", {}) + + +def test_nested_schema_is_inlined(): + schema = _mcp_schema(LearnSampleRequest) + schema_str = json.dumps(schema) + + assert "$ref" not in schema_str + assert "$defs" not in schema_str + assert "samples" in schema.get("properties", {}) + + +def test_install_hints_are_client_agnostic(): + assert not _CLIENT_PATTERN.search(SERVER_INSTALL_HINT) + assert not _CLIENT_PATTERN.search(ADAPTERS_INSTALL_HINT) + + +def test_error_messages_are_client_agnostic(): + for err in ( + SessionNotFoundError("session-1"), + ForbiddenInSafeModeError("ace.learn.sample"), + ACEValidationError("prompt too long", details={"field": "question"}), + RuntimeError("boom"), + ): + mapped = map_error_to_mcp(err) + assert not _CLIENT_PATTERN.search(mapped["message"]) + + +@pytest.fixture +def wired_server(): + Server, _, _ = _require_mcp() + + config = MCPServerConfig(safe_mode=False) + registry = SessionRegistry(config) + handlers = MCPHandlers(registry, config) + server = Server("ace-mcp-server") + register_tools(server, handlers) + return server, registry + + +@pytest.mark.asyncio +async def test_published_tool_schemas_are_inlined(wired_server): + server, _ = wired_server + _, _, ListToolsRequest = _require_mcp() + + handler = server.request_handlers.get(ListToolsRequest) + assert handler is not None + + result = await handler(MagicMock()) + for tool in result.root.tools: + schema_str = json.dumps(tool.inputSchema) + assert "$ref" not in schema_str + assert "$defs" not in schema_str + assert not _CLIENT_PATTERN.search(tool.description or "") + + +@pytest.mark.asyncio +async def test_call_tool_ace_ask_returns_json_payload(wired_server): + server, _ = wired_server + _, CallToolRequest, _ = _require_mcp() + + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + runner.ask.return_value = "The answer is 42." + runner.skillbook.skills.return_value = [] + mock_runner_cls.from_model.return_value = runner + + handler = server.request_handlers.get(CallToolRequest) + assert handler is not None + + req = MagicMock() + req.params.name = "ace.ask" + req.params.arguments = { + "session_id": "generic-client-1", + "question": "What is the meaning of life?", + } + + result = await handler(req) + assert not result.root.isError + + payload = json.loads(result.root.content[0].text) + assert payload["answer"] == "The answer is 42." + assert payload["session_id"] == "generic-client-1" + + +@pytest.mark.asyncio +async def test_call_tool_unknown_tool_returns_structured_error(wired_server): + server, _ = wired_server + _, CallToolRequest, _ = _require_mcp() + + handler = server.request_handlers.get(CallToolRequest) + assert handler is not None + + req = MagicMock() + req.params.name = "nonexistent.tool" + req.params.arguments = {} + + result = await handler(req) + assert result.root.isError + + payload = json.loads(result.root.content[0].text) + assert payload["code"] == "ACE_MCP_INTERNAL_ERROR" + assert "Unknown tool" in payload["message"] + + +@pytest.mark.asyncio +async def test_session_ids_are_opaque_strings(): + config = MCPServerConfig() + registry = SessionRegistry(config) + + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + mock_runner_cls.from_model.side_effect = lambda *a, **kw: MagicMock() + + ids = [ + "simple-id", + "uuid-550e8400-e29b-41d4-a716-446655440000", + "cursor/project/session-1", + "claude-code:workspace:12345", + ] + + sessions = [await registry.get_or_create(session_id) for session_id in ids] + + assert [session.session_id for session in sessions] == ids + assert len({id(session.runner) for session in sessions}) == len(ids) diff --git a/tests/test_ace_mcp_handlers.py b/tests/test_ace_mcp_handlers.py new file mode 100644 index 0000000000000000000000000000000000000000..8e474314e10e21ac24228d4c2afff7071298d16e --- /dev/null +++ b/tests/test_ace_mcp_handlers.py @@ -0,0 +1,489 @@ +import asyncio +import json + +import pytest +from unittest.mock import MagicMock, patch +from ace.integrations.mcp.config import MCPServerConfig +from ace.integrations.mcp.registry import SessionRegistry +from ace.integrations.mcp.handlers import MCPHandlers +from ace.integrations.mcp.models import ( + AskRequest, + LearnSampleRequest, + LearnFeedbackRequest, + SkillbookGetRequest, + SkillbookSaveRequest, + SkillbookLoadRequest, + SampleItem, +) +from ace.integrations.mcp.errors import ( + ForbiddenInSafeModeError, + SaveLoadDisabledError, + ValidationError, + map_error_to_mcp, +) +from ace.integrations.mcp.errors import TimeoutError as MCPTimeoutError + + +@pytest.fixture +def config(): + return MCPServerConfig(safe_mode=False) + + +@pytest.fixture +def registry(config): + return SessionRegistry(config) + + +@pytest.fixture +def handlers(registry, config): + return MCPHandlers(registry, config) + + +# ── ace.ask ────────────────────────────────────────────────────── + + +@pytest.mark.asyncio +async def test_handle_ask(handlers): + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + runner.ask.return_value = "answer" + runner.skillbook.skills.return_value = [1, 2, 3] + mock_runner_cls.from_model.return_value = runner + + req = AskRequest(session_id="s1", question="q") + resp = await handlers.handle_ask(req) + + assert resp.answer == "answer" + assert resp.skill_count == 3 + # applied_skill_ids was removed from the response model + assert "applied_skill_ids" not in resp.model_fields + runner.ask.assert_called_once() + + +@pytest.mark.asyncio +async def test_handle_ask_enforces_prompt_limit(handlers): + handlers.config.max_prompt_chars = 10 + req = AskRequest(session_id="s1", question="12345678901") + with pytest.raises(ValidationError): + await handlers.handle_ask(req) + + +# ── ace.skillbook.get ──────────────────────────────────────────── + + +@pytest.mark.asyncio +async def test_handle_skillbook_get(handlers, registry): + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + mock_skill = MagicMock() + mock_skill.id = "k1" + mock_skill.insight = "cont" + mock_skill.issue = "cont" + mock_skill.section = "test" + mock_skill.helpful_count = 1 + mock_skill.harmful_count = 0 + mock_skill.neutral_count = 0 + runner.skillbook.skills.return_value = [mock_skill] + runner.skillbook.stats.return_value = {"skills": 1} + mock_runner_cls.from_model.return_value = runner + + await registry.get_or_create("s1") + req = SkillbookGetRequest(session_id="s1") + resp = await handlers.handle_skillbook_get(req) + + assert len(resp.skills) == 1 + assert resp.skills[0].id == "k1" + assert resp.stats["skills"] == 1 + + +@pytest.mark.asyncio +async def test_handle_skillbook_get_uses_skill_type(handlers, registry): + """When skills are actual Skill dataclass instances, use direct access.""" + from ace.core.skillbook import Skill + + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + skill = Skill( + id="s1", + section="context", + keywords=["topic-a"], + issue="do X", + insight="do X", + ) + runner.skillbook.skills.return_value = [skill] + runner.skillbook.stats.return_value = {"skills": 1} + mock_runner_cls.from_model.return_value = runner + + await registry.get_or_create("s1") + req = SkillbookGetRequest(session_id="s1") + resp = await handlers.handle_skillbook_get(req) + + assert resp.skills[0].id == "s1" + assert resp.skills[0].topic == "context" + assert resp.skills[0].content == "do X" + assert resp.skills[0].helpful == 0 + assert resp.skills[0].harmful == 0 + assert resp.skills[0].neutral == 0 + + +# ── ace.learn.sample ───────────────────────────────────────────── + + +@pytest.mark.asyncio +async def test_handle_learn_sample_safe_mode(handlers): + handlers.config.safe_mode = True + req = LearnSampleRequest(session_id="s1", samples=[SampleItem(question="q")]) + with pytest.raises(ForbiddenInSafeModeError): + await handlers.handle_learn_sample(req) + + +@pytest.mark.asyncio +async def test_handle_learn_sample_enforces_runtime_sample_limit(handlers): + handlers.config.max_samples_per_call = 1 + req = LearnSampleRequest( + session_id="s1", + samples=[SampleItem(question="q1"), SampleItem(question="q2")], + ) + with pytest.raises(ValidationError): + await handlers.handle_learn_sample(req) + + +# ── ace.learn.feedback ─────────────────────────────────────────── + + +@pytest.mark.asyncio +async def test_handle_learn_feedback_uses_trace_learning(handlers): + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + runner.skillbook.skills.side_effect = [["a"], ["a", "b"]] + runner.learn_from_feedback.return_value = False + runner.learn_from_traces.return_value = [] + mock_runner_cls.from_model.return_value = runner + + req = LearnFeedbackRequest( + session_id="s1", + question="q", + answer="a", + feedback="good", + ) + resp = await handlers.handle_learn_feedback(req) + + assert resp.learned is True + assert resp.new_skill_count == 1 + runner.learn_from_traces.assert_called_once() + + +@pytest.mark.asyncio +async def test_handle_learn_feedback_always_reports_learned_true(handlers): + """learned=True on success even when no new skills are created.""" + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + runner.skillbook.skills.side_effect = [["a"], ["a"]] + runner.learn_from_feedback.return_value = True + mock_runner_cls.from_model.return_value = runner + + req = LearnFeedbackRequest( + session_id="s1", + question="q", + answer="a", + feedback="good", + ) + resp = await handlers.handle_learn_feedback(req) + + assert resp.learned is True + assert resp.new_skill_count == 0 + + +@pytest.mark.asyncio +async def test_handle_learn_feedback_trace_uses_context_not_reasoning(handlers): + """Fallback trace must map context to 'context', not 'reasoning'.""" + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + runner.skillbook.skills.side_effect = [[], []] + runner.learn_from_feedback.return_value = False + runner.learn_from_traces.return_value = [] + mock_runner_cls.from_model.return_value = runner + + req = LearnFeedbackRequest( + session_id="s1", + question="q", + answer="a", + feedback="good", + context="some background", + ) + await handlers.handle_learn_feedback(req) + + trace = runner.learn_from_traces.call_args[0][0][0] + assert trace["context"] == "some background" + assert "reasoning" not in trace + + +# ── ace.skillbook.save/load ────────────────────────────────────── + + +@pytest.mark.asyncio +async def test_handle_save_safe_mode(handlers, registry): + handlers.config.safe_mode = True + with patch("ace.integrations.mcp.registry.ACELiteLLM"): + await registry.get_or_create("s1") + req = SkillbookSaveRequest(session_id="s1", path="/tmp/some") + with pytest.raises(ForbiddenInSafeModeError): + await handlers.handle_skillbook_save(req) + + +@pytest.mark.asyncio +async def test_handle_load_safe_mode(handlers): + handlers.config.safe_mode = True + req = SkillbookLoadRequest(session_id="s1", path="/tmp/some") + with pytest.raises(ForbiddenInSafeModeError): + await handlers.handle_skillbook_load(req) + + +@pytest.mark.asyncio +async def test_handle_save_load_disabled(handlers, registry): + """allow_save_load=false with safe_mode=false raises SaveLoadDisabledError.""" + handlers.config.safe_mode = False + handlers.config.allow_save_load = False + with patch("ace.integrations.mcp.registry.ACELiteLLM"): + await registry.get_or_create("s1") + with pytest.raises(SaveLoadDisabledError): + await handlers.handle_skillbook_save( + SkillbookSaveRequest(session_id="s1", path="/tmp/f") + ) + with pytest.raises(SaveLoadDisabledError): + await handlers.handle_skillbook_load( + SkillbookLoadRequest(session_id="s1", path="/tmp/f") + ) + + +@pytest.mark.asyncio +async def test_handle_skillbook_save_rejects_path_outside_root(handlers, registry): + handlers.config.skillbook_root = "/tmp/ace-root" + with patch("ace.integrations.mcp.registry.ACELiteLLM"): + await registry.get_or_create("s1") + req = SkillbookSaveRequest(session_id="s1", path="/tmp/not-allowed/file.json") + with pytest.raises(ValidationError): + await handlers.handle_skillbook_save(req) + + +@pytest.mark.asyncio +async def test_handle_skillbook_load_rejects_path_outside_root(handlers): + handlers.config.skillbook_root = "/tmp/ace-root" + req = SkillbookLoadRequest(session_id="s1", path="/tmp/not-allowed/file.json") + with pytest.raises(ValidationError): + await handlers.handle_skillbook_load(req) + + +# ── ace.learn.sample success path ──────────────────────────────── + + +@pytest.mark.asyncio +async def test_handle_learn_sample_success(handlers): + """Success path: learning processes samples and returns counts.""" + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + result_ok = MagicMock(error=None) + runner.learn.return_value = [result_ok, result_ok] + runner.skillbook.skills.side_effect = [["a"], ["a", "b", "c"]] + mock_runner_cls.from_model.return_value = runner + + req = LearnSampleRequest( + session_id="s1", + samples=[ + SampleItem(question="q1", ground_truth="gt1"), + SampleItem(question="q2", ground_truth="gt2"), + ], + ) + resp = await handlers.handle_learn_sample(req) + + assert resp.processed == 2 + assert resp.failed == 0 + assert resp.skill_count_before == 1 + assert resp.skill_count_after == 3 + assert resp.new_skill_count == 2 + runner.learn.assert_called_once() + + +@pytest.mark.asyncio +async def test_handle_learn_sample_partial_failure(handlers): + """When some samples fail, counts reflect partial success.""" + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + result_ok = MagicMock(error=None) + result_fail = MagicMock(error="provider error") + runner.learn.return_value = [result_ok, result_fail] + runner.skillbook.skills.side_effect = [[], ["s1"]] + mock_runner_cls.from_model.return_value = runner + + req = LearnSampleRequest( + session_id="s1", + samples=[ + SampleItem(question="q1"), + SampleItem(question="q2"), + ], + ) + resp = await handlers.handle_learn_sample(req) + + assert resp.processed == 1 + assert resp.failed == 1 + + +@pytest.mark.asyncio +async def test_handle_learn_sample_timeout(handlers): + """learn.sample raises MCPTimeoutError when learn() exceeds timeout.""" + handlers.config.learn_timeout_seconds = 0 # instant timeout + + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + + async def slow_learn(*args, **kwargs): + await asyncio.sleep(10) + + runner.learn.side_effect = ( + lambda *a, **kw: asyncio.get_event_loop().run_until_complete(slow_learn()) + ) + runner.skillbook.skills.return_value = [] + mock_runner_cls.from_model.return_value = runner + + req = LearnSampleRequest( + session_id="s1", + samples=[SampleItem(question="q")], + ) + with pytest.raises(MCPTimeoutError): + await handlers.handle_learn_sample(req) + + +# ── ace.skillbook.save/load success paths ──────────────────────── + + +@pytest.mark.asyncio +async def test_handle_skillbook_save_success(handlers, registry): + """Success path: save returns the resolved path and skill count.""" + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + runner.save.return_value = None + runner.skillbook.skills.return_value = ["s1", "s2"] + mock_runner_cls.from_model.return_value = runner + + await registry.get_or_create("s1") + req = SkillbookSaveRequest(session_id="s1", path="/tmp/test.json") + resp = await handlers.handle_skillbook_save(req) + + assert resp.saved_skill_count == 2 + assert resp.session_id == "s1" + runner.save.assert_called_once() + + +@pytest.mark.asyncio +async def test_handle_skillbook_load_success(handlers): + """Success path: load returns the resolved path and new skill count.""" + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + runner.load.return_value = None + runner.skillbook.skills.return_value = ["s1", "s2", "s3"] + mock_runner_cls.from_model.return_value = runner + + req = SkillbookLoadRequest(session_id="s1", path="/tmp/test.json") + resp = await handlers.handle_skillbook_load(req) + + assert resp.skill_count == 3 + assert resp.session_id == "s1" + runner.load.assert_called_once() + + +# ── ace.skillbook.save/load uses resolved path ─────────────────── + + +@pytest.mark.asyncio +async def test_handle_save_uses_resolved_path(handlers, registry): + """save() receives the resolved path, not the raw user input.""" + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + runner = MagicMock() + runner.save.return_value = None + runner.skillbook.skills.return_value = [] + mock_runner_cls.from_model.return_value = runner + + await registry.get_or_create("s1") + # Path with .. that resolves to /tmp/test.json + req = SkillbookSaveRequest(session_id="s1", path="/tmp/sub/../test.json") + resp = await handlers.handle_skillbook_save(req) + + # The runner should receive the resolved path + called_path = runner.save.call_args[0][0] + assert ".." not in called_path + assert resp.path == called_path + + +# ── error-to-MCP mapping ──────────────────────────────────────── + + +@pytest.mark.asyncio +async def test_handle_call_tool_error_mapping(handlers): + """handle_call_tool maps domain errors to MCP error envelopes.""" + from ace.integrations.mcp.adapters import register_tools + + try: + from mcp.server import Server + from mcp import types + except ImportError: + pytest.skip("mcp not installed") + + server = Server("test") + register_tools(server, handlers) + + # Call a tool that will fail (session not found for skillbook.get) + req = SkillbookGetRequest(session_id="nonexistent") + # Use the handlers directly — the adapter error mapping is tested via map_error_to_mcp + from ace.integrations.mcp.errors import SessionNotFoundError + + err = SessionNotFoundError("nonexistent") + mapped = map_error_to_mcp(err) + assert mapped["code"] == "ACE_MCP_SESSION_NOT_FOUND" + assert "nonexistent" in mapped["message"] + assert mapped["details"]["session_id"] == "nonexistent" + + +def test_map_error_to_mcp_unknown_error(): + """Unknown exceptions map to ACE_MCP_INTERNAL_ERROR.""" + err = RuntimeError("boom") + mapped = map_error_to_mcp(err) + assert mapped["code"] == "ACE_MCP_INTERNAL_ERROR" + assert "boom" in mapped["message"] + assert mapped["details"]["type"] == "RuntimeError" + + +# ── sample indexing uses 0-based ───────────────────────────────── + + +@pytest.mark.asyncio +async def test_handle_learn_sample_prompt_limit_uses_zero_index(handlers): + """Error message for oversized samples uses 0-based index.""" + handlers.config.max_prompt_chars = 5 + req = LearnSampleRequest( + session_id="s1", + samples=[ + SampleItem(question="ok"), # fits + SampleItem(question="toolong"), # exceeds limit + ], + ) + with pytest.raises(ValidationError, match=r"samples\[1\]"): + await handlers.handle_learn_sample(req) + + +# ── ground_truth included in feedback prompt limit ─────────────── + + +@pytest.mark.asyncio +async def test_handle_learn_feedback_prompt_limit_includes_ground_truth(handlers): + """ground_truth contributes to the prompt limit check.""" + handlers.config.max_prompt_chars = 20 + req = LearnFeedbackRequest( + session_id="s1", + question="q", + answer="a", + feedback="f", + context="c", + ground_truth="x" * 20, # pushes total over 20 + ) + with pytest.raises(ValidationError): + await handlers.handle_learn_feedback(req) diff --git a/tests/test_ace_mcp_models.py b/tests/test_ace_mcp_models.py new file mode 100644 index 0000000000000000000000000000000000000000..0ad133e981198484217a6435f5cc9386c7179b4a --- /dev/null +++ b/tests/test_ace_mcp_models.py @@ -0,0 +1,95 @@ +import pytest +from pydantic import ValidationError +from ace.integrations.mcp.models import ( + AskRequest, + AskResponse, + LearnSampleRequest, + LearnSampleResponse, + LearnFeedbackRequest, + LearnFeedbackResponse, + SkillbookGetRequest, + SkillbookGetResponse, + SkillbookSaveRequest, + SkillbookSaveResponse, + SkillbookLoadRequest, + SkillbookLoadResponse, + SessionConfig, + SampleItem, + SkillItem, + ErrorEnvelope, +) + + +def test_session_config_validation(): + # Valid with all fields + config = SessionConfig(model="gpt-4o", temperature=0.7, max_tokens=100) + assert config.model == "gpt-4o" + + # Valid without model (optional per contract) + config2 = SessionConfig(temperature=0.5) + assert config2.model is None + + # Invalid: empty string for model + with pytest.raises(ValidationError): + SessionConfig(model="") + + # Invalid temp + with pytest.raises(ValidationError): + SessionConfig(model="gpt-4o", temperature=2.5) + + +def test_ask_request_validation(): + # Valid + req = AskRequest(session_id="s1", question="hello") + assert req.context == "" + assert req.metadata is None + + # Max length question + with pytest.raises(ValidationError): + AskRequest(session_id="s1", question="a" * 100001) + + # Removed compatibility flags must remain invalid + with pytest.raises(ValidationError): + AskRequest(session_id="s1", question="hello", learn=True) + + +def test_learn_sample_request_limits(): + # Min items + with pytest.raises(ValidationError): + LearnSampleRequest(session_id="s1", samples=[]) + + # Max items + samples = [{"question": f"q{i}"} for i in range(26)] + with pytest.raises(ValidationError): + LearnSampleRequest(session_id="s1", samples=samples) + + +def test_skillbook_get_limits(): + # Valid + req = SkillbookGetRequest(session_id="s", limit=50) + assert req.limit == 50 + + # Max limit + with pytest.raises(ValidationError): + SkillbookGetRequest(session_id="s", limit=201) + + +def test_error_envelope(): + env = ErrorEnvelope(code="ERR1", message="Error message") + assert env.code == "ERR1" + + # Extra fields forbidden + with pytest.raises(ValidationError): + ErrorEnvelope(code="ERR1", message="m", extra="not allowed") + + +def test_skillbook_load_request_disallows_unsupported_flags(): + req = SkillbookLoadRequest(session_id="s1", path="/tmp/skillbook.json") + assert req.path == "/tmp/skillbook.json" + + with pytest.raises(ValidationError): + SkillbookLoadRequest( + session_id="s1", + path="/tmp/skillbook.json", + replace=False, + ) diff --git a/tests/test_ace_mcp_optional.py b/tests/test_ace_mcp_optional.py new file mode 100644 index 0000000000000000000000000000000000000000..308df865507e4b2f55b859646b9e706b8d29b7cf --- /dev/null +++ b/tests/test_ace_mcp_optional.py @@ -0,0 +1,33 @@ +from ace.integrations.mcp import adapters, server + + +def test_create_server_requires_mcp_extra(monkeypatch): + def fake_import_module(name: str): + err = ModuleNotFoundError("No module named 'mcp'") + err.name = "mcp" + raise err + + monkeypatch.setattr(server, "import_module", fake_import_module) + + try: + server.create_server() + except RuntimeError as exc: + assert "ace-framework[mcp]" in str(exc) + else: # pragma: no cover - defensive check + raise AssertionError("create_server() should require the mcp extra") + + +def test_register_tools_requires_mcp_extra(monkeypatch): + def fake_import_module(name: str): + err = ModuleNotFoundError("No module named 'mcp'") + err.name = "mcp" + raise err + + monkeypatch.setattr(adapters, "import_module", fake_import_module) + + try: + adapters.register_tools(object(), object()) # type: ignore[arg-type] + except RuntimeError as exc: + assert "ace-framework[mcp]" in str(exc) + else: # pragma: no cover - defensive check + raise AssertionError("register_tools() should require the mcp extra") diff --git a/tests/test_ace_mcp_registry.py b/tests/test_ace_mcp_registry.py new file mode 100644 index 0000000000000000000000000000000000000000..a5d2c68760a4be3de13a6571651300983fa6aedf --- /dev/null +++ b/tests/test_ace_mcp_registry.py @@ -0,0 +1,72 @@ +import pytest +import asyncio +from unittest.mock import MagicMock, patch +from ace.integrations.mcp.config import MCPServerConfig +from ace.integrations.mcp.registry import SessionRegistry +from ace.integrations.mcp.errors import SessionNotFoundError + + +@pytest.fixture +def config(): + return MCPServerConfig(session_ttl_seconds=1) + + +@pytest.fixture +def registry(config): + return SessionRegistry(config) + + +@pytest.mark.asyncio +async def test_get_or_create(registry): + with patch("ace.integrations.mcp.registry.ACELiteLLM") as mock_runner_cls: + mock_runner_cls.from_model.return_value = MagicMock() + + # Create + s1 = await registry.get_or_create("s1") + assert s1.session_id == "s1" + assert s1.runner is not None + mock_runner_cls.from_model.assert_called_once_with("gpt-4o-mini") + + # Get existing + s1_again = await registry.get_or_create("s1") + assert s1 is s1_again + assert mock_runner_cls.from_model.call_count == 1 + + +@pytest.mark.asyncio +async def test_get_existing(registry): + with patch("ace.integrations.mcp.registry.ACELiteLLM"): + s1 = await registry.get_or_create("s1") + s1_get = await registry.get("s1") + assert s1 is s1_get + + +@pytest.mark.asyncio +async def test_get_not_found(registry): + with pytest.raises(SessionNotFoundError): + await registry.get("nonexistent") + + +@pytest.mark.asyncio +async def test_sweep_expired(registry): + with patch("ace.integrations.mcp.registry.ACELiteLLM"): + s1 = await registry.get_or_create("s1") + + # Should not expire immediately + await registry.get("s1") + + # Wait for TTL to pass (config TTL is 1 sec) + await asyncio.sleep(1.1) + + with pytest.raises(SessionNotFoundError): + await registry.get("s1") + + +@pytest.mark.asyncio +async def test_delete(registry): + with patch("ace.integrations.mcp.registry.ACELiteLLM"): + await registry.get_or_create("s1") + await registry.delete("s1") + + with pytest.raises(SessionNotFoundError): + await registry.get("s1") diff --git a/tests/test_ace_mcp_server.py b/tests/test_ace_mcp_server.py new file mode 100644 index 0000000000000000000000000000000000000000..269a0034119bf66f6ff27a6ee10f55dbe4165309 --- /dev/null +++ b/tests/test_ace_mcp_server.py @@ -0,0 +1,37 @@ +import pytest +from unittest.mock import MagicMock + +pytest.importorskip("mcp.server") +pytest.importorskip("mcp.types") + +from ace.integrations.mcp.server import create_server +from mcp.server import Server +from mcp.types import ListToolsRequest + +EXPECTED_TOOL_NAMES = { + "ace.ask", + "ace.learn.sample", + "ace.learn.feedback", + "ace.skillbook.get", + "ace.skillbook.save", + "ace.skillbook.load", +} + + +def test_create_server(): + server = create_server() + assert isinstance(server, Server) + assert server.name == "ace-mcp-server" + + +@pytest.mark.asyncio +async def test_tool_registration(): + """All 6 MVP tools must be registered (FR-002).""" + server = create_server() + + handler = server.request_handlers.get(ListToolsRequest) + assert handler is not None, "tools/list handler not registered" + + result = await handler(MagicMock()) + registered_names = {t.name for t in result.root.tools} + assert registered_names == EXPECTED_TOOL_NAMES diff --git a/tests/test_ace_runners.py b/tests/test_ace_runners.py new file mode 100644 index 0000000000000000000000000000000000000000..785a12f9e946a327674e44933358f7dfe47cca3e --- /dev/null +++ b/tests/test_ace_runners.py @@ -0,0 +1,393 @@ +"""Tests for ace runners: ACE, TraceAnalyser, ACERunner, ACELiteLLM.""" + +from __future__ import annotations + +from typing import Any, Optional +from unittest.mock import MagicMock, patch + +import pytest + +from ace.core.context import ACEStepContext, SkillbookView +from ace.core.environments import Sample, SimpleEnvironment +from ace.core.insight_source import TRACE_IDENTITY_METADATA_KEY +from ace.core.outputs import ( + AgentOutput, + ReflectorOutput, + SkillManagerOutput, +) +from ace.core.skillbook import Skillbook, UpdateBatch, UpdateOperation +from ace.runners.base import ACERunner + +# ------------------------------------------------------------------ # +# Mock roles — satisfy protocols without any LLM dependency +# ------------------------------------------------------------------ # + + +class MockAgent: + """Minimal mock satisfying AgentLike.""" + + def generate( + self, + *, + question: str, + context: Optional[str], + skillbook: Any, + reflection: Optional[str] = None, + **kwargs: Any, + ) -> AgentOutput: + return AgentOutput(reasoning="mock reasoning", final_answer="mock answer") + + +class MockReflector: + """Minimal mock satisfying ReflectorLike.""" + + def reflect( + self, + *, + question: str, + agent_output: AgentOutput, + skillbook: Any, + ground_truth: Optional[str] = None, + feedback: Optional[str] = None, + **kwargs: Any, + ) -> ReflectorOutput: + return ReflectorOutput( + reasoning="mock reflection", + correct_approach="mock approach", + key_insight="mock insight", + ) + + +class MockSkillManager: + """Minimal mock satisfying SkillManagerLike. + + The real agentic SkillManager mutates the skillbook directly via + tools; this mock does the same so ``UpdateStep`` behaves realistically + without a live LLM. + """ + + def update_skills( + self, + *, + reflections: tuple[ReflectorOutput, ...], + skillbook: Any, + question_context: str, + progress: str, + **kwargs: Any, + ) -> SkillManagerOutput: + skill = skillbook.add_skill(section="learned", issue="mock skill") + return SkillManagerOutput( + update=UpdateBatch( + reasoning="mock update", + operations=[ + UpdateOperation( + type="ADD", + section="learned", + issue="mock skill", + skill_id=skill.id, + ) + ], + ), + ) + + +# ------------------------------------------------------------------ # +# ACERunner base class +# ------------------------------------------------------------------ # + + +class TestACERunnerBase: + def test_save_and_load(self, tmp_path): + """save() and load() should round-trip the skillbook.""" + sb = Skillbook() + sb.add_skill("sec", "content", skill_id="s-001") + pipeline = MagicMock() + runner = ACERunner(pipeline=pipeline, skillbook=sb) + + path = str(tmp_path / "sb.json") + runner.save(path) + + # Modify skillbook + sb.add_skill("sec", "new", skill_id="s-002") + assert len(runner.skillbook.skills()) == 2 + + # Load should replace the skillbook + runner.load(path) + assert len(runner.skillbook.skills()) == 1 + assert runner.skillbook.get_skill("s-001") is not None + + def test_multi_epoch_requires_sequence(self): + """Multi-epoch with non-Sequence should raise ValueError.""" + pipeline = MagicMock() + sb = Skillbook() + runner = ACERunner(pipeline=pipeline, skillbook=sb) + + def gen(): + yield "item" + + with pytest.raises(ValueError, match="Sequence"): + runner._run(gen(), epochs=2) + + +# ------------------------------------------------------------------ # +# load_skillbook alias correctness +# ------------------------------------------------------------------ # + + +class TestLoadSkillbookAlias: + def test_langchain_alias(self): + from ace.runners.langchain import LangChain + + assert LangChain.load_skillbook is ACERunner.load + assert LangChain.save_skillbook is ACERunner.save + + def test_browser_use_alias(self): + from ace.runners.browser_use import BrowserUse + + assert BrowserUse.load_skillbook is ACERunner.load + assert BrowserUse.save_skillbook is ACERunner.save + + def test_claude_code_alias(self): + from ace.runners.claude_code import ClaudeCode + + assert ClaudeCode.load_skillbook is ACERunner.load + assert ClaudeCode.save_skillbook is ACERunner.save + + def test_litellm_alias(self): + from ace.runners.litellm import ACELiteLLM + + assert ACELiteLLM.load_skillbook is ACELiteLLM.load + assert ACELiteLLM.save_skillbook is ACELiteLLM.save + + def test_load_not_save(self): + """Critical: load_skillbook must NOT point to save.""" + from ace.runners.langchain import LangChain + + assert LangChain.load_skillbook is not ACERunner.save + assert LangChain.load_skillbook is not LangChain.save_skillbook + + +# ------------------------------------------------------------------ # +# ACE runner (full pipeline) with mocks +# ------------------------------------------------------------------ # + + +class TestACERunner: + def test_from_roles_run(self): + """ACE.from_roles().run() should complete without error with mock roles.""" + from ace.runners.ace import ACE + + env = SimpleEnvironment() + + runner = ACE.from_roles( + agent=MockAgent(), + reflector=MockReflector(), + skill_manager=MockSkillManager(), + environment=env, + ) + + samples = [ + Sample(question="What is 2+2?", ground_truth="4"), + Sample(question="Capital of France?", ground_truth="Paris"), + ] + + results = runner.run(samples, epochs=1) + assert len(results) == 2 + # After learning, skillbook should have skills + assert len(runner.skillbook.skills()) > 0 + + def test_multi_epoch(self): + from ace.runners.ace import ACE + + env = SimpleEnvironment() + + runner = ACE.from_roles( + agent=MockAgent(), + reflector=MockReflector(), + skill_manager=MockSkillManager(), + environment=env, + ) + + samples = [Sample(question="Q1", ground_truth="A1")] + results = runner.run(samples, epochs=2) + assert len(results) == 2 # 1 sample × 2 epochs + + def test_build_context_adds_trace_identity_metadata(self): + from ace.runners.ace import ACE + + runner = ACE.from_roles( + agent=MockAgent(), + reflector=MockReflector(), + skill_manager=MockSkillManager(), + ) + sample = Sample( + question="Why did pagination stop early?", + id="conv-123", + metadata={ + "source_system": "kayba-hosted", + "trace_id": "conv-123", + "display_name": "checkout-failure.md", + }, + ) + + ctx = runner._build_context( + sample, + epoch=1, + total_epochs=1, + index=1, + total=1, + global_sample_index=1, + ) + + identity = ctx.metadata[TRACE_IDENTITY_METADATA_KEY] + assert identity["trace_uid"] == "kayba-hosted:conv-123" + assert identity["display_name"] == "checkout-failure.md" + + +# ------------------------------------------------------------------ # +# TraceAnalyser runner +# ------------------------------------------------------------------ # + + +class TestTraceAnalyser: + def test_from_roles_run(self): + """TraceAnalyser.from_roles().run() with mock roles should work.""" + from ace.runners.trace_analyser import TraceAnalyser + + runner = TraceAnalyser.from_roles( + reflector=MockReflector(), + skill_manager=MockSkillManager(), + ) + + traces = [ + { + "question": "What is 2+2?", + "answer": "4", + "reasoning": "simple", + "ground_truth": "4", + "feedback": "Correct!", + }, + ] + + results = runner.run(traces) + assert len(results) == 1 + assert len(runner.skillbook.skills()) > 0 + + def test_build_context_adds_inferred_trace_identity(self): + from ace.runners.trace_analyser import TraceAnalyser + + runner = TraceAnalyser.from_roles( + reflector=MockReflector(), + skill_manager=MockSkillManager(), + ) + + ctx = runner._build_context( + {"sample_id": "trace-001", "question": "Q"}, + epoch=1, + total_epochs=1, + index=1, + total=1, + global_sample_index=1, + ) + + identity = ctx.metadata[TRACE_IDENTITY_METADATA_KEY] + assert identity["trace_uid"] == "trace:trace-001" + assert identity["trace_id"] == "trace-001" + + +# ------------------------------------------------------------------ # +# ACELiteLLM +# ------------------------------------------------------------------ # + + +class TestACELiteLLM: + def _make_ace(self, **kwargs): + from ace.runners.litellm import ACELiteLLM + + return ACELiteLLM( + "test-model", + agent=MockAgent(), + reflector=MockReflector(), + skill_manager=MockSkillManager(), + **kwargs, + ) + + def test_ask(self): + ace = self._make_ace() + answer = ace.ask("What is 2+2?") + assert answer == "mock answer" + + def test_learn_from_feedback_no_prior_ask(self): + """learn_from_feedback with no prior ask() should return False.""" + ace = self._make_ace() + assert ace.learn_from_feedback("good answer") is False + + def test_learn_from_feedback_after_ask(self): + """learn_from_feedback after ask() should return True.""" + ace = self._make_ace() + ace.ask("What is 2+2?") + result = ace.learn_from_feedback("Correct!", ground_truth="4") + assert result is True + assert len(ace.skillbook.skills()) > 0 + + def test_learn_from_feedback_disabled(self): + """learn_from_feedback with learning disabled should return False.""" + ace = self._make_ace(is_learning=False) + ace.ask("What is 2+2?") + assert ace.learn_from_feedback("Correct!") is False + + def test_learn(self): + ace = self._make_ace(environment=SimpleEnvironment()) + samples = [Sample(question="Q", ground_truth="A")] + results = ace.learn(samples) + assert len(results) == 1 + + def test_learn_disabled(self): + ace = self._make_ace(is_learning=False) + with pytest.raises(RuntimeError, match="disabled"): + ace.learn([Sample(question="Q", ground_truth="A")]) + + def test_save_and_load(self, tmp_path): + ace = self._make_ace() + ace.ask("Q") + ace.learn_from_feedback("good", ground_truth="A") + + path = str(tmp_path / "sb.json") + ace.save(path) + skills_before = len(ace.skillbook.skills()) + + # Load into same instance + ace.load(path) + assert len(ace.skillbook.skills()) == skills_before + + def test_enable_disable_learning(self): + ace = self._make_ace() + assert ace.is_learning is True + + ace.disable_learning() + assert ace.is_learning is False + + ace.enable_learning() + assert ace.is_learning is True + + def test_get_strategies_empty(self): + ace = self._make_ace() + assert ace.get_strategies() == "" + + def test_skillbook_path_loading(self, tmp_path): + """Constructor with skillbook_path should load from file.""" + sb = Skillbook() + sb.add_skill("test", "content", skill_id="t-001") + path = str(tmp_path / "sb.json") + sb.save_to_file(path) + + from ace.runners.litellm import ACELiteLLM + + ace = ACELiteLLM( + "test-model", + agent=MockAgent(), + reflector=MockReflector(), + skill_manager=MockSkillManager(), + skillbook_path=path, + ) + assert ace.skillbook.get_skill("t-001") is not None diff --git a/tests/test_ace_steps.py b/tests/test_ace_steps.py new file mode 100644 index 0000000000000000000000000000000000000000..a8b8c8aff70e2a1eac0fb85db183b559c338829e --- /dev/null +++ b/tests/test_ace_steps.py @@ -0,0 +1,334 @@ +"""Tests for ace steps: ReflectStep, UpdateStep, provenance.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass +from typing import Any, Optional +from unittest.mock import MagicMock + +import pytest + +from ace.core.context import ACEStepContext, SkillbookView +from ace.core.outputs import ( + AgentOutput, + ReflectorOutput, + SkillManagerOutput, +) +from ace.core.skillbook import Skillbook, UpdateBatch, UpdateOperation +from ace.steps import learning_tail +from ace.steps.reflect import ReflectStep +from ace.steps.update import UpdateStep + +# ------------------------------------------------------------------ # +# Helpers — mock roles satisfying protocols +# ------------------------------------------------------------------ # + + +class MockReflector: + """Minimal mock satisfying ReflectorLike.""" + + def __init__(self, output: ReflectorOutput | None = None): + self.output = output or ReflectorOutput( + reasoning="test reasoning", + correct_approach="test approach", + key_insight="test insight", + ) + self.calls: list[dict] = [] + + def reflect( + self, + *, + question: str, + agent_output: AgentOutput, + skillbook: Any, + ground_truth: Optional[str] = None, + feedback: Optional[str] = None, + **kwargs: Any, + ) -> ReflectorOutput: + self.calls.append( + { + "question": question, + "agent_output": agent_output, + "ground_truth": ground_truth, + "feedback": feedback, + **kwargs, + } + ) + return self.output + + +class MockSkillManager: + """Minimal mock satisfying SkillManagerLike.""" + + def __init__(self, output: SkillManagerOutput | None = None): + self.output = output or SkillManagerOutput( + update=UpdateBatch(reasoning="test", operations=[]), + ) + self.calls: list[dict] = [] + + def update_skills( + self, + *, + reflections: tuple[ReflectorOutput, ...], + skillbook: Any, + question_context: str, + progress: str, + **kwargs: Any, + ) -> SkillManagerOutput: + self.calls.append( + { + "reflections": reflections, + "question_context": question_context, + "progress": progress, + } + ) + return self.output + + +# ------------------------------------------------------------------ # +# ReflectStep +# ------------------------------------------------------------------ # + + +class TestReflectStep: + def test_dict_trace(self): + """Structured dict trace should extract known fields.""" + reflector = MockReflector() + step = ReflectStep(reflector) + + trace = { + "question": "What is 2+2?", + "answer": "4", + "reasoning": "simple math", + "ground_truth": "4", + "feedback": "Correct!", + } + sb = Skillbook() + ctx = ACEStepContext( + trace=trace, + skillbook=SkillbookView(sb), + ) + + result = step(ctx) + assert len(result.reflections) == 1 + assert len(reflector.calls) == 1 + call = reflector.calls[0] + assert call["question"] == "What is 2+2?" + assert call["agent_output"].final_answer == "4" + assert call["ground_truth"] == "4" + assert call["feedback"] == "Correct!" + + def test_raw_trace(self): + """Non-dict trace should be passed as-is via kwargs.""" + reflector = MockReflector() + step = ReflectStep(reflector) + + raw_trace = ["step1", "step2", "step3"] + sb = Skillbook() + ctx = ACEStepContext( + trace=raw_trace, + skillbook=SkillbookView(sb), + ) + + result = step(ctx) + assert len(result.reflections) == 1 + assert len(reflector.calls) == 1 + call = reflector.calls[0] + assert call["question"] == "" + assert call["agent_output"].final_answer == "" + assert call.get("trace") is raw_trace + + def test_batch_dict_trace_is_passed_raw(self): + """Batch dict traces should bypass structured trace extraction.""" + reflector = MockReflector() + step = ReflectStep(reflector) + + batch_trace = { + "tasks": [ + {"task_id": "task-0", "trace": {"question": "What is 2+2?"}}, + {"task_id": "task-1", "trace": {"question": "What is 3+3?"}}, + ] + } + sb = Skillbook() + ctx = ACEStepContext( + trace=batch_trace, + skillbook=SkillbookView(sb), + ) + + result = step(ctx) + assert len(result.reflections) == 1 + assert len(reflector.calls) == 1 + call = reflector.calls[0] + assert call["question"] == "" + assert call["agent_output"].final_answer == "" + assert call.get("trace") is batch_trace + + def test_provides_and_requires(self): + step = ReflectStep(MockReflector()) + assert "trace" in step.requires + assert "skillbook" in step.requires + assert "reflections" in step.provides + assert step.async_boundary is True + assert step.max_workers == 3 + + +# ------------------------------------------------------------------ # +# UpdateStep +# ------------------------------------------------------------------ # + + +class TestUpdateStep: + def test_generates_update_batch(self): + sm = MockSkillManager() + sb = Skillbook() + step = UpdateStep(sm, sb) + + reflection = ReflectorOutput( + reasoning="r", + correct_approach="c", + key_insight="k", + ) + trace = {"question": "What is 2+2?", "context": "math quiz"} + ctx = ACEStepContext( + reflections=(reflection,), + skillbook=SkillbookView(sb), + trace=trace, + epoch=2, + total_epochs=3, + step_index=5, + total_steps=10, + ) + + result = step(ctx) + assert result.skill_manager_output is not None + assert len(sm.calls) == 1 + call = sm.calls[0] + assert "Epoch 2/3" in call["progress"] + assert "sample 5/10" in call["progress"] + assert "What is 2+2?" in call["question_context"] + + def test_non_dict_trace(self): + """Non-dict trace should produce empty question_context.""" + sm = MockSkillManager() + sb = Skillbook() + step = UpdateStep(sm, sb) + + reflection = ReflectorOutput( + reasoning="r", + correct_approach="c", + key_insight="k", + ) + ctx = ACEStepContext( + reflections=(reflection,), + skillbook=SkillbookView(sb), + trace="raw string trace", + ) + + step(ctx) + assert sm.calls[0]["question_context"] == "" + + def test_forwards_full_reflections_tuple(self): + """UpdateStep forwards the entire reflections tuple to the skill manager.""" + sm = MockSkillManager() + sb = Skillbook() + step = UpdateStep(sm, sb) + + r1 = ReflectorOutput(reasoning="r1", correct_approach="c", key_insight="k1") + r2 = ReflectorOutput(reasoning="r2", correct_approach="c", key_insight="k2") + ctx = ACEStepContext( + reflections=(r1, r2), + skillbook=SkillbookView(sb), + ) + + step(ctx) + assert len(sm.calls) == 1 + assert sm.calls[0]["reflections"] == (r1, r2) + + def test_provides_and_requires(self): + sb = Skillbook() + step = UpdateStep(MockSkillManager(), sb) + assert "reflections" in step.requires + assert "skillbook" in step.requires + assert "skill_manager_output" in step.provides + assert step.max_workers == 1 + + +# ------------------------------------------------------------------ # +# learning_tail helper +# ------------------------------------------------------------------ # + + +class TestLearningTail: + def test_basic_tail(self): + reflector = MockReflector() + sm = MockSkillManager() + sb = Skillbook() + + steps = learning_tail(reflector, sm, sb) + assert len(steps) == 2 + assert isinstance(steps[0], ReflectStep) + assert isinstance(steps[1], UpdateStep) + + def test_step_like_reflector_is_inserted_directly(self): + class ReflectorStep(MockReflector): + requires = frozenset({"trace", "skillbook"}) + provides = frozenset({"reflections"}) + + def __call__(self, ctx: ACEStepContext) -> ACEStepContext: + return ctx.replace(reflections=(self.output,)) + + reflector = ReflectorStep() + sm = MockSkillManager() + sb = Skillbook() + + steps = learning_tail(reflector, sm, sb) + + assert steps[0] is reflector + assert isinstance(steps[1], UpdateStep) + + def test_with_checkpoint(self, tmp_path): + reflector = MockReflector() + sm = MockSkillManager() + sb = Skillbook() + + steps = learning_tail( + reflector, + sm, + sb, + checkpoint_dir=str(tmp_path), + checkpoint_interval=5, + ) + assert len(steps) == 3 # 2 + CheckpointStep + + def test_with_dedup(self): + reflector = MockReflector() + sm = MockSkillManager() + sb = Skillbook() + dedup = MagicMock() + + steps = learning_tail( + reflector, + sm, + sb, + dedup_manager=dedup, + dedup_interval=5, + ) + assert len(steps) == 3 # 2 + DeduplicateStep + + def test_with_both(self, tmp_path): + reflector = MockReflector() + sm = MockSkillManager() + sb = Skillbook() + dedup = MagicMock() + + steps = learning_tail( + reflector, + sm, + sb, + dedup_manager=dedup, + dedup_interval=5, + checkpoint_dir=str(tmp_path), + checkpoint_interval=5, + ) + assert len(steps) == 4 # 2 + DeduplicateStep + CheckpointStep diff --git a/tests/test_claude_sdk_live.py b/tests/test_claude_sdk_live.py new file mode 100644 index 0000000000000000000000000000000000000000..05f633856a4058a1413bc9907854f083770fa7ef --- /dev/null +++ b/tests/test_claude_sdk_live.py @@ -0,0 +1,475 @@ +"""Live integration tests for the Claude SDK step. + +These tests make REAL API calls to the Anthropic API. They require: +- ``ANTHROPIC_API_KEY`` environment variable to be set +- Network access to ``api.anthropic.com`` + +Run with:: + + ANTHROPIC_API_KEY=sk-... uv run pytest tests/test_claude_sdk_live.py -v -m integration +""" + +from __future__ import annotations + +import os + +import pytest + +from ace.core.context import ACEStepContext, SkillbookView +from ace.core.skillbook import Skillbook +from ace.integrations.claude_sdk import ( + ClaudeSDKExecuteStep, + ClaudeSDKResult, + ClaudeSDKToTrace, +) + +pytestmark = [ + pytest.mark.integration, + pytest.mark.requires_api, + pytest.mark.skipif( + not os.environ.get("ANTHROPIC_API_KEY"), + reason="ANTHROPIC_API_KEY not set", + ), +] + +pytest.importorskip("anthropic") + +MODEL = "claude-sonnet-4-20250514" + + +# ------------------------------------------------------------------ # +# 1. Basic text generation +# ------------------------------------------------------------------ # + + +class TestBasicGeneration: + def test_simple_question(self): + """Verify a basic question returns a successful result with tokens.""" + step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=100) + ctx = ACEStepContext( + sample="What is 2+2? Answer with just the number.", skillbook=None + ) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is True + assert r.error is None + assert "4" in r.output + assert r.model == MODEL + assert r.stop_reason in ("end_turn", "max_tokens") + assert r.input_tokens > 0 + assert r.output_tokens > 0 + assert r.total_tokens == r.input_tokens + r.output_tokens + assert r.latency_seconds > 0 + assert r.raw_response is not None + + def test_longer_response(self): + """Verify the step handles multi-sentence responses.""" + step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=300) + ctx = ACEStepContext( + sample="Explain in 2-3 sentences why the sky is blue.", + skillbook=None, + ) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is True + assert len(r.output) > 50 + assert r.output_tokens > 10 + + +# ------------------------------------------------------------------ # +# 2. System prompt +# ------------------------------------------------------------------ # + + +class TestSystemPrompt: + def test_system_prompt_affects_output(self): + """A system prompt instructing a specific format should be followed.""" + step = ClaudeSDKExecuteStep( + model=MODEL, + max_tokens=50, + system_prompt="You are a calculator. Only output numbers, nothing else.", + ) + ctx = ACEStepContext(sample="What is 15 * 3?", skillbook=None) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is True + assert "45" in r.output + + +# ------------------------------------------------------------------ # +# 3. Skillbook injection +# ------------------------------------------------------------------ # + + +class TestSkillbookInjection: + def test_skillbook_injected_into_system(self): + """When a skillbook has skills, they're injected and the model can reference them.""" + sb = Skillbook() + sb.add_skill("math", "Always show step-by-step work for math problems") + sb.add_skill("format", "End every answer with 'QED'") + + step = ClaudeSDKExecuteStep( + model=MODEL, + max_tokens=200, + inject_skillbook=True, + ) + ctx = ACEStepContext( + sample="What is 7 * 8? Follow the strategies you've been given.", + skillbook=SkillbookView(sb), + ) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is True + assert "56" in r.output + + def test_skillbook_injection_disabled(self): + """With inject_skillbook=False, skills are NOT sent to the model.""" + sb = Skillbook() + sb.add_skill("format", "End every answer with the word BANANA") + + step = ClaudeSDKExecuteStep( + model=MODEL, + max_tokens=50, + inject_skillbook=False, + ) + ctx = ACEStepContext( + sample="What is 1+1? Just answer the number.", + skillbook=SkillbookView(sb), + ) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is True + assert "BANANA" not in r.output + + +# ------------------------------------------------------------------ # +# 4. Tool use +# ------------------------------------------------------------------ # + + +class TestToolUse: + def test_tool_call_returned(self): + """When given a tool, the model should call it and we capture the call.""" + tools = [ + { + "name": "get_weather", + "description": "Get the current weather for a city.", + "input_schema": { + "type": "object", + "properties": { + "city": {"type": "string", "description": "City name"}, + }, + "required": ["city"], + }, + } + ] + step = ClaudeSDKExecuteStep( + model=MODEL, + max_tokens=200, + tools=tools, + ) + ctx = ACEStepContext( + sample="What's the weather in Paris right now?", + skillbook=None, + ) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is True + assert r.stop_reason in ("tool_use", "end_turn") + if r.stop_reason == "tool_use": + assert len(r.tool_calls) >= 1 + tc = r.tool_calls[0] + assert tc.name == "get_weather" + assert "city" in tc.input + assert tc.id.startswith("toolu_") + + +# ------------------------------------------------------------------ # +# 5. Temperature +# ------------------------------------------------------------------ # + + +class TestTemperature: + def test_zero_temperature_deterministic(self): + """Temperature 0 should produce near-identical outputs.""" + step = ClaudeSDKExecuteStep( + model=MODEL, + max_tokens=20, + temperature=0.0, + ) + ctx = ACEStepContext( + sample="Complete this: 1, 2, 3, 4, ", + skillbook=None, + ) + + r1: ClaudeSDKResult = step(ctx).trace # type: ignore[assignment] + r2: ClaudeSDKResult = step(ctx).trace # type: ignore[assignment] + + assert r1.success and r2.success + # With temp=0 outputs should be identical or very similar + assert r1.output[:10] == r2.output[:10] + + +# ------------------------------------------------------------------ # +# 6. Error handling +# ------------------------------------------------------------------ # + + +class TestErrorHandling: + def test_invalid_model_returns_error(self): + """An invalid model name should return a failed result, not crash.""" + step = ClaudeSDKExecuteStep( + model="claude-nonexistent-99", + max_tokens=10, + ) + ctx = ACEStepContext(sample="hello", skillbook=None) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is False + assert r.error is not None + assert r.latency_seconds >= 0 + + def test_max_tokens_respected(self): + """A very low max_tokens should truncate the response.""" + step = ClaudeSDKExecuteStep( + model=MODEL, + max_tokens=5, + ) + ctx = ACEStepContext( + sample="Write a 500-word essay about the history of computing.", + skillbook=None, + ) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is True + assert r.stop_reason == "max_tokens" + assert r.output_tokens <= 10 # small buffer around max_tokens + + +# ------------------------------------------------------------------ # +# 7. ToTrace conversion with real data +# ------------------------------------------------------------------ # + + +class TestToTraceWithRealData: + def test_full_pipeline_execute_then_convert(self): + """Run execute step then ToTrace step — end-to-end pipeline flow.""" + execute = ClaudeSDKExecuteStep(model=MODEL, max_tokens=100) + to_trace = ClaudeSDKToTrace() + + ctx = ACEStepContext(sample="What is the capital of France?", skillbook=None) + + # Execute + ctx = execute(ctx) + r: ClaudeSDKResult = ctx.trace # type: ignore[assignment] + assert r.success is True + assert "Paris" in r.output + + # Convert + ctx = to_trace(ctx) + trace = ctx.trace + assert isinstance(trace, dict) + assert trace["question"] == "What is the capital of France?" + assert "Paris" in trace["answer"] + assert "succeeded" in trace["reasoning"] + assert str(r.input_tokens) in trace["reasoning"] + assert "succeeded" in trace["feedback"] + assert trace["ground_truth"] is None + + +# ------------------------------------------------------------------ # +# 8. ACESample input (structured sample) +# ------------------------------------------------------------------ # + + +class TestACESampleInput: + def test_structured_sample(self): + """Step should extract question+context from a structured sample.""" + from dataclasses import dataclass + + @dataclass + class Sample: + question: str = "What color is a ripe banana?" + context: str = "We are discussing fruits." + ground_truth: str = "yellow" + metadata: dict = None # type: ignore[assignment] + + def __post_init__(self): + self.metadata = self.metadata or {} + + step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=30) + ctx = ACEStepContext(sample=Sample(), skillbook=None) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is True + assert r.task # task was extracted + assert "banana" in r.task.lower() or "color" in r.task.lower() + + +# ------------------------------------------------------------------ # +# 9. Observability data quality +# ------------------------------------------------------------------ # + + +class TestObservabilityData: + def test_token_counts_realistic(self): + """Token counts should be reasonable for a simple prompt.""" + step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=50) + ctx = ACEStepContext(sample="Say 'hello world'", skillbook=None) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is True + # Input: system overhead + short prompt, should be < 100 + assert 1 < r.input_tokens < 100 + # Output: short response + assert 1 < r.output_tokens <= 50 + # Total is sum + assert r.total_tokens == r.input_tokens + r.output_tokens + # Latency > 0 and reasonable (< 30s for a short request) + assert 0 < r.latency_seconds < 30 + + def test_model_field_matches_request(self): + """The model field in the result should match what we requested.""" + step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=10) + ctx = ACEStepContext(sample="hi", skillbook=None) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.model == MODEL + + +# ------------------------------------------------------------------ # +# 10. Pydantic validation on real results +# ------------------------------------------------------------------ # + + +class TestPydanticValidation: + def test_result_is_pydantic_model(self): + """ClaudeSDKResult should be a Pydantic BaseModel.""" + from pydantic import BaseModel + + step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=10) + ctx = ACEStepContext(sample="hi", skillbook=None) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert isinstance(r, BaseModel) + + def test_model_dump(self): + """Real result should serialise cleanly, with raw_response excluded.""" + step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=20) + ctx = ACEStepContext(sample="Say hello", skillbook=None) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + d = r.model_dump() + assert isinstance(d, dict) + assert d["success"] is True + assert d["input_tokens"] > 0 + assert d["output_tokens"] > 0 + assert d["total_tokens"] == d["input_tokens"] + d["output_tokens"] + assert "raw_response" not in d + + def test_model_dump_json(self): + """Result should serialise to JSON string.""" + import json + + step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=10) + ctx = ACEStepContext(sample="hi", skillbook=None) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + j = r.model_dump_json() + parsed = json.loads(j) + assert parsed["task"] == "hi" + assert parsed["success"] is True + + def test_total_tokens_auto_computed(self): + """total_tokens should equal input + output on a real response.""" + step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=20) + ctx = ACEStepContext(sample="Count to 3", skillbook=None) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.total_tokens == r.input_tokens + r.output_tokens + assert r.total_tokens > 0 + + def test_tool_calls_are_pydantic_models(self): + """Tool calls in the result should be validated ToolCall models.""" + from ace.integrations.claude_sdk import ToolCall + + tools = [ + { + "name": "lookup", + "description": "Look up a value.", + "input_schema": { + "type": "object", + "properties": {"key": {"type": "string"}}, + "required": ["key"], + }, + } + ] + step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=100, tools=tools) + ctx = ACEStepContext(sample="Look up the value of 'pi'", skillbook=None) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is True + if r.tool_calls: + tc = r.tool_calls[0] + assert isinstance(tc, ToolCall) + assert isinstance(tc.id, str) + assert isinstance(tc.name, str) + assert isinstance(tc.input, dict) + + +# ------------------------------------------------------------------ # +# 11. Logfire integration (real) +# ------------------------------------------------------------------ # + + +class TestLogfireIntegration: + def test_logfire_configure_and_instrument(self): + """Configure Logfire, create a step, make a call — verify spans are sent.""" + from ace.observability import configure_logfire, is_configured + + configured = configure_logfire() + if not configured: + pytest.skip("Logfire not configured (missing LOGFIRE_TOKEN?)") + + assert is_configured() + + step = ClaudeSDKExecuteStep(model=MODEL, max_tokens=20) + ctx = ACEStepContext(sample="Say 'logfire test'", skillbook=None) + + result_ctx = step(ctx) + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + + assert r.success is True + assert r.output_tokens > 0 diff --git a/tests/test_claude_sdk_step.py b/tests/test_claude_sdk_step.py new file mode 100644 index 0000000000000000000000000000000000000000..626ae886ede2c114e661949e9c5ec80d234074a1 --- /dev/null +++ b/tests/test_claude_sdk_step.py @@ -0,0 +1,616 @@ +"""Tests for the Claude SDK integration step.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass +from types import SimpleNamespace +from typing import Any, List, Optional +from unittest.mock import MagicMock, patch + +import pytest +from pydantic import ValidationError + +from ace.core.context import ACEStepContext, SkillbookView +from ace.core.skillbook import Skillbook +from ace.integrations.claude_sdk import ( + ClaudeSDKExecuteStep, + ClaudeSDKResult, + ClaudeSDKToTrace, + ToolCall, +) + +# ------------------------------------------------------------------ # +# Helpers — mock Anthropic API objects +# ------------------------------------------------------------------ # + + +def _make_text_block(text: str) -> SimpleNamespace: + return SimpleNamespace(type="text", text=text) + + +def _make_tool_block( + tool_id: str = "toolu_01", name: str = "calculator", inp: Any = None +) -> SimpleNamespace: + return SimpleNamespace(type="tool_use", id=tool_id, name=name, input=inp or {}) + + +def _make_usage(input_tokens: int = 100, output_tokens: int = 50) -> SimpleNamespace: + return SimpleNamespace(input_tokens=input_tokens, output_tokens=output_tokens) + + +def _make_response( + content: list | None = None, + model: str = "claude-sonnet-4-20250514", + stop_reason: str = "end_turn", + input_tokens: int = 100, + output_tokens: int = 50, +) -> SimpleNamespace: + if content is None: + content = [_make_text_block("Hello, world!")] + return SimpleNamespace( + content=content, + model=model, + stop_reason=stop_reason, + usage=_make_usage(input_tokens, output_tokens), + ) + + +def _make_mock_client(response: SimpleNamespace | None = None) -> MagicMock: + client = MagicMock() + client.messages.create.return_value = response or _make_response() + return client + + +@dataclass +class FakeSample: + question: str = "What is 2+2?" + context: str = "math quiz" + ground_truth: str = "4" + metadata: dict = None # type: ignore[assignment] + + def __post_init__(self) -> None: + if self.metadata is None: + self.metadata = {} + + +# ------------------------------------------------------------------ # +# ClaudeSDKResult +# ------------------------------------------------------------------ # + + +class TestClaudeSDKResult: + def test_defaults(self): + r = ClaudeSDKResult(task="test", success=True) + assert r.task == "test" + assert r.success is True + assert r.output == "" + assert r.error is None + assert r.input_tokens == 0 + assert r.output_tokens == 0 + assert r.total_tokens == 0 + assert r.latency_seconds == 0.0 + assert r.tool_calls == [] + assert r.cited_skill_ids == [] + assert r.raw_response is None + + def test_full(self): + tc = ToolCall(id="toolu_01", name="calc", input={"expr": "1+1"}) + r = ClaudeSDKResult( + task="hello", + success=True, + output="world", + model="claude-sonnet-4-20250514", + stop_reason="end_turn", + input_tokens=100, + output_tokens=50, + total_tokens=150, + latency_seconds=1.5, + tool_calls=[tc], + cited_skill_ids=["math-001"], + ) + assert r.total_tokens == 150 + assert r.tool_calls[0].name == "calc" + + def test_auto_compute_total_tokens(self): + """total_tokens is auto-computed from input + output when left at 0.""" + r = ClaudeSDKResult( + task="test", success=True, input_tokens=100, output_tokens=50 + ) + assert r.total_tokens == 150 + + def test_explicit_total_not_overwritten(self): + """An explicitly set total_tokens is preserved.""" + r = ClaudeSDKResult( + task="test", + success=True, + input_tokens=100, + output_tokens=50, + total_tokens=999, + ) + assert r.total_tokens == 999 + + def test_negative_tokens_rejected(self): + """Negative token counts should be rejected by validation.""" + with pytest.raises(Exception): + ClaudeSDKResult(task="test", success=True, input_tokens=-1) + + def test_negative_latency_rejected(self): + """Negative latency should be rejected by validation.""" + with pytest.raises(Exception): + ClaudeSDKResult(task="test", success=True, latency_seconds=-0.1) + + def test_serialization(self): + """Result should serialise to dict/JSON (raw_response excluded).""" + r = ClaudeSDKResult( + task="test", + success=True, + input_tokens=10, + output_tokens=5, + raw_response=object(), + ) + d = r.model_dump() + assert d["task"] == "test" + assert "raw_response" not in d # excluded + + def test_tool_call_validation(self): + """ToolCall should validate required fields.""" + tc = ToolCall(id="toolu_01", name="calc") + assert tc.input == {} + + with pytest.raises(Exception): + ToolCall(name="calc") # missing id + + +# ------------------------------------------------------------------ # +# ClaudeSDKExecuteStep — contracts +# ------------------------------------------------------------------ # + + +class TestClaudeSDKExecuteStepContracts: + def test_requires_and_provides(self): + client = _make_mock_client() + step = ClaudeSDKExecuteStep(client=client) + assert "sample" in step.requires + assert "skillbook" in step.requires + assert "trace" in step.provides + + def test_not_available_raises_without_client(self): + with patch("ace.integrations.claude_sdk.ANTHROPIC_SDK_AVAILABLE", False): + with pytest.raises(ImportError, match="anthropic SDK not installed"): + ClaudeSDKExecuteStep() + + def test_injected_client_skips_availability_check(self): + with patch("ace.integrations.claude_sdk.ANTHROPIC_SDK_AVAILABLE", False): + step = ClaudeSDKExecuteStep(client=_make_mock_client()) + assert "sample" in step.requires + + def test_invalid_max_tokens_rejected(self): + with pytest.raises(ValidationError): + ClaudeSDKExecuteStep(client=_make_mock_client(), max_tokens=0) + + def test_invalid_temperature_rejected(self): + with pytest.raises(ValidationError): + ClaudeSDKExecuteStep(client=_make_mock_client(), temperature=1.5) + + +# ------------------------------------------------------------------ # +# ClaudeSDKExecuteStep — execution +# ------------------------------------------------------------------ # + + +class TestClaudeSDKExecuteStepExecution: + def test_basic_call(self): + response = _make_response( + content=[_make_text_block("The answer is 4")], + input_tokens=80, + output_tokens=20, + ) + client = _make_mock_client(response) + step = ClaudeSDKExecuteStep(client=client, model="claude-sonnet-4-20250514") + + ctx = ACEStepContext(sample="What is 2+2?", skillbook=None) + result_ctx = step(ctx) + + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + assert r.success is True + assert r.output == "The answer is 4" + assert r.input_tokens == 80 + assert r.output_tokens == 20 + assert r.total_tokens == 100 + assert r.latency_seconds >= 0 + assert r.model == "claude-sonnet-4-20250514" + assert r.stop_reason == "end_turn" + + # Verify API was called correctly + call_kwargs = client.messages.create.call_args[1] + assert call_kwargs["model"] == "claude-sonnet-4-20250514" + assert call_kwargs["messages"] == [{"role": "user", "content": "What is 2+2?"}] + + def test_with_system_prompt(self): + client = _make_mock_client() + step = ClaudeSDKExecuteStep( + client=client, + system_prompt="You are a math tutor.", + ) + + ctx = ACEStepContext(sample="What is 2+2?", skillbook=None) + step(ctx) + + call_kwargs = client.messages.create.call_args[1] + assert call_kwargs["system"] == "You are a math tutor." + + def test_skillbook_injection(self): + client = _make_mock_client() + step = ClaudeSDKExecuteStep(client=client, inject_skillbook=True) + + sb = Skillbook() + sb.add_skill("math", "Always show your work") + ctx = ACEStepContext(sample="What is 2+2?", skillbook=SkillbookView(sb)) + step(ctx) + + call_kwargs = client.messages.create.call_args[1] + assert "system" in call_kwargs + assert "Strategic Knowledge" in call_kwargs["system"] + + def test_skillbook_injection_with_system_prompt(self): + client = _make_mock_client() + step = ClaudeSDKExecuteStep( + client=client, + system_prompt="You are a tutor.", + inject_skillbook=True, + ) + + sb = Skillbook() + sb.add_skill("math", "Show work") + ctx = ACEStepContext(sample="test", skillbook=SkillbookView(sb)) + step(ctx) + + call_kwargs = client.messages.create.call_args[1] + system = call_kwargs["system"] + assert "You are a tutor." in system + assert "Strategic Knowledge" in system + + def test_skillbook_injection_disabled(self): + client = _make_mock_client() + step = ClaudeSDKExecuteStep(client=client, inject_skillbook=False) + + sb = Skillbook() + sb.add_skill("math", "Show work") + ctx = ACEStepContext(sample="test", skillbook=SkillbookView(sb)) + step(ctx) + + call_kwargs = client.messages.create.call_args[1] + assert "system" not in call_kwargs + + def test_empty_skillbook_no_system(self): + client = _make_mock_client() + step = ClaudeSDKExecuteStep(client=client, inject_skillbook=True) + + sb = Skillbook() + ctx = ACEStepContext(sample="test", skillbook=SkillbookView(sb)) + step(ctx) + + call_kwargs = client.messages.create.call_args[1] + assert "system" not in call_kwargs + + def test_with_tools(self): + tools = [ + { + "name": "calculator", + "description": "A calculator", + "input_schema": { + "type": "object", + "properties": {"expr": {"type": "string"}}, + }, + } + ] + response = _make_response( + content=[ + _make_tool_block("toolu_01", "calculator", {"expr": "2+2"}), + _make_text_block("The result is 4"), + ] + ) + client = _make_mock_client(response) + step = ClaudeSDKExecuteStep(client=client, tools=tools) + + ctx = ACEStepContext(sample="Calculate 2+2", skillbook=None) + result_ctx = step(ctx) + + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + assert r.success is True + assert r.output == "The result is 4" + assert len(r.tool_calls) == 1 + assert r.tool_calls[0].name == "calculator" + assert r.tool_calls[0].input == {"expr": "2+2"} + + call_kwargs = client.messages.create.call_args[1] + assert call_kwargs["tools"] == tools + + def test_api_error_handled(self): + client = _make_mock_client() + client.messages.create.side_effect = RuntimeError("API down") + step = ClaudeSDKExecuteStep(client=client) + + ctx = ACEStepContext(sample="test", skillbook=None) + result_ctx = step(ctx) + + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + assert r.success is False + assert "API down" in r.error + assert r.latency_seconds >= 0 + + def test_temperature_and_max_tokens(self): + client = _make_mock_client() + step = ClaudeSDKExecuteStep(client=client, temperature=0.7, max_tokens=1024) + + ctx = ACEStepContext(sample="test", skillbook=None) + step(ctx) + + call_kwargs = client.messages.create.call_args[1] + assert call_kwargs["temperature"] == 0.7 + assert call_kwargs["max_tokens"] == 1024 + + +# ------------------------------------------------------------------ # +# ClaudeSDKExecuteStep — task extraction +# ------------------------------------------------------------------ # + + +class TestClaudeSDKTaskExtraction: + def test_string_sample(self): + assert ClaudeSDKExecuteStep._extract_task("hello") == "hello" + + def test_sample_with_question(self): + sample = FakeSample(question="What is 2+2?", context="") + result = ClaudeSDKExecuteStep._extract_task(sample) + assert result == "What is 2+2?" + + def test_sample_with_context(self): + sample = FakeSample(question="What is 2+2?", context="math quiz") + result = ClaudeSDKExecuteStep._extract_task(sample) + assert "What is 2+2?" in result + assert "Context: math quiz" in result + + def test_arbitrary_object(self): + result = ClaudeSDKExecuteStep._extract_task(42) + assert result == "42" + + +# ------------------------------------------------------------------ # +# ClaudeSDKExecuteStep — skill ID extraction +# ------------------------------------------------------------------ # + + +class TestClaudeSDKSkillExtraction: + def test_extracts_skill_ids(self): + response = _make_response( + content=[ + _make_text_block( + "Following [math-00001], the answer is 4. " + "Also [general-00042] applies." + ) + ] + ) + client = _make_mock_client(response) + step = ClaudeSDKExecuteStep(client=client) + + ctx = ACEStepContext(sample="test", skillbook=None) + result_ctx = step(ctx) + + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + assert "math-00001" in r.cited_skill_ids + assert "general-00042" in r.cited_skill_ids + + def test_no_skill_ids(self): + response = _make_response(content=[_make_text_block("No citations here")]) + client = _make_mock_client(response) + step = ClaudeSDKExecuteStep(client=client) + + ctx = ACEStepContext(sample="test", skillbook=None) + result_ctx = step(ctx) + + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + assert r.cited_skill_ids == [] + + +# ------------------------------------------------------------------ # +# ClaudeSDKExecuteStep — observability logging +# ------------------------------------------------------------------ # + + +class TestClaudeSDKObservability: + def test_auto_instruments_anthropic_when_logfire_configured(self): + mock_logfire = MagicMock() + mock_ctx = MagicMock() + mock_logfire.instrument_anthropic.return_value = mock_ctx + + with ( + patch("ace.observability.is_configured", return_value=True), + patch.dict("sys.modules", {"logfire": mock_logfire}), + ): + step = ClaudeSDKExecuteStep(client=_make_mock_client()) + + mock_logfire.instrument_anthropic.assert_called_once_with(step._client) + mock_ctx.__enter__.assert_not_called() + + def test_logs_metrics(self, caplog): + response = _make_response(input_tokens=200, output_tokens=100) + client = _make_mock_client(response) + step = ClaudeSDKExecuteStep(client=client) + + with caplog.at_level(logging.INFO, logger="ace.integrations.claude_sdk"): + ctx = ACEStepContext(sample="test", skillbook=None) + step(ctx) + + assert "ClaudeSDK:" in caplog.text + assert "tokens=" in caplog.text + + def test_logs_error(self, caplog): + client = _make_mock_client() + client.messages.create.side_effect = RuntimeError("boom") + step = ClaudeSDKExecuteStep(client=client) + + with caplog.at_level(logging.ERROR, logger="ace.integrations.claude_sdk"): + ctx = ACEStepContext(sample="test", skillbook=None) + step(ctx) + + assert "failed" in caplog.text.lower() + + def test_logfire_span_on_success(self): + """When Logfire is configured, __call__ opens a span with attributes.""" + mock_span = MagicMock() + mock_logfire = MagicMock() + mock_logfire.span.return_value.__enter__ = MagicMock(return_value=mock_span) + mock_logfire.span.return_value.__exit__ = MagicMock(return_value=False) + + response = _make_response(input_tokens=50, output_tokens=25) + client = _make_mock_client(response) + step = ClaudeSDKExecuteStep(client=client) + + with ( + patch( + "ace.integrations.claude_sdk._get_logfire", return_value=mock_logfire + ), + ): + ctx = ACEStepContext(sample="What is 2+2?", skillbook=None) + result_ctx = step(ctx) + + # Span was opened + mock_logfire.span.assert_called_once() + call_kwargs = mock_logfire.span.call_args + assert call_kwargs[0][0] == "ClaudeSDKExecuteStep" + assert call_kwargs[1]["model"] == "claude-sonnet-4-20250514" + + # Attributes were set on the span + attr_calls = {c[0][0]: c[0][1] for c in mock_span.set_attribute.call_args_list} + assert attr_calls["success"] is True + assert attr_calls["input_tokens"] == 50 + assert attr_calls["output_tokens"] == 25 + assert attr_calls["total_tokens"] == 75 + assert "error" not in attr_calls + + # logfire.info was called with metrics + mock_logfire.info.assert_called_once() + info_kwargs = mock_logfire.info.call_args[1] + assert info_kwargs["input_tokens"] == 50 + assert info_kwargs["output_tokens"] == 25 + + def test_logfire_span_on_failure(self): + """On API error, span captures error attribute and logfire.error is called.""" + mock_span = MagicMock() + mock_logfire = MagicMock() + mock_logfire.span.return_value.__enter__ = MagicMock(return_value=mock_span) + mock_logfire.span.return_value.__exit__ = MagicMock(return_value=False) + + client = _make_mock_client() + client.messages.create.side_effect = RuntimeError("rate limited") + step = ClaudeSDKExecuteStep(client=client) + + with ( + patch( + "ace.integrations.claude_sdk._get_logfire", return_value=mock_logfire + ), + ): + ctx = ACEStepContext(sample="test", skillbook=None) + step(ctx) + + # Span captured the error attribute + attr_calls = {c[0][0]: c[0][1] for c in mock_span.set_attribute.call_args_list} + assert attr_calls["success"] is False + assert "rate limited" in attr_calls["error"] + + # logfire.error was called + mock_logfire.error.assert_called_once() + error_kwargs = mock_logfire.error.call_args[1] + assert "rate limited" in error_kwargs["error"] + + def test_no_logfire_noop(self): + """When Logfire is not configured, execution proceeds without spans.""" + client = _make_mock_client() + step = ClaudeSDKExecuteStep(client=client) + + with patch("ace.integrations.claude_sdk._get_logfire", return_value=None): + ctx = ACEStepContext(sample="test", skillbook=None) + result_ctx = step(ctx) + + r: ClaudeSDKResult = result_ctx.trace # type: ignore[assignment] + assert r.success is True + + +# ------------------------------------------------------------------ # +# ClaudeSDKToTrace +# ------------------------------------------------------------------ # + + +class TestClaudeSDKToTrace: + def test_requires_and_provides(self): + step = ClaudeSDKToTrace() + assert "trace" in step.requires + assert "trace" in step.provides + + def test_success_trace(self): + r = ClaudeSDKResult( + task="What is 2+2?", + success=True, + output="4", + model="claude-sonnet-4-20250514", + stop_reason="end_turn", + input_tokens=100, + output_tokens=50, + total_tokens=150, + latency_seconds=1.2, + cited_skill_ids=["math-001"], + ) + ctx = ACEStepContext(trace=r) + result_ctx = ClaudeSDKToTrace()(ctx) + + trace = result_ctx.trace + assert trace["question"] == "What is 2+2?" + assert trace["answer"] == "4" + assert trace["skill_ids"] == ["math-001"] + assert "succeeded" in trace["reasoning"] + assert "claude-sonnet-4-20250514" in trace["reasoning"] + assert "100" in trace["reasoning"] # input tokens + assert "50" in trace["reasoning"] # output tokens + assert "1.2" in trace["reasoning"] # latency + assert "succeeded" in trace["feedback"] + assert trace["ground_truth"] is None + + def test_failure_trace(self): + r = ClaudeSDKResult( + task="fail", + success=False, + error="API timeout", + model="claude-sonnet-4-20250514", + latency_seconds=30.0, + ) + ctx = ACEStepContext(trace=r) + result_ctx = ClaudeSDKToTrace()(ctx) + + trace = result_ctx.trace + assert trace["question"] == "fail" + assert trace["answer"] == "" + assert "failed" in trace["reasoning"] + assert "API timeout" in trace["reasoning"] + assert "failed" in trace["feedback"] + assert "API timeout" in trace["feedback"] + + def test_tool_calls_in_reasoning(self): + r = ClaudeSDKResult( + task="calc", + success=True, + output="4", + model="claude-sonnet-4-20250514", + tool_calls=[ + ToolCall(id="toolu_01", name="calculator", input={"expr": "2+2"}), + ToolCall(id="toolu_02", name="formatter"), + ], + ) + ctx = ACEStepContext(trace=r) + result_ctx = ClaudeSDKToTrace()(ctx) + + trace = result_ctx.trace + assert "Tool calls (2)" in trace["reasoning"] + assert "calculator" in trace["reasoning"] + assert "formatter" in trace["reasoning"] diff --git a/tests/test_kayba_cli.py b/tests/test_kayba_cli.py new file mode 100644 index 0000000000000000000000000000000000000000..c1ee936696441f2c0f4e5d1f6d82352bf43a1a72 --- /dev/null +++ b/tests/test_kayba_cli.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest +from click.testing import CliRunner + +from ace.cli import cloud +from ace.cli.client import KaybaAPIError, KaybaClient + + +class _FakeTraceClient: + def __init__(self, traces=None): + self._traces = traces or [] + self.upload_calls: list[list[dict[str, str]]] = [] + + def list_traces(self): + return {"traces": self._traces} + + def upload_traces(self, traces): + self.upload_calls.append(traces) + return { + "count": len(traces), + "traces": [ + {"id": f"trace-{idx}", "filename": trace["filename"]} + for idx, trace in enumerate(traces, start=1) + ], + } + + +class _FakeResponse: + def __init__(self, status_code: int, text: str, json_data=None): + self.status_code = status_code + self.text = text + self._json_data = json_data + + def json(self): + if self._json_data is None: + raise ValueError("no json") + return self._json_data + + +class _FakeSession: + def __init__(self, response: _FakeResponse): + self.response = response + self.headers = {} + + def request(self, method: str, url: str, json=None, params=None): + return self.response + + +def test_detect_file_type_handles_jsonl_and_markdown(): + assert cloud._detect_file_type("trace.jsonl") == "json" + assert cloud._detect_file_type("trace.markdown") == "md" + + +def test_traces_list_empty_state_explains_manual_upload(monkeypatch): + runner = CliRunner() + fake_client = _FakeTraceClient() + monkeypatch.setattr(cloud, "_client", lambda api_key, base_url: fake_client) + + result = runner.invoke(cloud.traces_list, []) + + assert result.exit_code == 0 + assert "Kayba does not auto-import local agent transcripts yet." in result.output + assert "~/.claude/projects//*.jsonl" in result.output + + +def test_traces_upload_skips_oversized_files(tmp_path: Path, monkeypatch): + runner = CliRunner() + fake_client = _FakeTraceClient() + monkeypatch.setattr(cloud, "_client", lambda api_key, base_url: fake_client) + + small = tmp_path / "small.jsonl" + small.write_text('{"ok": true}\n', encoding="utf-8") + large = tmp_path / "large.txt" + large.write_text("x" * (cloud.MAX_TRACE_CHARS + 1), encoding="utf-8") + + result = runner.invoke(cloud.traces_upload, [str(small), str(large)]) + + assert result.exit_code == 0 + assert "Uploaded 1 trace(s)." in result.output + assert "Skipping large.txt" in result.output + assert len(fake_client.upload_calls) == 1 + assert fake_client.upload_calls[0][0]["filename"] == "small.jsonl" + assert fake_client.upload_calls[0][0]["fileType"] == "json" + + +def test_prompts_install_replaces_managed_block(tmp_path: Path): + runner = CliRunner() + prompt_file = tmp_path / "prompt.md" + target_file = tmp_path / "CLAUDE.md" + prompt_file.write_text("First prompt", encoding="utf-8") + + first = runner.invoke( + cloud.prompts_install, + ["--input", str(prompt_file), "--target", "claude-code", "--file", str(target_file)], + ) + assert first.exit_code == 0 + first_text = target_file.read_text(encoding="utf-8") + assert "First prompt" in first_text + assert first_text.count(cloud.PROMPT_BLOCK_START) == 1 + + prompt_file.write_text("Second prompt", encoding="utf-8") + second = runner.invoke( + cloud.prompts_install, + ["--input", str(prompt_file), "--target", "claude-code", "--file", str(target_file)], + ) + assert second.exit_code == 0 + second_text = target_file.read_text(encoding="utf-8") + assert "Second prompt" in second_text + assert "First prompt" not in second_text + assert second_text.count(cloud.PROMPT_BLOCK_START) == 1 + + +def test_client_formats_non_json_http_errors(): + client = KaybaClient(api_key="test-key", base_url="https://example.com") + client.session = _FakeSession( + _FakeResponse(502, "gateway exploded in a surprisingly long way") + ) + + with pytest.raises(KaybaAPIError) as exc: + client._request("GET", "/traces") + + assert exc.value.code == "HTTP_ERROR" + assert "HTTP 502 from Kayba API:" in exc.value.message + assert "gateway exploded" in exc.value.message diff --git a/tests/test_load_traces_step.py b/tests/test_load_traces_step.py new file mode 100644 index 0000000000000000000000000000000000000000..3c112e7bb83241808902d12e624959a86a479bd2 --- /dev/null +++ b/tests/test_load_traces_step.py @@ -0,0 +1,148 @@ +"""Tests for LoadTracesStep — generic JSONL file loader.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from ace.core.context import ACEStepContext +from ace.steps.load_traces import LoadTracesStep + + +@pytest.fixture +def load_step(): + return LoadTracesStep() + + +@pytest.fixture +def sample_jsonl(tmp_path: Path) -> Path: + """Create a sample JSONL file with valid events.""" + path = tmp_path / "session.jsonl" + events = [ + {"type": "session", "id": "s1", "timestamp": "2026-01-01T00:00:00Z"}, + { + "type": "message", + "id": "m1", + "timestamp": "2026-01-01T00:00:01Z", + "message": {"role": "user", "content": [{"type": "text", "text": "hello"}]}, + }, + { + "type": "message", + "id": "m2", + "timestamp": "2026-01-01T00:00:02Z", + "message": { + "role": "assistant", + "content": [{"type": "text", "text": "hi there"}], + }, + }, + ] + path.write_text("\n".join(json.dumps(e) for e in events) + "\n") + return path + + +class TestLoadTracesStep: + def test_requires_provides(self, load_step: LoadTracesStep): + assert load_step.requires == frozenset({"sample"}) + assert load_step.provides == frozenset({"trace"}) + + def test_valid_jsonl(self, load_step: LoadTracesStep, sample_jsonl: Path): + ctx = ACEStepContext(sample=str(sample_jsonl)) + result = load_step(ctx) + + assert isinstance(result.trace, list) + assert len(result.trace) == 3 + assert result.trace[0]["type"] == "session" + assert result.trace[1]["type"] == "message" + assert result.trace[2]["type"] == "message" + + def test_empty_file(self, load_step: LoadTracesStep, tmp_path: Path): + path = tmp_path / "empty.jsonl" + path.write_text("") + + ctx = ACEStepContext(sample=str(path)) + result = load_step(ctx) + + assert result.trace == [] + + def test_missing_file(self, load_step: LoadTracesStep, tmp_path: Path): + path = tmp_path / "nonexistent.jsonl" + + ctx = ACEStepContext(sample=str(path)) + result = load_step(ctx) + + assert result.trace == [] + + def test_skips_unparseable_lines(self, load_step: LoadTracesStep, tmp_path: Path): + path = tmp_path / "mixed.jsonl" + path.write_text( + '{"type": "session", "id": "s1"}\n' + "this is not json\n" + '{"type": "message", "id": "m1"}\n' + "\n" # blank line + "also not json\n" + ) + + ctx = ACEStepContext(sample=str(path)) + result = load_step(ctx) + + assert len(result.trace) == 2 + assert result.trace[0]["type"] == "session" + assert result.trace[1]["type"] == "message" + + def test_preserves_full_event_data(self, load_step: LoadTracesStep, tmp_path: Path): + """Verify no truncation of event fields.""" + long_text = "x" * 10000 + event = { + "type": "message", + "id": "m1", + "timestamp": "2026-01-01T00:00:00Z", + "message": { + "role": "assistant", + "content": [ + {"type": "thinking", "thinking": long_text}, + { + "type": "toolCall", + "id": "tc1", + "name": "Read", + "arguments": {"file_path": "/a/b/c", "data": long_text}, + }, + ], + }, + } + path = tmp_path / "full.jsonl" + path.write_text(json.dumps(event) + "\n") + + ctx = ACEStepContext(sample=str(path)) + result = load_step(ctx) + + assert len(result.trace) == 1 + msg = result.trace[0]["message"] + assert msg["content"][0]["thinking"] == long_text + assert msg["content"][1]["arguments"]["data"] == long_text + + def test_returns_new_context(self, load_step: LoadTracesStep, sample_jsonl: Path): + """Step should return a new context, not mutate the original.""" + ctx = ACEStepContext(sample=str(sample_jsonl)) + result = load_step(ctx) + + assert result is not ctx + assert ctx.trace is None # original unchanged + assert result.trace is not None + + def test_with_real_sample_fixture(self, load_step: LoadTracesStep): + """Test with the actual sample JSONL from the examples directory.""" + sample = Path(__file__).resolve().parents[1] / "examples" / "openclaw" + jsonl_files = list(sample.glob("*.jsonl")) + if not jsonl_files: + pytest.skip("No sample JSONL files in examples/openclaw/") + + ctx = ACEStepContext(sample=str(jsonl_files[0])) + result = load_step(ctx) + + assert isinstance(result.trace, list) + assert len(result.trace) > 0 + # Should contain message events (sample file may have corrupted first line) + types = {e.get("type") for e in result.trace} + assert "message" in types diff --git a/tests/test_openclaw.py b/tests/test_openclaw.py new file mode 100644 index 0000000000000000000000000000000000000000..3b90f0a54a2bcafecb50cf78771c14cac320d011 --- /dev/null +++ b/tests/test_openclaw.py @@ -0,0 +1,321 @@ +"""Tests for OpenClaw integration — OpenClawToTraceStep and end-to-end pipeline.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Optional + +import pytest + +from ace.core.context import ACEStepContext, SkillbookView +from ace.core.outputs import ( + AgentOutput, + ReflectorOutput, + SkillManagerOutput, +) +from ace.core.skillbook import Skillbook, UpdateBatch, UpdateOperation +from ace.integrations.openclaw import OpenClawToTraceStep +from ace.steps import learning_tail +from ace.steps.load_traces import LoadTracesStep + +from pipeline import Pipeline + +# ------------------------------------------------------------------ # +# Helpers — mock roles +# ------------------------------------------------------------------ # + + +class MockReflector: + """Minimal mock satisfying ReflectorLike.""" + + def __init__(self, output: ReflectorOutput | None = None): + self.output = output or ReflectorOutput( + reasoning="test reasoning", + correct_approach="test approach", + key_insight="test insight", + ) + self.calls: list[dict] = [] + + def reflect( + self, + *, + question: str, + agent_output: AgentOutput, + skillbook: Any, + ground_truth: Optional[str] = None, + feedback: Optional[str] = None, + **kwargs: Any, + ) -> ReflectorOutput: + self.calls.append( + { + "question": question, + "agent_output": agent_output, + "ground_truth": ground_truth, + "feedback": feedback, + **kwargs, + } + ) + return self.output + + +class MockSkillManager: + """Minimal mock satisfying SkillManagerLike. + + The real SM mutates the skillbook directly via tool calls; this mock + applies its pre-canned ``output`` to the incoming skillbook so + ``UpdateStep`` behaves like the live code path. + """ + + def __init__(self, output: SkillManagerOutput | None = None): + self.output = output or SkillManagerOutput( + update=UpdateBatch(reasoning="test", operations=[]), + ) + self.calls: list[dict] = [] + + def update_skills( + self, + *, + reflections: tuple[ReflectorOutput, ...], + skillbook: Any, + question_context: str, + progress: str, + **kwargs: Any, + ) -> SkillManagerOutput: + self.calls.append( + { + "reflections": reflections, + "question_context": question_context, + "progress": progress, + } + ) + skillbook.apply_update(self.output.update) + return self.output + + +# ------------------------------------------------------------------ # +# Fixtures +# ------------------------------------------------------------------ # + + +@pytest.fixture +def sample_jsonl(tmp_path: Path) -> Path: + """Create a minimal OpenClaw session JSONL file.""" + events = [ + { + "type": "session", + "id": "s1", + "timestamp": "2026-01-01T00:00:00Z", + "version": 1, + "cwd": "/app", + }, + { + "type": "message", + "id": "m1", + "parentId": "s1", + "timestamp": "2026-01-01T00:00:01Z", + "message": { + "role": "user", + "content": [{"type": "text", "text": "Hello, help me debug this."}], + }, + }, + { + "type": "message", + "id": "m2", + "parentId": "m1", + "timestamp": "2026-01-01T00:00:02Z", + "message": { + "role": "assistant", + "content": [ + {"type": "thinking", "thinking": "Let me analyze the issue..."}, + {"type": "text", "text": "I'll help you debug this."}, + { + "type": "toolCall", + "id": "tc1", + "name": "Read", + "arguments": {"file_path": "/app/main.py"}, + }, + ], + }, + }, + { + "type": "message", + "id": "m3", + "parentId": "m2", + "timestamp": "2026-01-01T00:00:03Z", + "message": { + "role": "toolResult", + "content": [ + {"type": "text", "text": "def main():\n print('hello')"} + ], + }, + }, + ] + path = tmp_path / "test-session.jsonl" + path.write_text("\n".join(json.dumps(e) for e in events) + "\n") + return path + + +# ------------------------------------------------------------------ # +# OpenClawToTraceStep tests +# ------------------------------------------------------------------ # + + +class TestOpenClawToTraceStep: + def test_requires_provides(self): + step = OpenClawToTraceStep() + assert step.requires == frozenset({"trace"}) + assert step.provides == frozenset({"trace"}) + + def test_converts_to_trace_dict(self): + """Step should convert raw events into a structured trace dict.""" + raw_events = [ + {"type": "session", "id": "s1", "cwd": "/app"}, + { + "type": "message", + "id": "m1", + "message": { + "role": "user", + "content": [{"type": "text", "text": "Hello"}], + }, + }, + { + "type": "message", + "id": "m2", + "message": { + "role": "assistant", + "content": [{"type": "text", "text": "Hi there"}], + }, + }, + ] + ctx = ACEStepContext(trace=raw_events) + result = OpenClawToTraceStep()(ctx) + + trace = result.trace + assert isinstance(trace, dict) + assert trace["question"] == "User: Hello" + assert trace["answer"] == "Hi there" + assert trace["skill_ids"] == [] + assert trace["ground_truth"] is None + assert "reasoning" in trace + assert "feedback" in trace + + def test_none_trace(self): + """Step should handle None trace gracefully.""" + ctx = ACEStepContext(trace=None) + result = OpenClawToTraceStep()(ctx) + assert result.trace is None + + def test_empty_list_trace(self): + """Step should handle empty list trace gracefully.""" + ctx = ACEStepContext(trace=[]) + result = OpenClawToTraceStep()(ctx) + assert result.trace == [] + + +# ------------------------------------------------------------------ # +# End-to-end: LoadTracesStep → OpenClawToTraceStep → learning_tail +# ------------------------------------------------------------------ # + + +class TestOpenClawEndToEnd: + def test_load_and_convert(self, sample_jsonl: Path): + """LoadTracesStep → OpenClawToTraceStep should produce trace data.""" + load_step = LoadTracesStep() + convert_step = OpenClawToTraceStep() + + ctx = ACEStepContext(sample=str(sample_jsonl)) + ctx = load_step(ctx) + assert isinstance(ctx.trace, list) + assert len(ctx.trace) == 4 + + ctx = convert_step(ctx) + # Converted to structured trace dict + assert isinstance(ctx.trace, dict) + assert "question" in ctx.trace + assert "reasoning" in ctx.trace + assert "answer" in ctx.trace + assert ctx.trace["skill_ids"] == [] + assert ctx.trace["ground_truth"] is None + + def test_full_pipeline_with_mocks(self, sample_jsonl: Path): + """Full pipeline: load → convert → reflect → tag → update → apply.""" + reflector = MockReflector() + skill_manager = MockSkillManager() + skillbook = Skillbook() + + load_step = LoadTracesStep() + convert_step = OpenClawToTraceStep() + + steps = [ + load_step, + convert_step, + *learning_tail(reflector, skill_manager, skillbook), + ] + + pipeline = Pipeline(steps) + + ctx = ACEStepContext( + sample=str(sample_jsonl), + skillbook=SkillbookView(skillbook), + ) + + result = pipeline.run([ctx]) + pipeline.wait_for_background() + + assert len(result) == 1 + assert len(reflector.calls) == 1 + assert len(skill_manager.calls) == 1 + + def test_pipeline_with_add_operation(self, sample_jsonl: Path): + """Pipeline with a SkillManager that adds a skill.""" + add_op = UpdateOperation( + type="ADD", + section="debugging", + issue="Use structured logging for better debug traces", + insight="Use structured logging for better debug traces", + skill_id=None, + metadata={"helpful": 1, "harmful": 0, "neutral": 0}, + ) + sm_output = SkillManagerOutput( + update=UpdateBatch(reasoning="Found useful pattern", operations=[add_op]), + ) + reflector = MockReflector() + skill_manager = MockSkillManager(output=sm_output) + skillbook = Skillbook() + + steps = [ + LoadTracesStep(), + OpenClawToTraceStep(), + *learning_tail(reflector, skill_manager, skillbook), + ] + + pipeline = Pipeline(steps) + ctx = ACEStepContext( + sample=str(sample_jsonl), + skillbook=SkillbookView(skillbook), + ) + + pipeline.run([ctx]) + pipeline.wait_for_background() + + # Skillbook should now have one skill (legacy "debugging" → "context") + assert len(skillbook.skills()) == 1 + skill = skillbook.skills()[0] + assert skill.section == "context" + assert "structured logging" in skill.insight + + def test_empty_session_skipped(self, tmp_path: Path): + """Empty JSONL should produce empty trace.""" + path = tmp_path / "empty.jsonl" + path.write_text("") + + load_step = LoadTracesStep() + convert_step = OpenClawToTraceStep() + + ctx = ACEStepContext(sample=str(path)) + ctx = load_step(ctx) + assert ctx.trace == [] + + ctx = convert_step(ctx) + assert ctx.trace == [] diff --git a/tests/test_pipeline_callback.py b/tests/test_pipeline_callback.py new file mode 100644 index 0000000000000000000000000000000000000000..d0205153bafa029f929f50c9091fa7a718e813d3 --- /dev/null +++ b/tests/test_pipeline_callback.py @@ -0,0 +1,71 @@ +"""Tests for Pipeline.run() on_sample_done callback.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest + +from pipeline import Pipeline, SampleResult, StepContext + + +class PassthroughStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx + + +class FailingStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + raise RuntimeError("boom") + + +class TestOnSampleDone: + def test_callback_called_per_sample(self): + pipe = Pipeline([PassthroughStep()]) + contexts = [StepContext(sample=i) for i in range(5)] + cb = MagicMock() + + pipe.run(contexts, on_sample_done=cb) + + assert cb.call_count == 5 + for call in cb.call_args_list: + result = call[0][0] + assert isinstance(result, SampleResult) + assert result.error is None + + def test_callback_called_on_error(self): + pipe = Pipeline([FailingStep()]) + contexts = [StepContext(sample="x")] + cb = MagicMock() + + pipe.run(contexts, on_sample_done=cb) + + assert cb.call_count == 1 + result = cb.call_args[0][0] + assert isinstance(result, SampleResult) + assert isinstance(result.error, RuntimeError) + assert result.failed_at == "FailingStep" + + def test_none_callback_is_noop(self): + pipe = Pipeline([PassthroughStep()]) + contexts = [StepContext(sample=1)] + + # Should not raise + results = pipe.run(contexts, on_sample_done=None) + assert len(results) == 1 + assert results[0].error is None + + def test_callback_with_multiple_workers(self): + pipe = Pipeline([PassthroughStep()]) + contexts = [StepContext(sample=i) for i in range(10)] + cb = MagicMock() + + pipe.run(contexts, workers=4, on_sample_done=cb) + + assert cb.call_count == 10 diff --git a/tests/test_pipeline_exports.py b/tests/test_pipeline_exports.py new file mode 100644 index 0000000000000000000000000000000000000000..5b97d7b5faf5d7e6ff1b8872c5d60b72cedd7a06 --- /dev/null +++ b/tests/test_pipeline_exports.py @@ -0,0 +1,230 @@ +"""Tests that pipeline composition classes are importable from ace. + +Verifies the public API surface for pipeline-first composition. +""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from ace.core.outputs import ( + AgentOutput, + ReflectorOutput, + SkillManagerOutput, +) +from ace.core.skillbook import Skillbook, UpdateBatch, UpdateOperation + +# ------------------------------------------------------------------ # +# Mock roles for build_steps() tests +# ------------------------------------------------------------------ # + + +class MockAgent: + def run(self, *a: Any, **kw: Any) -> AgentOutput: + return AgentOutput(reasoning="r", final_answer="a") + + +class MockReflector: + def reflect(self, *a: Any, **kw: Any) -> ReflectorOutput: + return ReflectorOutput( + reasoning="r", + correct_approach="a", + key_insight="i", + ) + + +class MockSkillManager: + def update_skills(self, *a: Any, **kw: Any) -> SkillManagerOutput: + return SkillManagerOutput( + update=UpdateBatch( + reasoning="r", + operations=[UpdateOperation(type="ADD", section="learned", issue="c")], + ), + ) + + +# ------------------------------------------------------------------ # +# Pipeline primitives are importable from ace +# ------------------------------------------------------------------ # + + +class TestPipelineExports: + def test_pipeline_class(self): + from ace import Pipeline + + assert Pipeline is not None + + def test_branch_class(self): + from ace import Branch + + assert Branch is not None + + def test_merge_strategy(self): + from ace import MergeStrategy + + assert MergeStrategy is not None + + def test_step_protocol(self): + from ace import StepProtocol + + assert StepProtocol is not None + + def test_sample_result(self): + from ace import SampleResult + + assert SampleResult is not None + + +# ------------------------------------------------------------------ # +# ACE context types are importable from ace +# ------------------------------------------------------------------ # + + +class TestContextExports: + def test_ace_step_context(self): + from ace import ACEStepContext + + assert ACEStepContext is not None + + def test_skillbook_view(self): + from ace import SkillbookView + + assert SkillbookView is not None + + def test_ace_runner(self): + from ace import ACERunner + + assert ACERunner is not None + + +# ------------------------------------------------------------------ # +# All steps are importable from ace +# ------------------------------------------------------------------ # + + +class TestStepExports: + @pytest.mark.parametrize( + "name", + [ + "AgentStep", + "EvaluateStep", + "ReflectStep", + "UpdateStep", + "DeduplicateStep", + "CheckpointStep", + "LoadTracesStep", + "ExportSkillbookMarkdownStep", + "ObservabilityStep", + "PersistStep", + "learning_tail", + ], + ) + def test_step_importable(self, name: str): + import ace + + assert hasattr(ace, name), f"{name} not in ace" + + def test_all_steps_in_dunder_all(self): + import ace + + step_names = [ + "AgentStep", + "EvaluateStep", + "ReflectStep", + "UpdateStep", + "DeduplicateStep", + "CheckpointStep", + "LoadTracesStep", + "ExportSkillbookMarkdownStep", + "ObservabilityStep", + "PersistStep", + "learning_tail", + ] + for name in step_names: + assert name in ace.__all__, f"{name} not in __all__" + + +# ------------------------------------------------------------------ # +# build_steps() returns expected step types +# ------------------------------------------------------------------ # + + +class TestBuildSteps: + def test_ace_build_steps(self): + from ace import ACE + from ace.steps import AgentStep, EvaluateStep, ReflectStep + + steps = ACE.build_steps( + agent=MockAgent(), + reflector=MockReflector(), + skill_manager=MockSkillManager(), + ) + assert isinstance(steps, list) + assert len(steps) >= 4 # Agent, Evaluate, Reflect, Update + assert isinstance(steps[0], AgentStep) + assert isinstance(steps[1], EvaluateStep) + assert isinstance(steps[2], ReflectStep) + + def test_trace_analyser_build_steps(self): + from ace import TraceAnalyser + from ace.steps import ReflectStep + + steps = TraceAnalyser.build_steps( + reflector=MockReflector(), + skill_manager=MockSkillManager(), + ) + assert isinstance(steps, list) + assert len(steps) >= 2 # Reflect, Update + assert isinstance(steps[0], ReflectStep) + + def test_ace_from_roles_delegates_to_build_steps(self): + """from_roles() should produce the same steps as build_steps().""" + from ace import ACE + + kwargs = dict( + agent=MockAgent(), + reflector=MockReflector(), + skill_manager=MockSkillManager(), + ) + runner = ACE.from_roles(**kwargs) + steps = ACE.build_steps(**kwargs) + + # Same number of steps + assert len(runner.pipeline._steps) == len(steps) + # Same step types + for pipe_step, built_step in zip(runner.pipeline._steps, steps): + assert type(pipe_step) is type(built_step) + + def test_build_steps_with_extra_steps(self): + from ace import ACE + + class DummyStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx): + return ctx + + steps = ACE.build_steps( + agent=MockAgent(), + reflector=MockReflector(), + skill_manager=MockSkillManager(), + extra_steps=[DummyStep()], + ) + assert isinstance(steps[-1], DummyStep) + + def test_pipeline_from_build_steps(self): + """Pipeline constructed from build_steps() should be valid.""" + from ace import ACE, Pipeline + + steps = ACE.build_steps( + agent=MockAgent(), + reflector=MockReflector(), + skill_manager=MockSkillManager(), + ) + pipe = Pipeline(steps) + assert pipe is not None + assert len(pipe._steps) == len(steps) diff --git a/tests/test_pipeline_hooks.py b/tests/test_pipeline_hooks.py new file mode 100644 index 0000000000000000000000000000000000000000..8288d68c652710784544cf5d1fb1e1275e7db9fd --- /dev/null +++ b/tests/test_pipeline_hooks.py @@ -0,0 +1,399 @@ +"""Tests for PipelineHook, CancellationToken, and cancel_token_var.""" + +from __future__ import annotations + +import threading +import time + +import pytest + +from pipeline import ( + CancellationToken, + Pipeline, + PipelineCancelled, + PipelineHook, + SampleResult, + StepContext, + cancel_token_var, +) + +# ------------------------------------------------------------------ +# Helpers +# ------------------------------------------------------------------ + + +class PassthroughStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + return ctx + + +class SlowStep: + """Step that sleeps briefly — useful for cancel-during-run tests.""" + + requires = frozenset() + provides = frozenset() + + def __init__(self, delay: float = 0.1): + self._delay = delay + + def __call__(self, ctx: StepContext) -> StepContext: + time.sleep(self._delay) + return ctx + + +class FailingStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + raise RuntimeError("boom") + + +class RecordingHook: + """Collects (event, step_name) tuples for assertion.""" + + def __init__(self) -> None: + self.events: list[tuple[str, str]] = [] + + def before_step(self, step_name: str, ctx: StepContext) -> None: + self.events.append(("before", step_name)) + + def after_step(self, step_name: str, ctx: StepContext) -> None: + self.events.append(("after", step_name)) + + +class BrokenHook: + """Hook that raises on every call.""" + + def before_step(self, step_name: str, ctx: StepContext) -> None: + raise ValueError("hook broken before") + + def after_step(self, step_name: str, ctx: StepContext) -> None: + raise ValueError("hook broken after") + + +# ================================================================== +# PipelineHook tests +# ================================================================== + + +class TestPipelineHooks: + def test_hooks_fire_before_and_after_each_step(self): + hook = RecordingHook() + pipe = Pipeline([PassthroughStep(), PassthroughStep()], hooks=[hook]) + pipe.run([StepContext(sample=1)]) + + assert hook.events == [ + ("before", "PassthroughStep"), + ("after", "PassthroughStep"), + ("before", "PassthroughStep"), + ("after", "PassthroughStep"), + ] + + def test_hooks_fire_per_sample(self): + hook = RecordingHook() + pipe = Pipeline([PassthroughStep()], hooks=[hook]) + pipe.run([StepContext(sample=1), StepContext(sample=2)]) + + assert len(hook.events) == 4 # 2 samples × (before + after) + + def test_multiple_hooks(self): + hook1 = RecordingHook() + hook2 = RecordingHook() + pipe = Pipeline([PassthroughStep()], hooks=[hook1, hook2]) + pipe.run([StepContext(sample=1)]) + + assert hook1.events == [ + ("before", "PassthroughStep"), + ("after", "PassthroughStep"), + ] + assert hook2.events == [ + ("before", "PassthroughStep"), + ("after", "PassthroughStep"), + ] + + def test_broken_hook_does_not_kill_pipeline(self): + broken = BrokenHook() + recorder = RecordingHook() + pipe = Pipeline([PassthroughStep()], hooks=[broken, recorder]) + results = pipe.run([StepContext(sample=1)]) + + # Pipeline still succeeds + assert len(results) == 1 + assert results[0].error is None + assert results[0].output is not None + # Second hook still fired + assert len(recorder.events) == 2 + + def test_hooks_receive_correct_step_name(self): + hook = RecordingHook() + pipe = Pipeline([SlowStep(delay=0)], hooks=[hook]) + pipe.run([StepContext(sample=1)]) + + assert hook.events[0] == ("before", "SlowStep") + assert hook.events[1] == ("after", "SlowStep") + + def test_after_hook_not_called_on_step_error(self): + hook = RecordingHook() + pipe = Pipeline([FailingStep()], hooks=[hook]) + results = pipe.run([StepContext(sample=1)]) + + # before fires, step raises, after does NOT fire for that step + assert hook.events == [("before", "FailingStep")] + assert isinstance(results[0].error, RuntimeError) + + def test_no_hooks_is_backward_compatible(self): + pipe = Pipeline([PassthroughStep()]) + results = pipe.run([StepContext(sample=1)]) + + assert len(results) == 1 + assert results[0].error is None + + def test_hook_satisfies_protocol(self): + hook = RecordingHook() + assert isinstance(hook, PipelineHook) + + +# ================================================================== +# CancellationToken tests +# ================================================================== + + +class TestCancellationToken: + def test_not_cancelled_initially(self): + token = CancellationToken() + assert not token.is_cancelled + + def test_cancel_sets_flag(self): + token = CancellationToken() + token.cancel() + assert token.is_cancelled + + def test_cancel_is_idempotent(self): + token = CancellationToken() + token.cancel() + token.cancel() + assert token.is_cancelled + + def test_cancel_is_thread_safe(self): + token = CancellationToken() + + def cancel_from_thread(): + time.sleep(0.01) + token.cancel() + + t = threading.Thread(target=cancel_from_thread) + t.start() + t.join() + assert token.is_cancelled + + +# ================================================================== +# Pipeline cancellation tests +# ================================================================== + + +class TestPipelineCancellation: + def test_pre_cancelled_token_cancels_immediately(self): + token = CancellationToken() + token.cancel() + + pipe = Pipeline([PassthroughStep(), PassthroughStep()]) + results = pipe.run([StepContext(sample=1)], cancel_token=token) + + assert len(results) == 1 + assert isinstance(results[0].error, PipelineCancelled) + assert results[0].failed_at == "PassthroughStep" + assert results[0].output is None + + def test_cancel_between_steps(self): + """Cancel after the first step; second step should not run.""" + call_count = 0 + + class CountingStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + nonlocal call_count + call_count += 1 + return ctx + + token = CancellationToken() + + class CancelAfterFirstHook: + def before_step(self, step_name, ctx): + pass + + def after_step(self, step_name, ctx): + # Cancel after the first step completes + token.cancel() + + pipe = Pipeline( + [CountingStep(), CountingStep()], + hooks=[CancelAfterFirstHook()], + ) + results = pipe.run([StepContext(sample=1)], cancel_token=token) + + assert call_count == 1 # Only first step ran + assert isinstance(results[0].error, PipelineCancelled) + + def test_cancel_stops_remaining_samples(self): + token = CancellationToken() + samples_started = [] + + class TrackingStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + samples_started.append(ctx.sample) + if ctx.sample == 0: + token.cancel() + return ctx + + pipe = Pipeline([TrackingStep()]) + results = pipe.run( + [StepContext(sample=i) for i in range(5)], + cancel_token=token, + ) + + # First sample ran (triggered cancel), rest should be cancelled + assert 0 in samples_started + cancelled = [r for r in results if isinstance(r.error, PipelineCancelled)] + assert len(cancelled) >= 1 # At least some were cancelled + + def test_no_token_runs_normally(self): + pipe = Pipeline([PassthroughStep()]) + results = pipe.run([StepContext(sample=1)], cancel_token=None) + + assert len(results) == 1 + assert results[0].error is None + + def test_on_sample_done_fires_on_cancellation(self): + token = CancellationToken() + token.cancel() + cb_results = [] + + pipe = Pipeline([PassthroughStep()]) + pipe.run( + [StepContext(sample=1)], + cancel_token=token, + on_sample_done=lambda r: cb_results.append(r), + ) + + assert len(cb_results) == 1 + assert isinstance(cb_results[0].error, PipelineCancelled) + + def test_cancelled_result_has_correct_shape(self): + token = CancellationToken() + token.cancel() + + pipe = Pipeline([PassthroughStep()]) + results = pipe.run([StepContext(sample="x")], cancel_token=token) + + r = results[0] + assert r.sample == "x" + assert r.output is None + assert isinstance(r.error, PipelineCancelled) + assert r.failed_at is not None + + +# ================================================================== +# cancel_token_var contextvar tests +# ================================================================== + + +class TestCancelTokenVar: + def test_contextvar_is_none_by_default(self): + assert cancel_token_var.get(None) is None + + def test_contextvar_set_during_pipeline_run(self): + """Steps can read the cancel_token_var set by the pipeline.""" + observed_tokens = [] + + class TokenReadingStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + observed_tokens.append(cancel_token_var.get(None)) + return ctx + + token = CancellationToken() + pipe = Pipeline([TokenReadingStep()]) + pipe.run([StepContext(sample=1)], cancel_token=token) + + assert len(observed_tokens) == 1 + assert observed_tokens[0] is token + + def test_contextvar_is_none_without_cancel_token(self): + """When no cancel_token is passed, the contextvar is None inside steps.""" + observed_tokens = [] + + class TokenReadingStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + observed_tokens.append(cancel_token_var.get(None)) + return ctx + + pipe = Pipeline([TokenReadingStep()]) + pipe.run([StepContext(sample=1)]) + + assert len(observed_tokens) == 1 + assert observed_tokens[0] is None + + def test_contextvar_reset_after_run(self): + """The contextvar is reset after run() completes.""" + token = CancellationToken() + pipe = Pipeline([PassthroughStep()]) + pipe.run([StepContext(sample=1)], cancel_token=token) + + # After run, the contextvar should be back to default + assert cancel_token_var.get(None) is None + + def test_contextvar_visible_across_multiple_steps(self): + """All steps in the same pipeline run see the same token.""" + observed_tokens = [] + + class TokenReadingStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + observed_tokens.append(cancel_token_var.get(None)) + return ctx + + token = CancellationToken() + pipe = Pipeline([TokenReadingStep(), TokenReadingStep(), TokenReadingStep()]) + pipe.run([StepContext(sample=1)], cancel_token=token) + + assert len(observed_tokens) == 3 + assert all(t is token for t in observed_tokens) + + def test_contextvar_per_sample(self): + """Each sample in the same run sees the same token.""" + observed_tokens = [] + + class TokenReadingStep: + requires = frozenset() + provides = frozenset() + + def __call__(self, ctx: StepContext) -> StepContext: + observed_tokens.append(cancel_token_var.get(None)) + return ctx + + token = CancellationToken() + pipe = Pipeline([TokenReadingStep()]) + pipe.run( + [StepContext(sample=i) for i in range(3)], + cancel_token=token, + ) + + assert len(observed_tokens) == 3 + assert all(t is token for t in observed_tokens) diff --git a/tests/test_pydantic_ai_integration.py b/tests/test_pydantic_ai_integration.py new file mode 100644 index 0000000000000000000000000000000000000000..11cdd779d62a97c1ccae14aaa3e63b62886cea94 --- /dev/null +++ b/tests/test_pydantic_ai_integration.py @@ -0,0 +1,510 @@ +"""Integration tests for PydanticAI-backed ACE roles with real API calls. + +Requires AWS credentials for Bedrock access. +Run with: uv run pytest tests/test_pydantic_ai_integration.py -v -s --no-cov +""" + +from __future__ import annotations + +import os + +import pytest +from dotenv import load_dotenv + +load_dotenv() + +# Skip entire module if no API credentials +pytestmark = pytest.mark.requires_api + +HAS_API = bool(os.environ.get("OPENAI_API_KEY")) +if not HAS_API: + pytest.skip("OPENAI_API_KEY not set", allow_module_level=True) + +from ace.core.outputs import ( + AgentOutput, + ReflectorOutput, + SkillManagerOutput, +) +from ace.core.skillbook import Skillbook, UpdateBatch +from ace.implementations import Agent, Reflector, SkillManager +from ace.runners.litellm import ACELiteLLM +from ace.core.environments import Sample, SimpleEnvironment + +MODEL = "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0" + + +class TestAgentRole: + """Test Agent role produces valid structured output.""" + + def test_basic_question(self): + agent = Agent(MODEL) + sb = Skillbook() + output = agent.generate( + question="What is the capital of France?", + context="Answer in one word.", + skillbook=sb, + ) + + assert isinstance(output, AgentOutput) + assert len(output.reasoning) > 0, "reasoning should be non-empty" + assert len(output.final_answer) > 0, "final_answer should be non-empty" + assert ( + "paris" in output.final_answer.lower() + ), f"Expected 'Paris' in answer, got: {output.final_answer}" + assert isinstance(output.skill_ids, list) + assert "usage" in output.raw, f"raw should contain usage, got: {output.raw}" + assert output.raw["usage"]["prompt_tokens"] > 0 + assert output.raw["usage"]["completion_tokens"] > 0 + print(f"\n Agent answer: {output.final_answer}") + print(f" Usage: {output.raw['usage']}") + + def test_with_skillbook(self): + agent = Agent(MODEL) + sb = Skillbook() + sb.add_skill( + "math", + "Use decomposition: break large multiplications into (a*10 + b) parts", + skill_id="math-001", + ) + + output = agent.generate( + question="What is 17 × 23?", + context="Show your work step by step.", + skillbook=sb, + ) + + assert isinstance(output, AgentOutput) + assert len(output.final_answer) > 0 + assert ( + "391" in output.final_answer + ), f"Expected '391' in answer, got: {output.final_answer}" + print(f"\n Agent answer: {output.final_answer}") + print(f" Reasoning excerpt: {output.reasoning[:300]}...") + print(f" Cited skills: {output.skill_ids}") + + def test_with_reflection(self): + agent = Agent(MODEL) + sb = Skillbook() + sb.add_skill( + "physics", + "Always include the unit when stating temperatures", + skill_id="phys-001", + ) + + output = agent.generate( + question="What temperature does water boil at in Fahrenheit? Reply with just the number and unit.", + context="This is a factual science question. Answer concisely.", + skillbook=sb, + reflection="Your previous answer was incorrect. The correct answer is 212°F at standard atmospheric pressure.", + ) + + assert isinstance(output, AgentOutput) + assert len(output.final_answer) > 0 + # The reflection explicitly states 212°F — verify the model uses it + full_text = f"{output.final_answer} {output.reasoning}" + assert ( + "212" in full_text + ), f"Expected '212' somewhere in output, got answer: {output.final_answer}" + print(f"\n Agent answer with reflection: {output.final_answer}") + + +class TestReflectorRole: + """Test Reflector role produces valid structured analysis.""" + + def test_correct_answer_reflection(self): + reflector = Reflector(MODEL) + sb = Skillbook() + sb.add_skill("math", "Break down multiplication", skill_id="math-001") + + agent_output = AgentOutput( + reasoning="Following [math-001], I decomposed 15×24 as 15×20 + 15×4 = 300 + 60 = 360", + final_answer="360", + skill_ids=["math-001"], + ) + + output = reflector.reflect( + question="What is 15 × 24?", + agent_output=agent_output, + skillbook=sb, + ground_truth="360", + feedback="Correct!", + ) + + assert isinstance(output, ReflectorOutput) + assert len(output.reasoning) > 0 + assert len(output.correct_approach) > 0 + assert len(output.key_insight) > 0 + assert "usage" in output.raw + print(f"\n Key insight: {output.key_insight}") + + def test_wrong_answer_reflection(self): + reflector = Reflector(MODEL) + sb = Skillbook() + + agent_output = AgentOutput( + reasoning="I calculated 15×24 = 15×20 + 15×4 = 310 + 60 = 370", + final_answer="370", + ) + + output = reflector.reflect( + question="What is 15 × 24?", + agent_output=agent_output, + skillbook=sb, + ground_truth="360", + feedback="Incorrect. The answer is 360.", + ) + + assert isinstance(output, ReflectorOutput) + assert len(output.error_identification) > 0, "Should identify the error" + assert len(output.root_cause_analysis) > 0, "Should analyze root cause" + print(f"\n Error identified: {output.error_identification[:200]}") + print(f" Root cause: {output.root_cause_analysis[:200]}") + print(f" Key insight: {output.key_insight[:200]}") + + +class TestSkillManagerRole: + """Test SkillManager role produces valid skillbook updates.""" + + def test_add_new_skill(self): + sm = SkillManager(MODEL) + sb = Skillbook() + + reflection = ReflectorOutput( + reasoning="The agent failed because it didn't decompose the problem", + error_identification="Tried to multiply directly without decomposition", + root_cause_analysis="Missing strategy for breaking down multiplication", + correct_approach="Use decomposition: 15×24 = 15×(20+4) = 300+60 = 360", + key_insight="Break large multiplications into manageable parts", + ) + + output = sm.update_skills( + reflections=(reflection,), + skillbook=sb, + question_context="Mental arithmetic", + progress="0/1 correct", + ) + + assert isinstance(output, SkillManagerOutput) + assert isinstance(output.update, UpdateBatch) + assert len(output.update.reasoning) > 0 + assert "usage" in output.raw + print(f"\n Reasoning: {output.update.reasoning[:200]}") + print(f" Operations: {len(output.update.operations)}") + for op in output.update.operations: + print( + f" {op.type}: {(op.insight or op.issue)[:80] if (op.insight or op.issue) else 'N/A'}" + ) + + def test_tag_existing_skill(self): + sm = SkillManager(MODEL) + sb = Skillbook() + sb.add_skill( + "math", + "Use decomposition for multiplication", + skill_id="math-001", + ) + + reflection = ReflectorOutput( + reasoning="The agent correctly applied decomposition strategy", + correct_approach="Decomposition worked well", + key_insight="Decomposition strategy is effective", + ) + + output = sm.update_skills( + reflections=(reflection,), + skillbook=sb, + question_context="Mental arithmetic", + progress="1/1 correct", + ) + + assert isinstance(output, SkillManagerOutput) + print(f"\n Operations: {len(output.update.operations)}") + for op in output.update.operations: + print( + f" {op.type} {op.skill_id or ''}: " + f"{(op.insight or op.issue) or op.metadata}" + ) + + +class TestACELiteLLMIntegration: + """Test the full ACELiteLLM flow with real API calls.""" + + def test_ask(self): + ace = ACELiteLLM.from_model(MODEL) + answer = ace.ask("What is 2 + 2?") + assert "4" in answer, f"Expected '4' in answer, got: {answer}" + print(f"\n ask() answer: {answer}") + + def test_ask_and_learn_from_feedback(self): + ace = ACELiteLLM.from_model(MODEL) + + answer = ace.ask("What is the chemical symbol for gold?") + print(f"\n Answer: {answer}") + assert len(answer) > 0 + + result = ace.learn_from_feedback( + feedback="Correct! Gold's symbol Au comes from the Latin 'aurum'.", + ground_truth="Au", + ) + assert result is True, "learn_from_feedback should return True" + print(f" Skills after learning: {len(ace.skillbook.skills())}") + for skill in ace.skillbook.skills(): + print(f" [{skill.id}] {skill.content[:80]}") + + def test_full_learning_pipeline(self): + """End-to-end: learn from samples, verify skillbook grows.""" + ace = ACELiteLLM.from_model(MODEL) + env = SimpleEnvironment() + + samples = [ + Sample( + question="What is the speed of light in km/s?", + ground_truth="approximately 300,000 km/s", + ), + ] + + results = ace.learn(samples, environment=env) + assert len(results) == 1 + assert results[0].error is None, f"Pipeline error: {results[0].error}" + print(f"\n Pipeline completed. Skills: {len(ace.skillbook.skills())}") + for skill in ace.skillbook.skills(): + print(f" [{skill.id}] {skill.content[:80]}") + + def test_save_and_load_after_learning(self, tmp_path): + """Skills survive save/load cycle.""" + ace = ACELiteLLM.from_model(MODEL) + answer = ace.ask("What is H2O?") + ace.learn_from_feedback("Correct!", ground_truth="Water") + + path = str(tmp_path / "skillbook.json") + skills_before = len(ace.skillbook.skills()) + ace.save(path) + + ace2 = ACELiteLLM.from_model(MODEL, skillbook_path=path) + assert len(ace2.skillbook.skills()) == skills_before + print(f"\n Saved and loaded {skills_before} skills successfully") + + +class TestRetryAndConsistency: + """Test structured output consistency across multiple calls.""" + + def test_structured_output_consistency(self): + """Multiple calls should always produce valid structured output.""" + agent = Agent(MODEL) + sb = Skillbook() + + questions = [ + ("What is 7 × 8?", "56"), + ("What is the capital of Japan?", "Tokyo"), + ("Who wrote Romeo and Juliet?", "Shakespeare"), + ] + + for q, expected in questions: + output = agent.generate( + question=q, + context="Answer concisely.", + skillbook=sb, + ) + assert isinstance(output, AgentOutput), f"Wrong type for '{q}'" + assert len(output.reasoning) > 0, f"Empty reasoning for '{q}'" + assert len(output.final_answer) > 0, f"Empty answer for '{q}'" + assert isinstance(output.raw, dict), f"raw not dict for '{q}'" + assert "usage" in output.raw, f"No usage in raw for '{q}'" + assert ( + expected.lower() in output.final_answer.lower() + ), f"Expected '{expected}' in answer for '{q}', got: {output.final_answer}" + print(f"\n Q: {q} -> A: {output.final_answer}") + + +class TestRRStepIntegration: + """Test the PydanticAI-based Recursive Reflector (RRStep) with real API calls.""" + + RR_MODEL = "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0" + + @pytest.mark.integration + def test_rr_basic_reflection(self): + """RRStep.reflect returns a valid ReflectorOutput with non-empty fields.""" + from ace.steps.rr_step import RRStep + from ace.implementations.rr.config import RecursiveConfig + + config = RecursiveConfig( + max_requests=15, + timeout=15.0, + ) + rr = RRStep(model=self.RR_MODEL, config=config) + sb = Skillbook() + + agent_out = AgentOutput( + reasoning="I recall that the capital of Australia is Sydney because it is the largest city.", + final_answer="Sydney", + skill_ids=[], + ) + + output = rr.reflect( + question="What is the capital of Australia?", + agent_output=agent_out, + skillbook=sb, + ground_truth="Canberra", + feedback="Incorrect. The capital of Australia is Canberra, not Sydney.", + ) + + assert isinstance( + output, ReflectorOutput + ), f"Expected ReflectorOutput, got {type(output)}" + assert len(output.reasoning) > 0, "reasoning should be non-empty" + assert len(output.key_insight) > 0, "key_insight should be non-empty" + assert isinstance(output.raw, dict), "raw should be a dict" + + # Verify rr_trace metadata is populated + rr_trace = output.raw.get("rr_trace", {}) + assert isinstance(rr_trace, dict), "rr_trace should be a dict in raw" + assert "total_iterations" in rr_trace, "rr_trace should have total_iterations" + + print(f"\n Reasoning: {output.reasoning[:300]}") + print(f" Key insight: {output.key_insight[:200]}") + print(f" RR trace: {rr_trace}") + + @pytest.mark.integration + def test_rr_with_skillbook(self): + """RRStep.reflect with a populated skillbook references or tags skills.""" + from ace.steps.rr_step import RRStep + from ace.implementations.rr.config import RecursiveConfig + + config = RecursiveConfig( + max_requests=25, + timeout=15.0, + ) + rr = RRStep(model=self.RR_MODEL, config=config) + sb = Skillbook() + sb.add_skill( + "geography", + "Always verify capital cities — the largest city is often not the capital", + skill_id="geo-001", + ) + + agent_out = AgentOutput( + reasoning="The largest city in Brazil is Sao Paulo, so it must be the capital.", + final_answer="Sao Paulo", + skill_ids=[], + ) + + output = rr.reflect( + question="What is the capital of Brazil?", + agent_output=agent_out, + skillbook=sb, + ground_truth="Brasilia", + feedback="Incorrect. The capital of Brazil is Brasilia.", + ) + + assert isinstance(output, ReflectorOutput) + assert len(output.reasoning) > 0 + assert len(output.key_insight) > 0 + + # The RR should produce a meaningful analysis referencing the + # capital city error. + full_text = f"{output.reasoning} {output.key_insight}" + has_analysis = ( + "capital" in full_text.lower() + or "largest" in full_text.lower() + or "brasilia" in full_text.lower() + ) + assert has_analysis, ( + "Expected the reflector to analyze the capital city error. " + f"reasoning={output.reasoning[:200]}" + ) + + print(f"\n Key insight: {output.key_insight[:200]}") + + @pytest.mark.integration + def test_rr_step_protocol(self): + """RRStep used as a StepProtocol: __call__(ctx) populates reflections.""" + from ace.steps.rr_step import RRStep + from ace.implementations.rr.config import RecursiveConfig + from ace.core.context import ACEStepContext, SkillbookView + + config = RecursiveConfig( + max_requests=15, + timeout=15.0, + ) + rr = RRStep(model=self.RR_MODEL, config=config) + sb = Skillbook() + + trace = { + "question": "What is 15 x 24?", + "ground_truth": "360", + "feedback": "Incorrect. The correct answer is 360.", + "steps": [ + { + "role": "agent", + "reasoning": "15 x 24 = 15 x 20 + 15 x 4 = 310 + 60 = 370", + "answer": "370", + "skill_ids": [], + }, + ], + } + + ctx = ACEStepContext( + trace=trace, + skillbook=SkillbookView(sb), + ) + + result_ctx = rr(ctx) + + assert result_ctx.reflections is not None, "reflections should be set" + assert len(result_ctx.reflections) > 0, "reflections should be non-empty" + for reflection in result_ctx.reflections: + assert isinstance(reflection, ReflectorOutput) + assert len(reflection.reasoning) > 0 + + print(f"\n Reflections count: {len(result_ctx.reflections)}") + print(f" First reasoning: {result_ctx.reflections[0].reasoning[:300]}") + print(f" First key_insight: {result_ctx.reflections[0].key_insight[:200]}") + + @pytest.mark.integration + def test_rr_execute_code_tool_used(self): + """Verify the agent uses execute_code (total_iterations > 0 in rr_trace).""" + from ace.steps.rr_step import RRStep + from ace.implementations.rr.config import RecursiveConfig + + config = RecursiveConfig( + max_requests=15, + timeout=15.0, + ) + rr = RRStep(model=self.RR_MODEL, config=config) + sb = Skillbook() + + agent_out = AgentOutput( + reasoning=( + "I need to find the square root of 144. " + "I think it might be 14 since 14 x 14 is close to 144." + ), + final_answer="14", + skill_ids=[], + ) + + output = rr.reflect( + question="What is the square root of 144?", + agent_output=agent_out, + skillbook=sb, + ground_truth="12", + feedback="Incorrect. The square root of 144 is 12, not 14.", + ) + + assert isinstance(output, ReflectorOutput) + + rr_trace = output.raw.get("rr_trace", {}) + total_iterations = rr_trace.get("total_iterations", 0) + assert total_iterations > 0, ( + f"Expected execute_code to be called at least once " + f"(total_iterations > 0), got {total_iterations}. " + f"rr_trace={rr_trace}" + ) + + print(f"\n Total iterations (execute_code calls): {total_iterations}") + print(f" Timed out: {rr_trace.get('timed_out', 'N/A')}") + print(f" Key insight: {output.key_insight[:200]}") + print(f" Reasoning: {output.reasoning[:300]}") + + +if __name__ == "__main__": + pytest.main([__file__, "-v", "-s", "--no-cov"]) diff --git a/tests/test_rr_pipeline/__init__.py b/tests/test_rr_pipeline/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_rr_pipeline/test_rr_stress.py b/tests/test_rr_pipeline/test_rr_stress.py new file mode 100644 index 0000000000000000000000000000000000000000..2c1f127fc32cf8fc3df22bc431317ce5013b7194 --- /dev/null +++ b/tests/test_rr_pipeline/test_rr_stress.py @@ -0,0 +1,520 @@ +"""Stress tests for RR components. + +Tests sandbox behavior and the PydanticAI-based RRStep entry points. +""" + +import copy +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from pydantic_ai.messages import ( + ModelRequest, + ModelResponse, + TextPart, + ToolReturnPart, + UserPromptPart, +) +from pydantic_ai.usage import UsageLimits + +from ace.implementations.rr.config import RecursiveConfig +from ace.core.sandbox import TraceSandbox, create_readonly_sandbox + +from ace.core.context import ACEStepContext, SkillbookView +from ace.core.outputs import AgentOutput, ReflectorOutput +from ace.core.skillbook import Skillbook +from ace.steps.rr_step import RRConfig, RRStep +from ace.implementations.rr.tools import RRDeps + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_ctx( + question: str = "q", + answer: str = "4", + reasoning: str = "r", + ground_truth: str | None = None, + feedback: str | None = None, +) -> ACEStepContext: + """Build an ACEStepContext suitable for RRStep.__call__.""" + trace: dict = { + "question": question, + "steps": [ + {"role": "agent", "reasoning": reasoning, "answer": answer, "skill_ids": []} + ], + } + if ground_truth is not None: + trace["ground_truth"] = ground_truth + if feedback is not None: + trace["feedback"] = feedback + return ACEStepContext(trace=trace, skillbook=SkillbookView(Skillbook())) + + +_RUN_SYNC = "ace.core.recursive_agent.run_agent_sync" + + +def _mock_compaction_result( + *, + reasoning: str = "done", + key_insight: str = "insight", + correct_approach: str = "approach", + timed_out: bool = False, +) -> tuple[ReflectorOutput, dict]: + """Create a mock return value for run_agent_sync.""" + output = ReflectorOutput( + reasoning=reasoning, + error_identification="none", + root_cause_analysis="mock root cause", + correct_approach=correct_approach, + key_insight=key_insight, + raw={}, + ) + metadata = { + "usage": { + "input_tokens": 100, + "output_tokens": 50, + "total_tokens": 150, + "requests": 3, + }, + "compactions": 0, + "depth": 0, + "iterations": 2, + "timed_out": timed_out, + } + + return output, metadata + + +# ========================================================================= +# 1. RRStep lifecycle (PydanticAI-based) +# ========================================================================= + + +@pytest.mark.unit +class TestLoopLifecycle: + def test_successful_reflection(self): + """Happy path: PydanticAI agent produces valid ReflectorOutput.""" + rr = RRStep("test-model", config=RRConfig()) + reflection, metadata = _mock_compaction_result(key_insight="insight") + + with patch(_RUN_SYNC, return_value=(reflection, metadata)): + result_ctx = rr( + _make_ctx( + question="What is 2+2?", + ground_truth="4", + feedback="Correct!", + ) + ) + + result = result_ctx.reflections[0] + assert isinstance(result, ReflectorOutput) + assert result.key_insight == "insight" + + def test_max_requests_timeout(self): + """Budget exhaustion produces timeout output.""" + rr = RRStep( + "test-model", + config=RRConfig(max_requests=3), + ) + + reflection, metadata = _mock_compaction_result( + reasoning="Analysis reached budget limit.", + timed_out=True, + ) + + with patch(_RUN_SYNC, return_value=(reflection, metadata)): + result_ctx = rr(_make_ctx()) + + assert len(result_ctx.reflections) == 1 + assert isinstance(result_ctx.reflections[0], ReflectorOutput) + assert "budget limit" in result_ctx.reflections[0].reasoning.lower() + + def test_budget_field_in_config(self): + """max_tokens and max_requests config fields exist.""" + rr = RRStep( + "test-model", + config=RRConfig(max_tokens=100_000, max_requests=42), + ) + assert rr.config.max_tokens == 100_000 + assert rr.config.max_requests == 42 + + def test_config_build_usage_limits(self): + """build_usage_limits() produces correct UsageLimits.""" + cfg = RecursiveConfig( + max_tokens=500_000, max_requests=50, context_window=128_000 + ) + limits = cfg.build_usage_limits() + assert limits.total_tokens_limit == 500_000 + assert limits.request_limit == 50 + + def test_config_build_usage_limits_with_remaining(self): + """build_usage_limits() uses remaining_tokens when provided.""" + cfg = RecursiveConfig(max_tokens=500_000, max_requests=50) + limits = cfg.build_usage_limits(remaining_tokens=100_000) + assert limits.total_tokens_limit == 100_000 + assert limits.request_limit == 50 + + def test_rr_trace_metadata_on_success(self): + """Successful reflection populates rr_trace metadata.""" + rr = RRStep("test-model", config=RRConfig()) + reflection, metadata = _mock_compaction_result() + + with patch(_RUN_SYNC, return_value=(reflection, metadata)): + result_ctx = rr(_make_ctx()) + + result = result_ctx.reflections[0] + assert "rr_trace" in result.raw + assert result.raw["rr_trace"]["timed_out"] is False + assert isinstance(result.raw["rr_trace"]["subagent_calls"], list) + + def test_rr_trace_metadata_on_timeout(self): + """Budget exhaustion produces rr_trace with timed_out=True.""" + from ace.core.recursive_agent import BudgetExhausted + + rr = RRStep("test-model", config=RRConfig()) + + with patch(_RUN_SYNC, side_effect=BudgetExhausted(compaction_count=1)): + result_ctx = rr(_make_ctx()) + + result = result_ctx.reflections[0] + assert "rr_trace" in result.raw + assert result.raw["rr_trace"]["timed_out"] is True + + +# ========================================================================= +# 2. Sandbox behavior +# ========================================================================= + + +@pytest.mark.unit +class TestSandboxBehavior: + def test_sandbox_variables_persist_across_iterations(self): + """Variables set in one execution persist for the next.""" + sandbox = TraceSandbox(trace=None) + sandbox.execute("x = 42", timeout=5.0) + result = sandbox.execute("print(x + 1)", timeout=5.0) + assert "43" in result.stdout + + def test_sandbox_code_modifies_injected_traces(self): + """Mutation of injected dict is visible in later executions.""" + sandbox = TraceSandbox(trace=None) + traces = {"question": "q", "items": [1, 2, 3]} + sandbox.inject("traces", traces) + sandbox.execute("traces['items'].append(4)", timeout=5.0) + result = sandbox.execute("print(len(traces['items']))", timeout=5.0) + assert "4" in result.stdout + + def test_sandbox_exception_produces_stderr(self): + """Code that raises captures error in stderr.""" + sandbox = TraceSandbox(trace=None) + result = sandbox.execute("raise RuntimeError('boom')", timeout=5.0) + assert not result.success + assert "RuntimeError" in result.stderr + assert "boom" in result.stderr + + def test_registered_helpers_persist_and_run(self): + """Registered helpers should persist across execute_code calls.""" + sandbox = TraceSandbox(trace=None) + sandbox.inject("traces", {"values": [1, 2, 3]}) + result = sandbox.execute( + """ +register_helper( + "sum_values", + "def sum_values():\\n return sum(traces['values'])\\n", + "Return the sum of traces['values']", +) +print(run_helper("sum_values")) + """.strip(), + timeout=5.0, + ) + + assert result.success + assert "6" in result.stdout + assert sandbox.namespace["list_helpers"]()[0]["name"] == "sum_values" + + def test_registered_helpers_are_rehydrated_in_snapshots(self): + """Sub-agent snapshots should recreate helpers against the child namespace.""" + parent = TraceSandbox(trace=None) + parent.inject("traces", {"values": [1, 2, 3]}) + parent.execute( + """ +register_helper( + "sum_values", + "def sum_values():\\n return sum(traces['values'])\\n", + "Return the sum of traces['values']", +) + """.strip(), + timeout=5.0, + ) + + child = create_readonly_sandbox(parent) + child.namespace["traces"]["values"].append(4) + result = child.execute('print(run_helper("sum_values"))', timeout=5.0) + + assert result.success + assert "10" in result.stdout + assert parent.namespace["traces"]["values"] == [1, 2, 3] + + def test_batch_accessors_handle_nested_task_payloads(self): + """Batch helpers should expose a stable view over nested task items.""" + sandbox = TraceSandbox(trace=None) + batch_items = [ + { + "task_id": "task_0", + "question": "Where is my order?", + "feedback": "Task FAILED (reward=0.0)", + "trace": { + "messages": [ + {"role": "assistant", "content": "Hi!"}, + {"role": "user", "content": "Where is my order?"}, + ] + }, + } + ] + sandbox.inject("batch_items", batch_items) + sandbox.inject("item_ids", ["task_0"]) + + result = sandbox.execute( + """ +item = get_batch_item(0) +print(get_item_id(0)) +print(get_item_question(item)) +print(get_item_feedback(item)) +messages = get_item_messages(item) +print(len(messages)) +print(get_message_text(messages[1])) +print(preview_item(0)["payload_type"]) + """.strip(), + timeout=5.0, + ) + + assert result.success + lines = result.stdout.strip().splitlines() + assert lines[0] == "task_0" + assert lines[1] == "Where is my order?" + assert lines[2] == "Task FAILED (reward=0.0)" + assert lines[3] == "2" + assert lines[4] == "Where is my order?" + assert lines[5] == "dict" + + +# ========================================================================= +# 3. Entry points (PydanticAI-based) +# ========================================================================= + + +@pytest.mark.unit +class TestEntryPoints: + def test_call_produces_reflection(self): + """__call__() produces a ReflectorOutput on the context.""" + rr = RRStep("test-model", config=RRConfig()) + reflection, metadata = _mock_compaction_result(key_insight="insight") + + traces = { + "question": "q", + "steps": [ + {"role": "agent", "reasoning": "r", "answer": "4", "skill_ids": []} + ], + } + ctx = ACEStepContext(trace=traces, skillbook=SkillbookView(Skillbook())) + + with patch(_RUN_SYNC, return_value=(reflection, metadata)): + result_ctx = rr(ctx) + + assert isinstance(result_ctx.reflections[0], ReflectorOutput) + assert result_ctx.reflections[0].key_insight == "insight" + + def test_reflect_method_works(self): + """reflect() works as ReflectorLike entry point.""" + rr = RRStep("test-model", config=RRConfig()) + reflection, metadata = _mock_compaction_result(key_insight="reflected") + + with patch(_RUN_SYNC, return_value=(reflection, metadata)): + result = rr.reflect( + question="What is 2+2?", + agent_output=AgentOutput(reasoning="r", final_answer="4"), + ground_truth="4", + ) + + assert isinstance(result, ReflectorOutput) + assert result.key_insight == "reflected" + + +# ========================================================================= +# 4. New architecture tests +# ========================================================================= + + +@pytest.mark.unit +class TestRecurseToolRegistration: + def test_recurse_tool_registered_at_non_leaf_depth(self): + """Agent at depth 0 with max_depth=2 should have recurse tool.""" + from ace.core.recursive_agent import AgenticConfig, RecursiveAgent + + ra = RecursiveAgent( + "test-model", + output_type=ReflectorOutput, + system_prompt="test", + config=AgenticConfig(max_depth=2), + ) + agent = ra._create_agent(depth=0) + tool_names = list(agent._function_toolset.tools.keys()) + assert "recurse" in tool_names + + def test_recurse_tool_not_registered_at_max_depth(self): + """Agent at max_depth should NOT have recurse tool.""" + from ace.core.recursive_agent import AgenticConfig, RecursiveAgent + + ra = RecursiveAgent( + "test-model", + output_type=ReflectorOutput, + system_prompt="test", + config=AgenticConfig(max_depth=2), + ) + agent = ra._create_agent(depth=2) + tool_names = list(agent._function_toolset.tools.keys()) + assert "recurse" not in tool_names + + def test_recurse_tool_not_registered_at_depth_zero_max_zero(self): + """Agent at depth=0, max_depth=0 should NOT have recurse tool.""" + from ace.core.recursive_agent import AgenticConfig, RecursiveAgent + + ra = RecursiveAgent( + "test-model", + output_type=ReflectorOutput, + system_prompt="test", + config=AgenticConfig(max_depth=0), + ) + agent = ra._create_agent(depth=0) + tool_names = list(agent._function_toolset.tools.keys()) + assert "recurse" not in tool_names + + +@pytest.mark.unit +class TestMicrocompaction: + def test_microcompact_clears_old_tool_results(self): + """_microcompact should clear old tool results, keeping recent ones.""" + rr = RRStep("test-model", config=RRConfig()) + + # Build a message list with 5 tool results + messages = [] + for i in range(5): + messages.append( + ModelRequest( + parts=[ + ToolReturnPart( + tool_name="execute_code", + content=f"result {i}", + tool_call_id=f"call_{i}", + ), + ] + ) + ) + + from ace.core.recursive_agent import microcompact + + compacted = microcompact(messages, keep_recent=2, tool_names=("execute_code",)) + + # Should NOT be the same object (changes were made) + assert compacted is not messages + + # First 3 should be cleared, last 2 kept + for i in range(3): + assert "[cleared" in compacted[i].parts[0].content + assert compacted[3].parts[0].content == "result 3" + assert compacted[4].parts[0].content == "result 4" + + def test_microcompact_returns_same_when_nothing_to_clear(self): + """_microcompact returns identity when keep_recent >= total tool results.""" + rr = RRStep("test-model", config=RRConfig()) + + messages = [ + ModelRequest( + parts=[ + ToolReturnPart( + tool_name="execute_code", + content="result 0", + tool_call_id="call_0", + ), + ] + ), + ] + + from ace.core.recursive_agent import microcompact + + result = microcompact(messages, keep_recent=3, tool_names=("execute_code",)) + assert result is messages # identity = no change + + def test_microcompact_ignores_non_tool_messages(self): + """_microcompact should not touch model response messages.""" + rr = RRStep("test-model", config=RRConfig()) + + messages = [ + ModelResponse(parts=[TextPart(content="thinking...")]), + ModelRequest( + parts=[ + ToolReturnPart( + tool_name="execute_code", + content="old result", + tool_call_id="call_0", + ), + ] + ), + ModelResponse(parts=[TextPart(content="more thinking...")]), + ModelRequest( + parts=[ + ToolReturnPart( + tool_name="execute_code", + content="new result", + tool_call_id="call_1", + ), + ] + ), + ] + + from ace.core.recursive_agent import microcompact + + compacted = microcompact(messages, keep_recent=1, tool_names=("execute_code",)) + assert compacted is not messages + # First tool result cleared, second kept + assert "[cleared" in compacted[1].parts[0].content + assert compacted[3].parts[0].content == "new result" + # Model responses untouched + assert compacted[0].parts[0].content == "thinking..." + assert compacted[2].parts[0].content == "more thinking..." + + +@pytest.mark.unit +class TestBudgetExhausted: + def test_is_budget_exhausted_tokens(self): + """is_budget_exhausted detects total token limit.""" + from ace.core.recursive_agent import is_budget_exhausted + + limits = UsageLimits(total_tokens_limit=1000, request_limit=50) + usage = MagicMock() + usage.total_tokens = 1000 + usage.requests = 5 + assert is_budget_exhausted(limits, usage) is True + + def test_is_budget_exhausted_requests(self): + """is_budget_exhausted detects request limit.""" + from ace.core.recursive_agent import is_budget_exhausted + + limits = UsageLimits(total_tokens_limit=100_000, request_limit=10) + usage = MagicMock() + usage.total_tokens = 500 + usage.requests = 10 + assert is_budget_exhausted(limits, usage) is True + + def test_is_budget_not_exhausted(self): + """is_budget_exhausted returns False when under budget.""" + from ace.core.recursive_agent import is_budget_exhausted + + limits = UsageLimits(total_tokens_limit=100_000, request_limit=50) + usage = MagicMock() + usage.total_tokens = 500 + usage.requests = 5 + assert is_budget_exhausted(limits, usage) is False diff --git a/tests/test_rr_pipeline/test_runner.py b/tests/test_rr_pipeline/test_runner.py new file mode 100644 index 0000000000000000000000000000000000000000..cedb39beeb0baa57ce3ea4a1471e4a2e92cb8a1f --- /dev/null +++ b/tests/test_rr_pipeline/test_runner.py @@ -0,0 +1,358 @@ +"""Tests for RRStep — PydanticAI-based Recursive Reflector.""" + +from __future__ import annotations + +from unittest.mock import patch + +import pytest +from pydantic_ai.settings import ModelSettings + +from ace.implementations.rr.config import RecursiveConfig +from ace.core.context import ACEStepContext, SkillbookView +from ace.core.outputs import AgentOutput, ReflectorOutput +from ace.core.skillbook import Skillbook + +from ace.steps.rr_step import RRStep, RRConfig +from ace.implementations.rr.tools import RRDeps + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_ctx( + question: str = "test", + answer: str = "a", + reasoning: str = "r", + ground_truth: str | None = None, + feedback: str | None = None, +) -> ACEStepContext: + """Build an ACEStepContext suitable for RRStep.__call__.""" + trace: dict = { + "question": question, + "steps": [ + {"role": "agent", "reasoning": reasoning, "answer": answer, "skill_ids": []} + ], + } + if ground_truth is not None: + trace["ground_truth"] = ground_truth + if feedback is not None: + trace["feedback"] = feedback + return ACEStepContext(trace=trace, skillbook=SkillbookView(Skillbook())) + + +_RUN_SYNC = "ace.core.recursive_agent.run_agent_sync" + + +def _mock_compaction_result( + *, + reasoning: str = "mock reasoning", + key_insight: str = "mock insight", + correct_approach: str = "mock approach", +) -> tuple[ReflectorOutput, dict]: + """Create a mock return value for run_agent_sync.""" + output = ReflectorOutput( + reasoning=reasoning, + error_identification="none", + root_cause_analysis="mock root cause", + correct_approach=correct_approach, + key_insight=key_insight, + raw={}, + ) + metadata = { + "usage": { + "input_tokens": 100, + "output_tokens": 50, + "total_tokens": 150, + "requests": 3, + }, + "compactions": 0, + "depth": 0, + "iterations": 2, + "timed_out": False, + } + return output, metadata + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestRRStep: + """Test RRStep construction and StepProtocol.""" + + def test_step_protocol_attributes(self): + rr = RRStep("test-model", config=RRConfig()) + assert "trace" in rr.requires + assert "skillbook" in rr.requires + assert "reflections" in rr.provides + assert "reflection" not in rr.provides + + def test_call_produces_reflection_on_context(self): + """RRStep.__call__ populates ctx.reflections.""" + rr = RRStep("test-model", config=RRConfig()) + + reflection, metadata = _mock_compaction_result(key_insight="step test") + + with patch(_RUN_SYNC, return_value=(reflection, metadata)): + ctx = _make_ctx( + question="What is 2+2?", + answer="4", + reasoning="2+2=4", + ground_truth="4", + feedback="Correct!", + ) + result_ctx = rr(ctx) + + assert len(result_ctx.reflections) == 1 + assert isinstance(result_ctx.reflections[0], ReflectorOutput) + assert result_ctx.reflections[0].key_insight == "step test" + + def test_rr_trace_metadata_populated(self): + """Successful reflection populates rr_trace in raw.""" + rr = RRStep("test-model", config=RRConfig()) + reflection, metadata = _mock_compaction_result() + + with patch(_RUN_SYNC, return_value=(reflection, metadata)): + result_ctx = rr(_make_ctx()) + + result = result_ctx.reflections[0] + assert "rr_trace" in result.raw + assert result.raw["rr_trace"]["timed_out"] is False + assert "usage" in result.raw + + def test_thoughts_are_exposed_in_raw(self): + """RRStep preserves think-tool notes recorded during evidence gathering.""" + rr = RRStep("test-model", config=RRConfig()) + reflection, metadata = _mock_compaction_result() + + def _run_with_thought(*args, **kwargs): + deps = kwargs["deps"] + deps.thoughts.append( + { + "thought": "The selected flights satisfy the requested dates.", + "evidence_refs": ["messages[5]", "messages[9]"], + } + ) + return reflection, metadata + + with patch(_RUN_SYNC, side_effect=_run_with_thought): + result_ctx = rr(_make_ctx()) + + thoughts = result_ctx.reflections[0].raw["thoughts"] + assert thoughts == [ + { + "thought": "The selected flights satisfy the requested dates.", + "evidence_refs": ["messages[5]", "messages[9]"], + } + ] + + def test_timeout_produces_output(self): + """Budget exhaustion produces a timeout ReflectorOutput.""" + from ace.core.recursive_agent import BudgetExhausted + + rr = RRStep("test-model", config=RRConfig()) + + with patch(_RUN_SYNC, side_effect=BudgetExhausted(compaction_count=0)): + result_ctx = rr(_make_ctx()) + + assert len(result_ctx.reflections) == 1 + output = result_ctx.reflections[0] + assert isinstance(output, ReflectorOutput) + assert "budget limit" in output.reasoning.lower() + assert output.raw.get("timeout") is True + + def test_timeout_with_ground_truth_correct(self): + """Timeout correctly detects correct answer.""" + from ace.core.recursive_agent import BudgetExhausted + + rr = RRStep("test-model", config=RRConfig()) + + with patch(_RUN_SYNC, side_effect=BudgetExhausted(compaction_count=0)): + output = rr.reflect( + question="What is 2+2?", + agent_output=AgentOutput(reasoning="r", final_answer="4"), + ground_truth="4", + ) + + assert isinstance(output, ReflectorOutput) + assert "correct" in output.reasoning.lower() + + def test_error_produces_safe_output(self): + """General exception produces a safe fallback output.""" + rr = RRStep("test-model", config=RRConfig()) + + with patch(_RUN_SYNC, side_effect=RuntimeError("unexpected error")): + result_ctx = rr(_make_ctx()) + + assert len(result_ctx.reflections) == 1 + output = result_ctx.reflections[0] + assert "failed" in output.reasoning.lower() + + +@pytest.mark.unit +class TestRRStepProtocol: + """Test that RRStep satisfies structural protocols.""" + + def test_satisfies_reflector_like(self): + """RRStep satisfies ReflectorLike protocol.""" + from ace.protocols import ReflectorLike + + rr = RRStep("test-model", config=RRConfig()) + assert isinstance(rr, ReflectorLike) + + def test_reflect_method(self): + """reflect() delegates to the PydanticAI agent.""" + rr = RRStep("test-model", config=RRConfig()) + reflection, metadata = _mock_compaction_result(key_insight="reflected") + + with patch(_RUN_SYNC, return_value=(reflection, metadata)): + result = rr.reflect( + question="What is 2+2?", + agent_output=AgentOutput(reasoning="r", final_answer="4"), + ground_truth="4", + feedback="Correct!", + ) + + assert isinstance(result, ReflectorOutput) + assert result.key_insight == "reflected" + + +@pytest.mark.unit +class TestMeteredModel: + """``MeteredModel`` fires the usage callback from the pydantic-ai model layer.""" + + def test_callback_invoked_with_request_usage_and_model_name(self): + from pydantic_ai import Agent + from pydantic_ai.models.test import TestModel + from pydantic_ai.usage import RequestUsage + + from ace.core.metered_model import MeteredModel + + calls: list[tuple[RequestUsage, str]] = [] + + def _cb(usage, model_id): + calls.append((usage, model_id)) + + inner = TestModel() + agent = Agent(MeteredModel(inner, _cb), output_type=str) + result = agent.run_sync("hello") + + assert result.output + assert len(calls) >= 1 + reported_usage, model_id = calls[-1] + assert isinstance(reported_usage, RequestUsage) + assert reported_usage.input_tokens > 0 + assert model_id == inner.model_name + + def test_callback_exception_does_not_break_agent_run(self): + from pydantic_ai import Agent + from pydantic_ai.models.test import TestModel + + from ace.core.metered_model import MeteredModel + + def _cb(usage, model_id): + raise RuntimeError("boom") + + agent = Agent(MeteredModel(TestModel(), _cb), output_type=str) + result = agent.run_sync("hello") + + assert result.output + + def test_rrstep_accepts_prebuilt_model_instance(self): + """Passing a pre-built ``Model`` flows through ``RRStep`` unchanged.""" + from pydantic_ai.models.test import TestModel + + test_model = TestModel() + rr = RRStep(test_model, config=RRConfig()) + + assert rr._model is test_model + assert rr._agent.model is test_model + + def test_rrstep_wraps_model_when_usage_callback_set(self): + """``RRStep.__init__`` routes the agent model through ``MeteredModel``.""" + from ace.core.metered_model import MeteredModel + + rr = RRStep( + "test-model", + config=RRConfig(usage_callback=lambda u, n: None), + ) + + assert isinstance(rr._agent.model, MeteredModel) + + def test_rrstep_does_not_wrap_when_no_callback(self): + """Without a callback there's no wrapper overhead.""" + from ace.core.metered_model import MeteredModel + + rr = RRStep("test-model", config=RRConfig()) + + assert not isinstance(rr._agent.model, MeteredModel) + + def test_rrstep_uses_prompted_reflector_output(self): + """RR should gather evidence with tools and return structured output directly.""" + rr = RRStep("test-model", config=RRConfig()) + + assert rr._agent._output_schema.mode == "prompted" + assert rr._agent._output_schema.allows_text is True + + def test_rrstep_defaults_to_deterministic_temperature(self): + """RR defaults to deterministic evidence analysis unless overridden.""" + rr = RRStep("test-model", config=RRConfig()) + + assert rr._agent.model_settings["temperature"] == 0.0 + + def test_rrstep_preserves_explicit_model_settings(self): + """Callers can still override RR model settings explicitly.""" + rr = RRStep( + "test-model", + config=RRConfig(), + model_settings=ModelSettings(temperature=0.7), + ) + + assert rr._agent.model_settings["temperature"] == 0.7 + + def test_rrstep_specializes_execute_code_tool_description(self): + """RR should present execute_code as an evidence tool, not a prose channel.""" + rr = RRStep("test-model", config=RRConfig()) + + tool = rr._agent._function_toolset.tools["execute_code"] + + assert "evidence workbench" in tool.description + assert "think" in tool.description + assert "store strings/snippets" in tool.description + assert tool.function_schema.description == tool.description + code_schema = tool.function_schema.json_schema["properties"]["code"] + assert "short snippet" in code_schema["description"] + + def test_small_trace_summary_includes_effort_guidance(self): + """Small traces should discourage transcript walkthroughs.""" + rr = RRStep("test-model", config=RRConfig()) + + summary = rr._build_data_summary( + { + "question": "q", + "feedback": "Task PASSED", + "messages": [{"role": "user", "content": "hello"}], + } + ) + + assert "Expected effort" in summary + assert "2-4 focused execute_code checks" in summary + assert "Do not produce a transcript walkthrough" in summary + + def test_prebuilt_model_and_callback_compose(self): + """Pre-built Model + usage_callback both apply — meter wraps the instance.""" + from pydantic_ai.models.test import TestModel + + from ace.core.metered_model import MeteredModel + + inner = TestModel() + rr = RRStep( + inner, + config=RRConfig(usage_callback=lambda u, n: None), + ) + + assert isinstance(rr._agent.model, MeteredModel) + assert rr._agent.model.wrapped is inner diff --git a/tests/test_tracing.py b/tests/test_tracing.py new file mode 100644 index 0000000000000000000000000000000000000000..a78b97a538fbbe6ff52c48cb4a9481a26110656e --- /dev/null +++ b/tests/test_tracing.py @@ -0,0 +1,294 @@ +"""Tests for the ace.tracing wrapper.""" + +from __future__ import annotations + +import os +from unittest.mock import patch + +import pytest + + +@pytest.mark.unit +class TestConfigure: + """Tests for ace.tracing.configure().""" + + def test_configure_sets_tracking_uri_and_token(self) -> None: + with patch.dict(os.environ, {}, clear=False): + from kayba_tracing._wrapper import configure + + with patch("kayba_tracing._wrapper.mlflow") as mock_mlflow: + configure(api_key="kb-test-key") + + mock_mlflow.set_tracking_uri.assert_called_once_with( + "https://use.kayba.ai/api/mlflow" + ) + assert os.environ["MLFLOW_TRACKING_TOKEN"] == "kb-test-key" + + def test_configure_custom_base_url(self) -> None: + with patch.dict(os.environ, {}, clear=False): + from kayba_tracing._wrapper import configure + + with patch("kayba_tracing._wrapper.mlflow") as mock_mlflow: + configure( + api_key="kb-test-key", + base_url="https://custom.example.com", + ) + + mock_mlflow.set_tracking_uri.assert_called_once_with( + "https://custom.example.com/api/mlflow" + ) + + def test_configure_strips_trailing_slash(self) -> None: + with patch.dict(os.environ, {}, clear=False): + from kayba_tracing._wrapper import configure + + with patch("kayba_tracing._wrapper.mlflow") as mock_mlflow: + configure( + api_key="kb-test-key", + base_url="https://custom.example.com/", + ) + + mock_mlflow.set_tracking_uri.assert_called_once_with( + "https://custom.example.com/api/mlflow" + ) + + def test_configure_reads_api_key_from_env(self) -> None: + with patch.dict(os.environ, {"KAYBA_API_KEY": "kb-env-key"}, clear=False): + from kayba_tracing._wrapper import configure + + with patch("kayba_tracing._wrapper.mlflow"): + configure() + + assert os.environ["MLFLOW_TRACKING_TOKEN"] == "kb-env-key" + + def test_configure_reads_base_url_from_env(self) -> None: + with patch.dict( + os.environ, + { + "KAYBA_API_KEY": "kb-key", + "KAYBA_API_URL": "https://env.example.com", + }, + clear=False, + ): + from kayba_tracing._wrapper import configure + + with patch("kayba_tracing._wrapper.mlflow") as mock_mlflow: + configure() + + mock_mlflow.set_tracking_uri.assert_called_once_with( + "https://env.example.com/api/mlflow" + ) + + def test_configure_raises_without_api_key(self) -> None: + with patch.dict(os.environ, {"KAYBA_API_KEY": ""}, clear=False): + from kayba_tracing._wrapper import configure + + with pytest.raises(ValueError, match="No API key provided"): + configure() + + def test_experiment_is_alias_for_folder(self) -> None: + import kayba_tracing._wrapper as w + + with patch.dict(os.environ, {}, clear=False): + with patch("kayba_tracing._wrapper.mlflow"): + w.configure(api_key="kb-key", experiment="my-project") + + assert w._folder == "my-project" + + def test_folder_takes_precedence_over_experiment(self) -> None: + import kayba_tracing._wrapper as w + + with patch.dict(os.environ, {}, clear=False): + with patch("kayba_tracing._wrapper.mlflow"): + w.configure( + api_key="kb-key", + experiment="from-experiment", + folder="from-folder", + ) + + assert w._folder == "from-folder" + + def test_configure_sets_folder(self) -> None: + import kayba_tracing._wrapper as w + + with patch.dict(os.environ, {}, clear=False): + with patch("kayba_tracing._wrapper.mlflow"): + w.configure(api_key="kb-key", folder="my-folder") + + assert w._folder == "my-folder" + + def test_configure_clears_folder_when_none(self) -> None: + import kayba_tracing._wrapper as w + + with patch.dict(os.environ, {}, clear=False): + with patch("kayba_tracing._wrapper.mlflow"): + w.configure(api_key="kb-key", folder="old") + w.configure(api_key="kb-key") + + assert w._folder is None + + +@pytest.mark.unit +class TestSanitizeFolder: + """Tests for folder name sanitization.""" + + def test_strips_html_tags(self) -> None: + from kayba_tracing._wrapper import _sanitize_folder + + assert _sanitize_folder('') == "alertxss" + + def test_strips_control_characters(self) -> None: + from kayba_tracing._wrapper import _sanitize_folder + + assert _sanitize_folder("folder\x00\x1f\nname") == "foldername" + + def test_allows_safe_characters(self) -> None: + from kayba_tracing._wrapper import _sanitize_folder + + assert _sanitize_folder("my-folder/sub_dir 2.0") == "my-folder/sub_dir 2.0" + + def test_truncates_long_names(self) -> None: + from kayba_tracing._wrapper import _sanitize_folder + + assert len(_sanitize_folder("a" * 500)) == 256 + + def test_strips_sql_injection_chars(self) -> None: + from kayba_tracing._wrapper import _sanitize_folder + + assert _sanitize_folder("folder'; DROP TABLE--") == "folder DROP TABLE--" + + def test_configure_sanitizes_folder(self) -> None: + import kayba_tracing._wrapper as w + + with patch.dict(os.environ, {}, clear=False): + with patch("kayba_tracing._wrapper.mlflow"): + w.configure(api_key="kb-key", folder='') + + # Entire input is an HTML tag, stripped to empty string + assert w._folder is None + + def test_set_folder_sanitizes(self) -> None: + import kayba_tracing._wrapper as w + + w.set_folder("bold") + assert w.get_folder() == "bold" + + +@pytest.mark.unit +class TestFolder: + """Tests for set_folder / get_folder.""" + + def test_set_and_get_folder(self) -> None: + import kayba_tracing._wrapper as w + + w.set_folder("production") + assert w.get_folder() == "production" + + def test_clear_folder(self) -> None: + import kayba_tracing._wrapper as w + + w.set_folder("production") + w.set_folder(None) + assert w.get_folder() is None + + def test_inject_folder_tag(self) -> None: + import kayba_tracing._wrapper as w + + w._folder = "my-folder" + with patch("kayba_tracing._wrapper.mlflow") as mock_mlflow: + w._inject_folder_tag() + mock_mlflow.update_current_trace.assert_called_once_with( + tags={"kayba.folder": "my-folder"} + ) + + def test_inject_folder_tag_noop_when_none(self) -> None: + import kayba_tracing._wrapper as w + + w._folder = None + with patch("kayba_tracing._wrapper.mlflow") as mock_mlflow: + w._inject_folder_tag() + mock_mlflow.update_current_trace.assert_not_called() + + +@pytest.mark.unit +class TestTraceDecorator: + """Tests for the trace decorator wrapper.""" + + def test_trace_wraps_function(self) -> None: + import kayba_tracing._wrapper as w + + w._folder = "test-folder" + + with patch("kayba_tracing._wrapper.mlflow") as mock_mlflow: + # Make mlflow.trace return a passthrough decorator + mock_mlflow.trace.side_effect = lambda fn=None, **kw: ( + fn if fn is not None else (lambda f: f) + ) + + @w.trace + def my_func(x: int) -> int: + return x + 1 + + result = my_func(5) + assert result == 6 + mock_mlflow.update_current_trace.assert_called_with( + tags={"kayba.folder": "test-folder"} + ) + + def test_trace_with_params(self) -> None: + import kayba_tracing._wrapper as w + + w._folder = None + + with patch("kayba_tracing._wrapper.mlflow") as mock_mlflow: + mock_mlflow.trace.return_value = lambda fn: fn + + @w.trace(name="custom", span_type="LLM") + def my_func() -> str: + return "ok" + + result = my_func() + assert result == "ok" + mock_mlflow.trace.assert_called_once_with(name="custom", span_type="LLM") + # No folder set, so no tag injection + mock_mlflow.update_current_trace.assert_not_called() + + +@pytest.mark.unit +class TestReExports: + """Verify utility re-exports.""" + + def test_enable_calls_mlflow(self) -> None: + from kayba_tracing._wrapper import enable + + with patch("kayba_tracing._wrapper.mlflow.tracing.enable") as mock: + enable() + mock.assert_called_once() + + def test_disable_calls_mlflow(self) -> None: + from kayba_tracing._wrapper import disable + + with patch("kayba_tracing._wrapper.mlflow.tracing.disable") as mock: + disable() + mock.assert_called_once() + + +@pytest.mark.unit +class TestPackageInit: + """Verify the public __init__ exports.""" + + def test_all_exports(self) -> None: + import ace.tracing + + expected = { + "configure", + "disable", + "enable", + "get_folder", + "get_trace", + "search_traces", + "set_folder", + "start_span", + "trace", + } + assert set(ace.tracing.__all__) == expected diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..59c3fd7f9f6a4fcddc58b5f9c29fa24995baa91d --- /dev/null +++ b/uv.lock @@ -0,0 +1,8872 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "accelerate" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/8e/ac2a9566747a93f8be36ee08532eb0160558b07630a081a6056a9f89bf1d/accelerate-1.12.0.tar.gz", hash = "sha256:70988c352feb481887077d2ab845125024b2a137a5090d6d7a32b57d03a45df6", size = 398399, upload-time = "2025-11-21T11:27:46.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d2/c581486aa6c4fbd7394c23c47b83fa1a919d34194e16944241daf9e762dd/accelerate-1.12.0-py3-none-any.whl", hash = "sha256:3e2091cd341423207e2f084a6654b1efcd250dc326f2a37d6dde446e07cabb11", size = 380935, upload-time = "2025-11-21T11:27:44.522Z" }, +] + +[[package]] +name = "ace-framework" +version = "0.12.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "litellm" }, + { name = "pydantic" }, + { name = "pydantic-ai-slim" }, + { name = "rank-bm25" }, + { name = "tau2" }, + { name = "tenacity" }, +] + +[package.optional-dependencies] +all = [ + { name = "accelerate" }, + { name = "boto3" }, + { name = "browser-use", version = "0.11.13", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "browser-use", version = "0.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "instructor" }, + { name = "kayba-tracing" }, + { name = "langchain-anthropic", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "langchain-anthropic", version = "1.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "langchain-litellm" }, + { name = "langchain-openai", version = "1.1.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "langchain-openai", version = "1.1.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "langgraph" }, + { name = "numpy" }, + { name = "python-dotenv", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "rank-bm25" }, + { name = "requests" }, + { name = "sentence-transformers" }, + { name = "tenacity" }, + { name = "torch" }, + { name = "transformers" }, +] +bedrock = [ + { name = "boto3" }, +] +browser-use = [ + { name = "browser-use", version = "0.11.13", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "browser-use", version = "0.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +claude-code = [ + { name = "python-dotenv", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "tenacity" }, +] +claude-sdk = [ + { name = "anthropic", version = "0.76.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "anthropic", version = "0.84.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +cloud = [ + { name = "questionary" }, + { name = "requests" }, +] +deduplication = [ + { name = "numpy" }, + { name = "sentence-transformers" }, +] +instructor = [ + { name = "instructor" }, +] +langchain = [ + { name = "langchain-anthropic", version = "1.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "langchain-anthropic", version = "1.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "langchain-litellm" }, + { name = "langchain-openai", version = "1.1.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "langchain-openai", version = "1.1.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "langgraph" }, +] +logfire = [ + { name = "logfire" }, +] +mcp = [ + { name = "mcp" }, + { name = "pydantic-settings" }, + { name = "tenacity" }, +] +tracing = [ + { name = "kayba-tracing" }, +] +transformers = [ + { name = "accelerate" }, + { name = "torch" }, + { name = "transformers" }, +] + +[package.dev-dependencies] +demos = [ + { name = "browser-use", version = "0.11.13", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "browser-use", version = "0.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "datasets" }, + { name = "openpyxl" }, + { name = "pandas" }, + { name = "playwright" }, + { name = "pyyaml" }, + { name = "rich", version = "14.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "rich", version = "14.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +dev = [ + { name = "black" }, + { name = "boto3" }, + { name = "git-changelog" }, + { name = "mypy" }, + { name = "nest-asyncio" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "types-requests" }, +] +tau-bench = [ + { name = "tau2" }, +] + +[package.metadata] +requires-dist = [ + { name = "accelerate", marker = "extra == 'all'", specifier = ">=0.20.0" }, + { name = "accelerate", marker = "extra == 'transformers'", specifier = ">=0.20.0" }, + { name = "anthropic", marker = "extra == 'claude-sdk'", specifier = ">=0.76.0" }, + { name = "boto3", marker = "extra == 'all'", specifier = ">=1.42.50" }, + { name = "boto3", marker = "extra == 'bedrock'", specifier = ">=1.42.50" }, + { name = "browser-use", marker = "extra == 'all'", specifier = ">=0.9.0" }, + { name = "browser-use", marker = "extra == 'browser-use'", specifier = ">=0.9.0" }, + { name = "click", specifier = ">=8.1.0" }, + { name = "instructor", marker = "extra == 'all'", specifier = ">=1.0.0" }, + { name = "instructor", marker = "extra == 'instructor'", specifier = ">=1.0.0" }, + { name = "kayba-tracing", marker = "extra == 'all'", editable = "sdk/python" }, + { name = "kayba-tracing", marker = "extra == 'tracing'", editable = "sdk/python" }, + { name = "langchain-anthropic", marker = "extra == 'all'", specifier = ">=0.3.0" }, + { name = "langchain-anthropic", marker = "extra == 'langchain'", specifier = ">=0.3.0" }, + { name = "langchain-litellm", marker = "extra == 'all'", specifier = ">=0.2.0" }, + { name = "langchain-litellm", marker = "extra == 'langchain'", specifier = ">=0.2.0" }, + { name = "langchain-openai", marker = "extra == 'all'", specifier = ">=0.3.35" }, + { name = "langchain-openai", marker = "extra == 'langchain'", specifier = ">=0.3.35" }, + { name = "langgraph", marker = "extra == 'all'", specifier = ">=0.2.0" }, + { name = "langgraph", marker = "extra == 'langchain'", specifier = ">=0.2.0" }, + { name = "litellm", specifier = ">=1.83.0" }, + { name = "logfire", extras = ["pydantic-ai"], marker = "extra == 'logfire'", specifier = ">=3.0.0" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.22.0" }, + { name = "numpy", marker = "extra == 'all'", specifier = ">=1.24.0" }, + { name = "numpy", marker = "extra == 'deduplication'", specifier = ">=1.24.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pydantic-ai-slim", extras = ["litellm"], specifier = ">=0.0.36" }, + { name = "pydantic-settings", marker = "extra == 'mcp'", specifier = ">=2.0.0" }, + { name = "python-dotenv", marker = "extra == 'all'", specifier = ">=1.0.0" }, + { name = "python-dotenv", marker = "extra == 'claude-code'", specifier = ">=1.0.0" }, + { name = "questionary", marker = "extra == 'cloud'", specifier = ">=2.0.0" }, + { name = "rank-bm25", specifier = ">=0.2.2" }, + { name = "rank-bm25", marker = "extra == 'all'", specifier = ">=0.2.2" }, + { name = "requests", marker = "extra == 'all'", specifier = ">=2.31.0" }, + { name = "requests", marker = "extra == 'cloud'", specifier = ">=2.31.0" }, + { name = "sentence-transformers", marker = "extra == 'all'", specifier = ">=2.2.0" }, + { name = "sentence-transformers", marker = "extra == 'deduplication'", specifier = ">=2.2.0" }, + { name = "tau2", git = "https://github.com/sierra-research/tau2-bench.git?branch=dev%2Ftau3" }, + { name = "tenacity", specifier = ">=9.1.4" }, + { name = "tenacity", marker = "extra == 'all'", specifier = ">=8.0.0" }, + { name = "tenacity", marker = "extra == 'claude-code'", specifier = ">=8.0.0" }, + { name = "tenacity", marker = "extra == 'mcp'", specifier = ">=8.0.0" }, + { name = "torch", marker = "extra == 'all'", specifier = ">=2.0.0" }, + { name = "torch", marker = "extra == 'transformers'", specifier = ">=2.0.0" }, + { name = "transformers", marker = "extra == 'all'", specifier = ">=4.30.0" }, + { name = "transformers", marker = "extra == 'transformers'", specifier = ">=4.30.0" }, +] +provides-extras = ["claude-sdk", "claude-code", "instructor", "deduplication", "browser-use", "logfire", "tracing", "bedrock", "langchain", "transformers", "all", "cloud", "mcp"] + +[package.metadata.requires-dev] +demos = [ + { name = "browser-use", specifier = ">=0.9.0" }, + { name = "datasets", specifier = ">=2.0.0" }, + { name = "openpyxl", specifier = ">=3.0.0" }, + { name = "pandas", specifier = ">=2.0.0" }, + { name = "playwright", specifier = ">=1.40.0" }, + { name = "pyyaml", specifier = ">=6.0.0" }, + { name = "rich", specifier = ">=13.0.0" }, +] +dev = [ + { name = "black", specifier = ">=23.0.0" }, + { name = "boto3", specifier = ">=1.42.50" }, + { name = "git-changelog", specifier = ">=2.5.0" }, + { name = "mypy", specifier = ">=1.0.0" }, + { name = "nest-asyncio", specifier = ">=1.6.0" }, + { name = "pre-commit", specifier = ">=3.0.0" }, + { name = "pytest", specifier = ">=7.0.0" }, + { name = "pytest-asyncio", specifier = ">=0.21.0" }, + { name = "pytest-cov", specifier = ">=4.0.0" }, + { name = "types-requests", specifier = ">=2.32.4.20260107" }, +] +tau-bench = [{ name = "tau2", git = "https://github.com/sierra-research/tau2-bench.git?branch=dev%2Ftau3" }] + +[[package]] +name = "addict" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ef/fd7649da8af11d93979831e8f1f8097e85e82d5bfeabc8c68b39175d8e75/addict-2.4.0.tar.gz", hash = "sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494", size = 9186, upload-time = "2020-11-21T16:21:31.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/00/b08f23b7d7e1e14ce01419a467b583edbb93c6cdb8654e54a9cc579cd61f/addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc", size = 3832, upload-time = "2020-11-21T16:21:29.588Z" }, +] + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + +[[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, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.76.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "distro", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "docstring-parser", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "httpx", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "jiter", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pydantic", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "sniffio", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/be/d11abafaa15d6304826438170f7574d750218f49a106c54424a40cef4494/anthropic-0.76.0.tar.gz", hash = "sha256:e0cae6a368986d5cf6df743dfbb1b9519e6a9eee9c6c942ad8121c0b34416ffe", size = 495483, upload-time = "2026-01-13T18:41:14.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/70/7b0fd9c1a738f59d3babe2b4212031c34ab7d0fda4ffef15b58a55c5bcea/anthropic-0.76.0-py3-none-any.whl", hash = "sha256:81efa3113901192af2f0fe977d3ec73fdadb1e691586306c4256cd6d5ccc331c", size = 390309, upload-time = "2026-01-13T18:41:13.483Z" }, +] + +[[package]] +name = "anthropic" +version = "0.84.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "distro", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "docstring-parser", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "httpx", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "jiter", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pydantic", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "sniffio", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/ea/0869d6df9ef83dcf393aeefc12dd81677d091c6ffc86f783e51cf44062f2/anthropic-0.84.0.tar.gz", hash = "sha256:72f5f90e5aebe62dca316cb013629cfa24996b0f5a4593b8c3d712bc03c43c37", size = 539457, upload-time = "2026-02-25T05:22:38.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/ca/218fa25002a332c0aa149ba18ffc0543175998b1f65de63f6d106689a345/anthropic-0.84.0-py3-none-any.whl", hash = "sha256:861c4c50f91ca45f942e091d83b60530ad6d4f98733bfe648065364da05d29e7", size = 455156, upload-time = "2026-02-25T05:22:40.468Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "authlib" +version = "1.6.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "cryptography", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" }, +] + +[[package]] +name = "authlib" +version = "1.6.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "black" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "boto3" +version = "1.42.59" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/4e/499cb52aaee9468c346bcc1158965e24e72b4e2a20052725b680e0ac949b/boto3-1.42.59.tar.gz", hash = "sha256:6c4a14a4eb37b58a9048901bdeefbe1c529638b73e8f55413319a25f010ca211", size = 112725, upload-time = "2026-02-27T20:25:33.228Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c0/22d868b9408dc5a33935a72896ec8d638b2766c459668d1b37c3e5ac2066/boto3-1.42.59-py3-none-any.whl", hash = "sha256:7a66e3e8e2087ea4403e135e9de592e6d63fc9a91080d8dac415bb74df873a72", size = 140557, upload-time = "2026-02-27T20:25:31.774Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.59" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/ae/50fb33bdf1911c216d50f98d989dd032a506f054cf829ebd737c6fa7e3e6/botocore-1.42.59.tar.gz", hash = "sha256:5314f19e1da8fc0ebc41bdb8bbe17c9a7397d87f4d887076ac8bdef972a34138", size = 14950271, upload-time = "2026-02-27T20:25:20.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/df/9d52819e0d804ead073d53ab1823bc0f0cb172a250fba31107b0b43fbb04/botocore-1.42.59-py3-none-any.whl", hash = "sha256:d2f2ff7ecc31e86ef46b5daee112cfbca052c13801285fb23af909f7bff5b657", size = 14619293, upload-time = "2026-02-27T20:25:17.455Z" }, +] + +[[package]] +name = "browser-use" +version = "0.11.13" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "aiohttp", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "anthropic", version = "0.84.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "anyio", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "authlib", version = "1.6.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "browser-use-sdk", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "bubus", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "cdp-use", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "click", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "cloudpickle", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "google-api-core", version = "2.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "google-api-python-client", version = "2.191.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "google-auth", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "google-auth-oauthlib", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "google-genai", version = "1.65.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "groq", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "httpx", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "inquirerpy", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "markdownify", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "mcp", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "ollama", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "openai", version = "2.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pillow", version = "12.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "portalocker", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "posthog", version = "7.9.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "psutil", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pydantic", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pyobjc", marker = "python_full_version >= '3.14' and platform_system == 'darwin' and sys_platform == 'win32'" }, + { name = "pyotp", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pypdf", version = "6.7.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "python-docx", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "reportlab", version = "4.4.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "requests", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "rich", version = "14.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "screeninfo", marker = "python_full_version >= '3.14' and platform_system != 'darwin' and sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "uuid7", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/86/40464b112d01dfedf2433570a6537dea1656715bf8631d18a6eaa2dce28b/browser_use-0.11.13.tar.gz", hash = "sha256:c20d029f17c44add2047a72c836cb589b85e90a31a91cf3632a22a2de1928dfe", size = 628359, upload-time = "2026-02-25T05:20:10.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ae/011c8a99708c82a2f8b75c5f24fb62541460fcb648050227db67d361bbe4/browser_use-0.11.13-py3-none-any.whl", hash = "sha256:f5232309213715e66e8f2079fb7097ac79a880728735968e4c7d41031ed15e83", size = 745686, upload-time = "2026-02-25T05:20:11.939Z" }, +] + +[[package]] +name = "browser-use" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "aiohttp", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "anthropic", version = "0.76.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "anyio", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "authlib", version = "1.6.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "browser-use-sdk", version = "2.0.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "bubus", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "cdp-use", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "click", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "cloudpickle", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "google-api-core", version = "2.29.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "google-api-python-client", version = "2.188.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "google-auth", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "google-auth-oauthlib", version = "1.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "google-genai", version = "1.60.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "groq", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "httpx", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "inquirerpy", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "markdownify", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "mcp", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "ollama", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "openai", version = "2.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "portalocker", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "posthog", version = "7.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "psutil", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pydantic", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pyobjc", marker = "(python_full_version < '3.14' and platform_system == 'darwin') or (platform_system == 'darwin' and sys_platform != 'win32')" }, + { name = "pyotp", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pypdf", version = "6.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "python-docx", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "python-dotenv", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "reportlab", version = "4.4.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "requests", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "rich", version = "14.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "screeninfo", marker = "(python_full_version < '3.14' and platform_system != 'darwin') or (platform_system != 'darwin' and sys_platform != 'win32')" }, + { name = "typing-extensions", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "uuid7", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/6b/72c82b0c254d39a9f84a4cd539a9bce8396318d56db2fc640d15a550eea2/browser_use-0.12.0.tar.gz", hash = "sha256:e7fee99a8b541720cd266d83f30209d1c541d482ef1df0504479ce47f800ec2f", size = 629049, upload-time = "2026-02-26T01:49:20.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/b5/ed3dec219a53d87c178e89e4291e5bf2a5e75c3e4356fbaed3eacf2be2d4/browser_use-0.12.0-py3-none-any.whl", hash = "sha256:db44ce05cc62e316df9fbcb95eae60086d1c68eb8396df87f5dcf0110c6e9a3d", size = 746445, upload-time = "2026-02-26T01:49:22.249Z" }, +] + +[[package]] +name = "browser-use-sdk" +version = "2.0.15" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "httpx", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pydantic", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pydantic-core", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/5c/ae0f1deaeb176496d932b9b5e14025a9ecf4b6f5ee5cce9a8212fef7dcda/browser_use_sdk-2.0.15.tar.gz", hash = "sha256:0832ae0998736e6386457e6cf506e2820db0d626c4d9dbf0f567ea2b6c6888d3", size = 61900, upload-time = "2026-02-09T20:33:11.69Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/4e/6b72701ec9dddf63c405c0535f3268e24a6f949388cf962bca24aad7c119/browser_use_sdk-2.0.15-py3-none-any.whl", hash = "sha256:5d8ba3836070c67a9774baac556b233e22f266823d3dee34295d98869bec7752", size = 127478, upload-time = "2026-02-09T20:33:10.796Z" }, +] + +[[package]] +name = "browser-use-sdk" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "httpx", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pydantic", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/c3/da2e5e42dc8a2af87bd2cbd9174946c4f137a60c5107c35ae83359a04412/browser_use_sdk-3.1.0.tar.gz", hash = "sha256:6680a87719fd083f8625b4375c7ff3175832a642ed1761acc65fc87143a4263d", size = 60673, upload-time = "2026-02-26T02:49:36.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/69/a8ed1350501c452bd57f7aaa8204a25e94ec85909f4883d2a6602128da19/browser_use_sdk-3.1.0-py3-none-any.whl", hash = "sha256:244e4b564ad4eb10397051b2b8b297fe61010cc20a61c164b1e6b1c32d1f3bae", size = 44939, upload-time = "2026-02-26T02:49:33.556Z" }, +] + +[[package]] +name = "bubus" +version = "1.5.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "anyio" }, + { name = "portalocker" }, + { name = "pydantic" }, + { name = "typing-extensions" }, + { name = "uuid7" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/85/aa72d1ffced7402fe41805519dab9935e9ce2bce18a10a55f2273ba8ba59/bubus-1.5.6.tar.gz", hash = "sha256:1a5456f0a576e86613a7bd66e819891b677778320b6e291094e339b0d9df2e0d", size = 60186, upload-time = "2025-08-30T18:20:43.032Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/54/23aae0681500a459fc4498b60754cb8ead8df964d8166e5915edb7e8136c/bubus-1.5.6-py3-none-any.whl", hash = "sha256:254ae37cd9299941f5e9d6afb11f8e3ce069f83e5b9476f88c6b2e32912f237d", size = 52121, upload-time = "2025-08-30T18:20:42.091Z" }, +] + +[[package]] +name = "cachetools" +version = "7.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, +] + +[[package]] +name = "cdp-use" +version = "1.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "typing-extensions" }, + { name = "websockets", version = "15.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "websockets", version = "16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/7a/c549417e8c5e4dface6d5d828cd7dc72502dcea33a99f5324abf5a853ce9/cdp_use-1.4.5.tar.gz", hash = "sha256:0da3a32df46336a03ff5a22bc6bc442cd7d2f2d50a118fd4856f29d37f6d26a0", size = 193961, upload-time = "2026-02-22T04:32:50.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/12/386d8c6bf0448c43674e24d6194c3b57d62e5361e90bca3d58108819ad32/cdp_use-1.4.5-py3-none-any.whl", hash = "sha256:8f8e2435e3a20e4009d2974144192cf3c132f6c2971338e156198814d9b91ecb", size = 350504, upload-time = "2026-02-22T04:32:49.22Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/60/d8f1dbfb7f06b94c662e98c95189e6f39b817da638bc8fcea0d003f89e5d/cuda_pathfinder-1.4.0-py3-none-any.whl", hash = "sha256:437079ca59e7b61ae439ecc501d69ed87b3accc34d58153ef1e54815e2c2e118", size = 38406, upload-time = "2026-02-25T22:13:00.807Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "cython" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/4d/1eb0c7c196a136b1926f4d7f0492a96c6fabd604d77e6cd43b56a3a16d83/cython-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64d7f71be3dd6d6d4a4c575bb3a4674ea06d1e1e5e4cd1b9882a2bc40ed3c4c9", size = 2970064, upload-time = "2026-01-04T14:15:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/18/b5/1cfca43b7d20a0fdb1eac67313d6bb6b18d18897f82dd0f17436bdd2ba7f/cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0", size = 2960506, upload-time = "2026-01-04T14:15:16.733Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d7/3bda3efce0c5c6ce79cc21285dbe6f60369c20364e112f5a506ee8a1b067/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa", size = 2971496, upload-time = "2026-01-04T14:15:25.038Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/fd393f0923c82be4ec0db712fffb2ff0a7a131707b842c99bf24b549274d/cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf", size = 2875622, upload-time = "2026-01-04T14:15:39.749Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" }, +] + +[[package]] +name = "databricks-sdk" +version = "0.102.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/b3/41ff1c3afe092df9085e084e0dc81c45bca5ed65f7b60dc59df0ade43c76/databricks_sdk-0.102.0.tar.gz", hash = "sha256:8fa5f82317ee27cc46323c6e2543d2cfefb4468653f92ba558271043c6f72fb9", size = 887450, upload-time = "2026-03-19T08:15:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/8c/d082bd5f72d7613524d5b35dfe1f71732b2246be2704fad68cd0e3fdd020/databricks_sdk-0.102.0-py3-none-any.whl", hash = "sha256:75d1253276ee8f3dd5e7b00d62594b7051838435e618f74a8570a6dbd723ec12", size = 838533, upload-time = "2026-03-19T08:15:52.248Z" }, +] + +[[package]] +name = "datasets" +version = "4.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/94/eb81c6fe32e9b6ef92223141b5a553aeff2e9456968424a8533cbe88f476/datasets-4.6.1.tar.gz", hash = "sha256:140ce500bc41939ff6ce995702d66b1f4b2ee7f117bb9b07512fab6804d4070a", size = 593865, upload-time = "2026-02-27T23:26:49.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/f0/99fe6eb530c7ee9ee1faee48059eb8a6437f80c893a496b98a78864e0fc6/datasets-4.6.1-py3-none-any.whl", hash = "sha256:f53228e6dadc9f837037b1bf3051d7d8c054abbb3eb29f1f022926e08090e0da", size = 520667, upload-time = "2026-02-27T23:26:46.855Z" }, +] + +[[package]] +name = "deepdiff" +version = "8.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "orderly-set" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/76/36c9aab3d5c19a94091f7c6c6e784efca50d87b124bf026c36e94719f33c/deepdiff-8.6.1.tar.gz", hash = "sha256:ec56d7a769ca80891b5200ec7bd41eec300ced91ebcc7797b41eb2b3f3ff643a", size = 634054, upload-time = "2025-09-03T19:40:41.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl", hash = "sha256:ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b", size = 91378, upload-time = "2025-09-03T19:40:39.679Z" }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, +] + +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "flask-cors" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" }, +] + +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + +[[package]] +name = "genai-prices" +version = "0.0.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/6b/94b3018a672c7775edfb485f0fed8f6068fba75e49b067e8a1ac5eb96764/genai_prices-0.0.56.tar.gz", hash = "sha256:ac24b16a84d0ab97539bfa48dfa4649689de8e3ce71c12ebacef29efb1998045", size = 65872, upload-time = "2026-03-20T20:33:00.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/f6/8ef7e4c286deb2709d11ca96a5237caae3ef4876ab3c48095856cfd2df30/genai_prices-0.0.56-py3-none-any.whl", hash = "sha256:dbe86be8f3f556bed1b72209ed36851fec8b01793b3b220f42921a4e7da945f6", size = 68966, upload-time = "2026-03-20T20:33:02.555Z" }, +] + +[[package]] +name = "git-changelog" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "semver" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/66/918cce4e4946645d212bbe9a50d490761f62c5745118fdb132c35143d4ed/git_changelog-2.7.0.tar.gz", hash = "sha256:bab8ecfe63e3ade284e1281e331240c09c37278a8f9ff54bf7f83d543ad9142f", size = 83835, upload-time = "2025-11-21T11:48:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/d1/a9baeae74278da6fcbb578147c45b602b719c6fa98bb8b6bca66c1601638/git_changelog-2.7.0-py3-none-any.whl", hash = "sha256:739b760149977729a293203aed0a85c35592637eec63b3bc739d3897c15984d7", size = 37600, upload-time = "2025-11-21T11:48:33.733Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.29.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "google-auth", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "googleapis-common-protos", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "proto-plus", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "protobuf", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "requests", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/10/05572d33273292bac49c2d1785925f7bc3ff2fe50e3044cf1062c1dde32e/google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", size = 177828, upload-time = "2026-01-08T22:21:39.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9", size = 173906, upload-time = "2026-01-08T22:21:36.093Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "google-auth", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "googleapis-common-protos", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "proto-plus", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "protobuf", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "requests", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/98/586ec94553b569080caef635f98a3723db36a38eac0e3d7eb3ea9d2e4b9a/google_api_core-2.30.0.tar.gz", hash = "sha256:02edfa9fab31e17fc0befb5f161b3bf93c9096d99aed584625f38065c511ad9b", size = 176959, upload-time = "2026-02-18T20:28:11.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/27/09c33d67f7e0dcf06d7ac17d196594e66989299374bfb0d4331d1038e76b/google_api_core-2.30.0-py3-none-any.whl", hash = "sha256:80be49ee937ff9aba0fd79a6eddfde35fe658b9953ab9b79c57dd7061afa8df5", size = 173288, upload-time = "2026-02-18T20:28:10.367Z" }, +] + +[[package]] +name = "google-api-python-client" +version = "2.188.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "google-api-core", version = "2.29.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "google-auth", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "google-auth-httplib2", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "httplib2", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "uritemplate", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/d7/14613c7efbab5b428b400961f5dbac46ad9e019c44e1f3fd14d67c33111c/google_api_python_client-2.188.0.tar.gz", hash = "sha256:5c469db6614f071009e3e5bb8b6aeeccae3beb3647fa9c6cd97f0d551edde0b6", size = 14302906, upload-time = "2026-01-13T22:15:13.747Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/67/a99a7d79d7a37a67cb8008f1d7dcedc46d29c6df5063aeb446112afd4aa4/google_api_python_client-2.188.0-py3-none-any.whl", hash = "sha256:3cad1b68f9d48b82b93d77927e8370a6f43f33d97848242601f14a93a1c70ef5", size = 14870005, upload-time = "2026-01-13T22:15:11.345Z" }, +] + +[[package]] +name = "google-api-python-client" +version = "2.191.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "google-api-core", version = "2.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "google-auth", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "google-auth-httplib2", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "httplib2", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "uritemplate", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/58/7d0843b7419a5ac51a27965e7233ef7c26ad693b490a74ab26548d0fd231/google_api_python_client-2.191.0.tar.gz", hash = "sha256:858c22fd46f51a65cee365a78aec2054e6d47b50434bee4ba62e91ac0944aea1", size = 14195310, upload-time = "2026-03-02T16:58:59.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/a4/da3a66c90f94b9eb433f0844f1bf79b5789bfa0859e55c0cd02c1139fed8/google_api_python_client-2.191.0-py3-none-any.whl", hash = "sha256:0768dde3202121abb3e897c4ee5150e58a25f32ee843780f6bee636dccc7ef23", size = 14769138, upload-time = "2026-03-02T16:58:57.665Z" }, +] + +[[package]] +name = "google-auth" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "httplib2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/ad/c1f2b1175096a8d04cf202ad5ea6065f108d26be6fc7215876bde4a7981d/google_auth_httplib2-0.3.0.tar.gz", hash = "sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b", size = 11134, upload-time = "2025-12-15T22:13:51.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/d5/3c97526c8796d3caf5f4b3bed2b05e8a7102326f00a334e7a438237f3b22/google_auth_httplib2-0.3.0-py3-none-any.whl", hash = "sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776", size = 9529, upload-time = "2025-12-15T22:13:51.048Z" }, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "google-auth", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "requests-oauthlib", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/dd/211f27c1e927e2292c2a71d5df1a2aaf261ce50ba7d50848c6ee24e20970/google_auth_oauthlib-1.2.4.tar.gz", hash = "sha256:3ca93859c6cc9003c8e12b2a0868915209d7953f05a70f4880ab57d57e56ee3e", size = 21185, upload-time = "2026-01-15T22:03:10.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/21/fb96db432d187b07756e62971c4d89bdef70259e4cfa76ee32bcc0ac97d1/google_auth_oauthlib-1.2.4-py3-none-any.whl", hash = "sha256:0e922eea5f2baacaf8867febb782e46e7b153236c21592ed76ab3ddb77ffd772", size = 19193, upload-time = "2026-01-15T22:03:09.046Z" }, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "google-auth", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "requests-oauthlib", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/b4/1b19567e4c567b796f5c593d89895f3cfae5a38e04f27c6af87618fd0942/google_auth_oauthlib-1.3.0.tar.gz", hash = "sha256:cd39e807ac7229d6b8b9c1e297321d36fcc8a9e4857dff4301870985df51a528", size = 21777, upload-time = "2026-02-27T14:13:01.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/56/909fd5632226d3fba31d7aeffd4754410735d49362f5809956fe3e9af344/google_auth_oauthlib-1.3.0-py3-none-any.whl", hash = "sha256:386b3fb85cf4a5b819c6ad23e3128d975216b4cac76324de1d90b128aaf38f29", size = 19308, upload-time = "2026-02-27T14:12:47.865Z" }, +] + +[[package]] +name = "google-genai" +version = "1.60.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "distro", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "google-auth", extra = ["requests"], marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "httpx", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pydantic", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "requests", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "sniffio", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "tenacity", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "websockets", version = "15.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/3f/a753be0dcee352b7d63bc6d1ba14a72591d63b6391dac0cdff7ac168c530/google_genai-1.60.0.tar.gz", hash = "sha256:9768061775fddfaecfefb0d6d7a6cabefb3952ebd246cd5f65247151c07d33d1", size = 487721, upload-time = "2026-01-21T22:17:30.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/e5/384b1f383917b5f0ae92e28f47bc27b16e3d26cd9bacb25e9f8ecab3c8fe/google_genai-1.60.0-py3-none-any.whl", hash = "sha256:967338378ffecebec19a8ed90cf8797b26818bacbefd7846a9280beb1099f7f3", size = 719431, upload-time = "2026-01-21T22:17:28.086Z" }, +] + +[[package]] +name = "google-genai" +version = "1.65.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "distro", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "google-auth", extra = ["requests"], marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "httpx", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pydantic", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "requests", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "sniffio", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "tenacity", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "websockets", version = "16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/f9/cc1191c2540d6a4e24609a586c4ed45d2db57cfef47931c139ee70e5874a/google_genai-1.65.0.tar.gz", hash = "sha256:d470eb600af802d58a79c7f13342d9ea0d05d965007cae8f76c7adff3d7a4750", size = 497206, upload-time = "2026-02-26T00:20:33.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/3c/3fea4e7c91357c71782d7dcaad7a2577d636c90317e003386893c25bc62c/google_genai-1.65.0-py3-none-any.whl", hash = "sha256:68c025205856919bc03edb0155c11b4b833810b7ce17ad4b7a9eeba5158f6c44", size = 724429, upload-time = "2026-02-26T00:20:32.186Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[[package]] +name = "graphene" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "graphql-core" }, + { name = "graphql-relay" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/f6/bf62ff950c317ed03e77f3f6ddd7e34aaa98fe89d79ebd660c55343d8054/graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa", size = 44739, upload-time = "2024-11-09T20:44:25.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/e0/61d8e98007182e6b2aca7cf65904721fb2e4bce0192272ab9cb6f69d8812/graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71", size = 114894, upload-time = "2024-11-09T20:44:23.851Z" }, +] + +[[package]] +name = "graphql-core" +version = "3.2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/c5/36aa96205c3ecbb3d34c7c24189e4553c7ca2ebc7e1dd07432339b980272/graphql_core-3.2.8.tar.gz", hash = "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3", size = 513181, upload-time = "2026-03-05T19:55:37.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/41/cb887d9afc5dabd78feefe6ccbaf83ff423c206a7a1b7aeeac05120b2125/graphql_core-3.2.8-py3-none-any.whl", hash = "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c", size = 207349, upload-time = "2026-03-05T19:55:35.911Z" }, +] + +[[package]] +name = "graphql-relay" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "graphql-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/13/98fbf8d67552f102488ffc16c6f559ce71ea15f6294728d33928ab5ff14d/graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c", size = 50027, upload-time = "2022-04-16T11:03:45.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/16/a4cf06adbc711bd364a73ce043b0b08d8fa5aae3df11b6ee4248bcdad2e0/graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5", size = 16940, upload-time = "2022-04-16T11:03:43.895Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/06/eccbd311c9e2b3ca45dbc063b93134c57a1ccc7607c5e545264ad092c4a9/griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934", size = 166312, upload-time = "2026-03-23T21:06:55.954Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, +] + +[[package]] +name = "groq" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/12/f4099a141677fcd2ed79dcc1fcec431e60c52e0e90c9c5d935f0ffaf8c0e/groq-1.0.0.tar.gz", hash = "sha256:66cb7bb729e6eb644daac7ce8efe945e99e4eb33657f733ee6f13059ef0c25a9", size = 146068, upload-time = "2025-12-17T23:34:23.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/88/3175759d2ef30406ea721f4d837bfa1ba4339fde3b81ba8c5640a96ed231/groq-1.0.0-py3-none-any.whl", hash = "sha256:6e22bf92ffad988f01d2d4df7729add66b8fd5dbfb2154b5bbf3af245b72c731", size = 138292, upload-time = "2025-12-17T23:34:21.957Z" }, +] + +[[package]] +name = "gunicorn" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/f4/e78fa054248fab913e2eab0332c6c2cb07421fca1ce56d8fe43b6aef57a4/gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889", size = 634883, upload-time = "2026-03-27T00:00:26.092Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/c8/8aaf447698c4d59aa853fd318eed300b5c9e44459f242ab8ead6c9c09792/gunicorn-25.3.0-py3-none-any.whl", hash = "sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660", size = 208403, upload-time = "2026-03-27T00:00:27.386Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/cb/9bb543bd987ffa1ee48202cc96a756951b734b79a542335c566148ade36c/hf_xet-1.3.2.tar.gz", hash = "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", size = 643646, upload-time = "2026-02-27T17:26:08.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/75/462285971954269432aad2e7938c5c7ff9ec7d60129cec542ab37121e3d6/hf_xet-1.3.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", size = 3761019, upload-time = "2026-02-27T17:25:49.441Z" }, + { url = "https://files.pythonhosted.org/packages/35/56/987b0537ddaf88e17192ea09afa8eca853e55f39a4721578be436f8409df/hf_xet-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", size = 3521565, upload-time = "2026-02-27T17:25:47.469Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5c/7e4a33a3d689f77761156cc34558047569e54af92e4d15a8f493229f6767/hf_xet-1.3.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", size = 4176494, upload-time = "2026-02-27T17:25:40.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b3/71e856bf9d9a69b3931837e8bf22e095775f268c8edcd4a9e8c355f92484/hf_xet-1.3.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", size = 3955601, upload-time = "2026-02-27T17:25:38.376Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/aecf97b3f0a981600a67ff4db15e2d433389d698a284bb0ea5d8fcdd6f7f/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", size = 4154770, upload-time = "2026-02-27T17:25:56.756Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e1/3af961f71a40e09bf5ee909842127b6b00f5ab4ee3817599dc0771b79893/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", size = 4394161, upload-time = "2026-02-27T17:25:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c3/859509bade9178e21b8b1db867b8e10e9f817ab9ac1de77cb9f461ced765/hf_xet-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", size = 3637377, upload-time = "2026-02-27T17:26:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/724cfbef4da92d577b71f68bf832961c8919f36c60d28d289a9fc9d024d4/hf_xet-1.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", size = 3497875, upload-time = "2026-02-27T17:26:09.034Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/9d54c1ae1d05fb704f977eca1671747babf1957f19f38ae75c5933bc2dc1/hf_xet-1.3.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c34e2c7aefad15792d57067c1c89b2b02c1bbaeabd7f8456ae3d07b4bbaf4094", size = 3761076, upload-time = "2026-02-27T17:25:55.42Z" }, + { url = "https://files.pythonhosted.org/packages/f2/8a/08a24b6c6f52b5d26848c16e4b6d790bb810d1bf62c3505bed179f7032d3/hf_xet-1.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4bc995d6c41992831f762096020dc14a65fdf3963f86ffed580b596d04de32e3", size = 3521745, upload-time = "2026-02-27T17:25:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/b5/db/a75cf400dd8a1a8acf226a12955ff6ee999f272dfc0505bafd8079a61267/hf_xet-1.3.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:959083c89dee30f7d6f890b36cdadda823386c4de63b1a30384a75bfd2ae995d", size = 4176301, upload-time = "2026-02-27T17:25:46.044Z" }, + { url = "https://files.pythonhosted.org/packages/01/40/6c4c798ffdd83e740dd3925c4e47793b07442a9efa3bc3866ba141a82365/hf_xet-1.3.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cfa760888633b08c01b398d212ce7e8c0d7adac6c86e4b20dfb2397d8acd78ee", size = 3955437, upload-time = "2026-02-27T17:25:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/0c/09/9a3aa7c5f07d3e5cc57bb750d12a124ffa72c273a87164bd848f9ac5cc14/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3155a02e083aa21fd733a7485c7c36025e49d5975c8d6bda0453d224dd0b0ac4", size = 4154535, upload-time = "2026-02-27T17:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e0/831f7fa6d90cb47a230bc23284b502c700e1483bbe459437b3844cdc0776/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91b1dc03c31cbf733d35dc03df7c5353686233d86af045e716f1e0ea4a2673cf", size = 4393891, upload-time = "2026-02-27T17:26:06.607Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/6ed472fdce7f8b70f5da6e3f05be76816a610063003bfd6d9cea0bbb58a3/hf_xet-1.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:211f30098512d95e85ad03ae63bd7dd2c4df476558a5095d09f9e38e78cbf674", size = 3637583, upload-time = "2026-02-27T17:26:17.349Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/a069edc4570b3f8e123c0b80fadc94530f3d7b01394e1fc1bb223339366c/hf_xet-1.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4a6817c41de7c48ed9270da0b02849347e089c5ece9a0e72ae4f4b3a57617f82", size = 3497977, upload-time = "2026-02-27T17:26:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/dbb024e2e3907f6f3052847ca7d1a2f7a3972fafcd53ff79018977fcb3e4/hf_xet-1.3.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f", size = 3763961, upload-time = "2026-02-27T17:25:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/e4/71/b99aed3823c9d1795e4865cf437d651097356a3f38c7d5877e4ac544b8e4/hf_xet-1.3.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", size = 3526171, upload-time = "2026-02-27T17:25:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/907890ce6ef5598b5920514f255ed0a65f558f820515b18db75a51b2f878/hf_xet-1.3.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", size = 4180750, upload-time = "2026-02-27T17:25:43.125Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ad/bc7f41f87173d51d0bce497b171c4ee0cbde1eed2d7b4216db5d0ada9f50/hf_xet-1.3.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", size = 3961035, upload-time = "2026-02-27T17:25:41.837Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/600f4dda40c4a33133404d9fe644f1d35ff2d9babb4d0435c646c63dd107/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", size = 4161378, upload-time = "2026-02-27T17:26:00.365Z" }, + { url = "https://files.pythonhosted.org/packages/00/b3/7bc1ff91d1ac18420b7ad1e169b618b27c00001b96310a89f8a9294fe509/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", size = 4398020, upload-time = "2026-02-27T17:26:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/99bfd948a3ed3620ab709276df3ad3710dcea61976918cce8706502927af/hf_xet-1.3.2-cp37-abi3-win_amd64.whl", hash = "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", size = 3641624, upload-time = "2026-02-27T17:26:13.542Z" }, + { url = "https://files.pythonhosted.org/packages/cc/02/9a6e4ca1f3f73a164c0cd48e41b3cc56585dcc37e809250de443d673266f/hf_xet-1.3.2-cp37-abi3-win_arm64.whl", hash = "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", size = 3503976, upload-time = "2026-02-27T17:26:12.123Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httplib2" +version = "0.31.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huey" +version = "2.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/29/3428d52eb8e85025e264a291641a9f9d6407cc1e51d1b630f6ac5815999a/huey-2.6.0.tar.gz", hash = "sha256:8d11f8688999d65266af1425b831f6e3773e99415027177b8734b0ffd5e251f6", size = 221068, upload-time = "2026-01-06T03:01:02.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/34/fae9ac8f1c3a552fd3f7ff652b94c78d219dedc5fce0c0a4232457760a00/huey-2.6.0-py3-none-any.whl", hash = "sha256:1b9df9d370b49c6d5721ba8a01ac9a787cf86b3bdc584e4679de27b920395c3f", size = 76951, upload-time = "2026-01-06T03:01:00.808Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.5.0" +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 = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/76/b5efb3033d8499b17f9386beaf60f64c461798e1ee16d10bc9c0077beba5/huggingface_hub-1.5.0.tar.gz", hash = "sha256:f281838db29265880fb543de7a23b0f81d3504675de82044307ea3c6c62f799d", size = 695872, upload-time = "2026-02-26T15:35:32.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/74/2bc951622e2dbba1af9a460d93c51d15e458becd486e62c29cc0ccb08178/huggingface_hub-1.5.0-py3-none-any.whl", hash = "sha256:c9c0b3ab95a777fc91666111f3b3ede71c0cdced3614c553a64e98920585c4ee", size = 596261, upload-time = "2026-02-26T15:35:31.1Z" }, +] + +[[package]] +name = "identify" +version = "2.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "inquirerpy" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pfzy" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/73/7570847b9da026e07053da3bbe2ac7ea6cde6bb2cbd3c7a5a950fa0ae40b/InquirerPy-0.3.4.tar.gz", hash = "sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e", size = 44431, upload-time = "2022-06-27T23:11:20.598Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/ff/3b59672c47c6284e8005b42e84ceba13864aa0f39f067c973d1af02f5d91/InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4", size = 67677, upload-time = "2022-06-27T23:11:17.723Z" }, +] + +[[package]] +name = "instructor" +version = "1.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "diskcache" }, + { name = "docstring-parser" }, + { name = "jinja2" }, + { name = "jiter" }, + { name = "openai", version = "2.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "openai", version = "2.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "rich", version = "14.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "rich", version = "14.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "tenacity" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/ef/986d059424db204ed57b29d8c07fda35de2a2c72dee8ea7994bc90a6f767/instructor-1.14.5.tar.gz", hash = "sha256:fcb6432867f2fe4a5986e8bf389dcc64ed2ad4039a12a2dff85464e51c2f171a", size = 69950754, upload-time = "2026-01-29T14:18:50.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/04/e442e1356c97b03a6d30d2b462f7c0bdfbf207e75f6833815fd1225a75b4/instructor-1.14.5-py3-none-any.whl", hash = "sha256:2a5a31222b008c0989be1cc001e33a237f49506e80ac5833f6d36d7690bae7b1", size = 177445, upload-time = "2026-01-29T14:18:53.641Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/68/0357982493a7b20925aece061f7fb7a2678e3b232f8d73a6edb7e5304443/jiter-0.11.1.tar.gz", hash = "sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc", size = 168385, upload-time = "2025-10-17T11:31:15.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/8b/318e8af2c904a9d29af91f78c1e18f0592e189bbdb8a462902d31fe20682/jiter-0.11.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c92148eec91052538ce6823dfca9525f5cfc8b622d7f07e9891a280f61b8c96c", size = 305655, upload-time = "2025-10-17T11:29:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/f7/29/6c7de6b5d6e511d9e736312c0c9bfcee8f9b6bef68182a08b1d78767e627/jiter-0.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd4da91b5415f183a6be8f7158d127bdd9e6a3174138293c0d48d6ea2f2009d", size = 315645, upload-time = "2025-10-17T11:29:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5f/ef9e5675511ee0eb7f98dd8c90509e1f7743dbb7c350071acae87b0145f3/jiter-0.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7e3ac25c00b9275684d47aa42febaa90a9958e19fd1726c4ecf755fbe5e553b", size = 348003, upload-time = "2025-10-17T11:29:22.712Z" }, + { url = "https://files.pythonhosted.org/packages/56/1b/abe8c4021010b0a320d3c62682769b700fb66f92c6db02d1a1381b3db025/jiter-0.11.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d7305c0a841858f866cd459cd9303f73883fb5e097257f3d4a3920722c69d4", size = 365122, upload-time = "2025-10-17T11:29:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/2a/2d/4a18013939a4f24432f805fbd5a19893e64650b933edb057cd405275a538/jiter-0.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e86fa10e117dce22c547f31dd6d2a9a222707d54853d8de4e9a2279d2c97f239", size = 488360, upload-time = "2025-10-17T11:29:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/38124f5d02ac4131f0dfbcfd1a19a0fac305fa2c005bc4f9f0736914a1a4/jiter-0.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae5ef1d48aec7e01ee8420155d901bb1d192998fa811a65ebb82c043ee186711", size = 376884, upload-time = "2025-10-17T11:29:27.056Z" }, + { url = "https://files.pythonhosted.org/packages/7b/43/59fdc2f6267959b71dd23ce0bd8d4aeaf55566aa435a5d00f53d53c7eb24/jiter-0.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb68e7bf65c990531ad8715e57d50195daf7c8e6f1509e617b4e692af1108939", size = 358827, upload-time = "2025-10-17T11:29:28.698Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d0/b3cc20ff5340775ea3bbaa0d665518eddecd4266ba7244c9cb480c0c82ec/jiter-0.11.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43b30c8154ded5845fa454ef954ee67bfccce629b2dea7d01f795b42bc2bda54", size = 385171, upload-time = "2025-10-17T11:29:30.078Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bc/94dd1f3a61f4dc236f787a097360ec061ceeebebf4ea120b924d91391b10/jiter-0.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:586cafbd9dd1f3ce6a22b4a085eaa6be578e47ba9b18e198d4333e598a91db2d", size = 518359, upload-time = "2025-10-17T11:29:31.464Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8c/12ee132bd67e25c75f542c227f5762491b9a316b0dad8e929c95076f773c/jiter-0.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:677cc2517d437a83bb30019fd4cf7cad74b465914c56ecac3440d597ac135250", size = 509205, upload-time = "2025-10-17T11:29:32.895Z" }, + { url = "https://files.pythonhosted.org/packages/39/d5/9de848928ce341d463c7e7273fce90ea6d0ea4343cd761f451860fa16b59/jiter-0.11.1-cp312-cp312-win32.whl", hash = "sha256:fa992af648fcee2b850a3286a35f62bbbaeddbb6dbda19a00d8fbc846a947b6e", size = 205448, upload-time = "2025-10-17T11:29:34.217Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/8002d78637e05009f5e3fb5288f9d57d65715c33b5d6aa20fd57670feef5/jiter-0.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88b5cae9fa51efeb3d4bd4e52bfd4c85ccc9cac44282e2a9640893a042ba4d87", size = 204285, upload-time = "2025-10-17T11:29:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a2/bb24d5587e4dff17ff796716542f663deee337358006a80c8af43ddc11e5/jiter-0.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:9a6cae1ab335551917f882f2c3c1efe7617b71b4c02381e4382a8fc80a02588c", size = 188712, upload-time = "2025-10-17T11:29:37.027Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4b/e4dd3c76424fad02a601d570f4f2a8438daea47ba081201a721a903d3f4c/jiter-0.11.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:71b6a920a5550f057d49d0e8bcc60945a8da998019e83f01adf110e226267663", size = 305272, upload-time = "2025-10-17T11:29:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/67/83/2cd3ad5364191130f4de80eacc907f693723beaab11a46c7d155b07a092c/jiter-0.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b3de72e925388453a5171be83379549300db01284f04d2a6f244d1d8de36f94", size = 314038, upload-time = "2025-10-17T11:29:40.563Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3c/8e67d9ba524e97d2f04c8f406f8769a23205026b13b0938d16646d6e2d3e/jiter-0.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc19dd65a2bd3d9c044c5b4ebf657ca1e6003a97c0fc10f555aa4f7fb9821c00", size = 345977, upload-time = "2025-10-17T11:29:42.009Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/489ce64d992c29bccbffabb13961bbb0435e890d7f2d266d1f3df5e917d2/jiter-0.11.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d58faaa936743cd1464540562f60b7ce4fd927e695e8bc31b3da5b914baa9abd", size = 364503, upload-time = "2025-10-17T11:29:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c0/e321dd83ee231d05c8fe4b1a12caf1f0e8c7a949bf4724d58397104f10f2/jiter-0.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:902640c3103625317291cb73773413b4d71847cdf9383ba65528745ff89f1d14", size = 487092, upload-time = "2025-10-17T11:29:44.835Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/8f24ec49c8d37bd37f34ec0112e0b1a3b4b5a7b456c8efff1df5e189ad43/jiter-0.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30405f726e4c2ed487b176c09f8b877a957f535d60c1bf194abb8dadedb5836f", size = 376328, upload-time = "2025-10-17T11:29:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/7f/70/ded107620e809327cf7050727e17ccfa79d6385a771b7fe38fb31318ef00/jiter-0.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3217f61728b0baadd2551844870f65219ac4a1285d5e1a4abddff3d51fdabe96", size = 356632, upload-time = "2025-10-17T11:29:47.454Z" }, + { url = "https://files.pythonhosted.org/packages/19/53/c26f7251613f6a9079275ee43c89b8a973a95ff27532c421abc2a87afb04/jiter-0.11.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1364cc90c03a8196f35f396f84029f12abe925415049204446db86598c8b72c", size = 384358, upload-time = "2025-10-17T11:29:49.377Z" }, + { url = "https://files.pythonhosted.org/packages/84/16/e0f2cc61e9c4d0b62f6c1bd9b9781d878a427656f88293e2a5335fa8ff07/jiter-0.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53a54bf8e873820ab186b2dca9f6c3303f00d65ae5e7b7d6bda1b95aa472d646", size = 517279, upload-time = "2025-10-17T11:29:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/4cd095eaee68961bca3081acbe7c89e12ae24a5dae5fd5d2a13e01ed2542/jiter-0.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7e29aca023627b0e0c2392d4248f6414d566ff3974fa08ff2ac8dbb96dfee92a", size = 508276, upload-time = "2025-10-17T11:29:52.619Z" }, + { url = "https://files.pythonhosted.org/packages/4f/25/f459240e69b0e09a7706d96ce203ad615ca36b0fe832308d2b7123abf2d0/jiter-0.11.1-cp313-cp313-win32.whl", hash = "sha256:f153e31d8bca11363751e875c0a70b3d25160ecbaee7b51e457f14498fb39d8b", size = 205593, upload-time = "2025-10-17T11:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/7c/16/461bafe22bae79bab74e217a09c907481a46d520c36b7b9fe71ee8c9e983/jiter-0.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:f773f84080b667c69c4ea0403fc67bb08b07e2b7ce1ef335dea5868451e60fed", size = 203518, upload-time = "2025-10-17T11:29:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/7b/72/c45de6e320edb4fa165b7b1a414193b3cae302dd82da2169d315dcc78b44/jiter-0.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:635ecd45c04e4c340d2187bcb1cea204c7cc9d32c1364d251564bf42e0e39c2d", size = 188062, upload-time = "2025-10-17T11:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/4a57922437ca8753ef823f434c2dec5028b237d84fa320f06a3ba1aec6e8/jiter-0.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d892b184da4d94d94ddb4031296931c74ec8b325513a541ebfd6dfb9ae89904b", size = 313814, upload-time = "2025-10-17T11:29:58.509Z" }, + { url = "https://files.pythonhosted.org/packages/76/50/62a0683dadca25490a4bedc6a88d59de9af2a3406dd5a576009a73a1d392/jiter-0.11.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa22c223a3041dacb2fcd37c70dfd648b44662b4a48e242592f95bda5ab09d58", size = 344987, upload-time = "2025-10-17T11:30:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/da/00/2355dbfcbf6cdeaddfdca18287f0f38ae49446bb6378e4a5971e9356fc8a/jiter-0.11.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330e8e6a11ad4980cd66a0f4a3e0e2e0f646c911ce047014f984841924729789", size = 356399, upload-time = "2025-10-17T11:30:02.084Z" }, + { url = "https://files.pythonhosted.org/packages/c9/07/c2bd748d578fa933d894a55bff33f983bc27f75fc4e491b354bef7b78012/jiter-0.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:09e2e386ebf298547ca3a3704b729471f7ec666c2906c5c26c1a915ea24741ec", size = 203289, upload-time = "2025-10-17T11:30:03.656Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ee/ace64a853a1acbd318eb0ca167bad1cf5ee037207504b83a868a5849747b/jiter-0.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:fe4a431c291157e11cee7c34627990ea75e8d153894365a3bc84b7a959d23ca8", size = 188284, upload-time = "2025-10-17T11:30:05.046Z" }, + { url = "https://files.pythonhosted.org/packages/8d/00/d6006d069e7b076e4c66af90656b63da9481954f290d5eca8c715f4bf125/jiter-0.11.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0fa1f70da7a8a9713ff8e5f75ec3f90c0c870be6d526aa95e7c906f6a1c8c676", size = 304624, upload-time = "2025-10-17T11:30:06.678Z" }, + { url = "https://files.pythonhosted.org/packages/fc/45/4a0e31eb996b9ccfddbae4d3017b46f358a599ccf2e19fbffa5e531bd304/jiter-0.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:569ee559e5046a42feb6828c55307cf20fe43308e3ae0d8e9e4f8d8634d99944", size = 315042, upload-time = "2025-10-17T11:30:08.87Z" }, + { url = "https://files.pythonhosted.org/packages/e7/91/22f5746f5159a28c76acdc0778801f3c1181799aab196dbea2d29e064968/jiter-0.11.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f69955fa1d92e81987f092b233f0be49d4c937da107b7f7dcf56306f1d3fcce9", size = 346357, upload-time = "2025-10-17T11:30:10.222Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4f/57620857d4e1dc75c8ff4856c90cb6c135e61bff9b4ebfb5dc86814e82d7/jiter-0.11.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:090f4c9d4a825e0fcbd0a2647c9a88a0f366b75654d982d95a9590745ff0c48d", size = 365057, upload-time = "2025-10-17T11:30:11.585Z" }, + { url = "https://files.pythonhosted.org/packages/ce/34/caf7f9cc8ae0a5bb25a5440cc76c7452d264d1b36701b90fdadd28fe08ec/jiter-0.11.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbf3d8cedf9e9d825233e0dcac28ff15c47b7c5512fdfe2e25fd5bbb6e6b0cee", size = 487086, upload-time = "2025-10-17T11:30:13.052Z" }, + { url = "https://files.pythonhosted.org/packages/50/17/85b5857c329d533d433fedf98804ebec696004a1f88cabad202b2ddc55cf/jiter-0.11.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa9b1958f9c30d3d1a558b75f0626733c60eb9b7774a86b34d88060be1e67fe", size = 376083, upload-time = "2025-10-17T11:30:14.416Z" }, + { url = "https://files.pythonhosted.org/packages/85/d3/2d9f973f828226e6faebdef034097a2918077ea776fb4d88489949024787/jiter-0.11.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42d1ca16590b768c5e7d723055acd2633908baacb3628dd430842e2e035aa90", size = 357825, upload-time = "2025-10-17T11:30:15.765Z" }, + { url = "https://files.pythonhosted.org/packages/f4/55/848d4dabf2c2c236a05468c315c2cb9dc736c5915e65449ccecdba22fb6f/jiter-0.11.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5db4c2486a023820b701a17aec9c5a6173c5ba4393f26662f032f2de9c848b0f", size = 383933, upload-time = "2025-10-17T11:30:17.34Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6c/204c95a4fbb0e26dfa7776c8ef4a878d0c0b215868011cc904bf44f707e2/jiter-0.11.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4573b78777ccfac954859a6eff45cbd9d281d80c8af049d0f1a3d9fc323d5c3a", size = 517118, upload-time = "2025-10-17T11:30:18.684Z" }, + { url = "https://files.pythonhosted.org/packages/88/25/09956644ea5a2b1e7a2a0f665cb69a973b28f4621fa61fc0c0f06ff40a31/jiter-0.11.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7593ac6f40831d7961cb67633c39b9fef6689a211d7919e958f45710504f52d3", size = 508194, upload-time = "2025-10-17T11:30:20.719Z" }, + { url = "https://files.pythonhosted.org/packages/09/49/4d1657355d7f5c9e783083a03a3f07d5858efa6916a7d9634d07db1c23bd/jiter-0.11.1-cp314-cp314-win32.whl", hash = "sha256:87202ec6ff9626ff5f9351507def98fcf0df60e9a146308e8ab221432228f4ea", size = 203961, upload-time = "2025-10-17T11:30:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/76/bd/f063bd5cc2712e7ca3cf6beda50894418fc0cfeb3f6ff45a12d87af25996/jiter-0.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:a5dd268f6531a182c89d0dd9a3f8848e86e92dfff4201b77a18e6b98aa59798c", size = 202804, upload-time = "2025-10-17T11:30:23.452Z" }, + { url = "https://files.pythonhosted.org/packages/52/ca/4d84193dfafef1020bf0bedd5e1a8d0e89cb67c54b8519040effc694964b/jiter-0.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:5d761f863f912a44748a21b5c4979c04252588ded8d1d2760976d2e42cd8d991", size = 188001, upload-time = "2025-10-17T11:30:24.915Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/3b05e5c9d32efc770a8510eeb0b071c42ae93a5b576fd91cee9af91689a1/jiter-0.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2cc5a3965285ddc33e0cab933e96b640bc9ba5940cea27ebbbf6695e72d6511c", size = 312561, upload-time = "2025-10-17T11:30:26.742Z" }, + { url = "https://files.pythonhosted.org/packages/50/d3/335822eb216154ddb79a130cbdce88fdf5c3e2b43dc5dba1fd95c485aaf5/jiter-0.11.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b572b3636a784c2768b2342f36a23078c8d3aa6d8a30745398b1bab58a6f1a8", size = 344551, upload-time = "2025-10-17T11:30:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/31/6d/a0bed13676b1398f9b3ba61f32569f20a3ff270291161100956a577b2dd3/jiter-0.11.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad93e3d67a981f96596d65d2298fe8d1aa649deb5374a2fb6a434410ee11915e", size = 363051, upload-time = "2025-10-17T11:30:30.009Z" }, + { url = "https://files.pythonhosted.org/packages/a4/03/313eda04aa08545a5a04ed5876e52f49ab76a4d98e54578896ca3e16313e/jiter-0.11.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83097ce379e202dcc3fe3fc71a16d523d1ee9192c8e4e854158f96b3efe3f2f", size = 485897, upload-time = "2025-10-17T11:30:31.429Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/a1011b9d325e40b53b1b96a17c010b8646013417f3902f97a86325b19299/jiter-0.11.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7042c51e7fbeca65631eb0c332f90c0c082eab04334e7ccc28a8588e8e2804d9", size = 375224, upload-time = "2025-10-17T11:30:33.18Z" }, + { url = "https://files.pythonhosted.org/packages/92/da/1b45026b19dd39b419e917165ff0ea629dbb95f374a3a13d2df95e40a6ac/jiter-0.11.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a68d679c0e47649a61df591660507608adc2652442de7ec8276538ac46abe08", size = 356606, upload-time = "2025-10-17T11:30:34.572Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9acb0e54d6a8ba59ce923a180ebe824b4e00e80e56cefde86cc8e0a948be/jiter-0.11.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b0da75dbf4b6ec0b3c9e604d1ee8beaf15bc046fff7180f7d89e3cdbd3bb51", size = 384003, upload-time = "2025-10-17T11:30:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2b/e5a5fe09d6da2145e4eed651e2ce37f3c0cf8016e48b1d302e21fb1628b7/jiter-0.11.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:69dd514bf0fa31c62147d6002e5ca2b3e7ef5894f5ac6f0a19752385f4e89437", size = 516946, upload-time = "2025-10-17T11:30:37.425Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fe/db936e16e0228d48eb81f9934e8327e9fde5185e84f02174fcd22a01be87/jiter-0.11.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:bb31ac0b339efa24c0ca606febd8b77ef11c58d09af1b5f2be4c99e907b11111", size = 507614, upload-time = "2025-10-17T11:30:38.977Z" }, + { url = "https://files.pythonhosted.org/packages/86/db/c4438e8febfb303486d13c6b72f5eb71cf851e300a0c1f0b4140018dd31f/jiter-0.11.1-cp314-cp314t-win32.whl", hash = "sha256:b2ce0d6156a1d3ad41da3eec63b17e03e296b78b0e0da660876fccfada86d2f7", size = 204043, upload-time = "2025-10-17T11:30:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/36/59/81badb169212f30f47f817dfaabf965bc9b8204fed906fab58104ee541f9/jiter-0.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f4db07d127b54c4a2d43b4cf05ff0193e4f73e0dd90c74037e16df0b29f666e1", size = 204046, upload-time = "2025-10-17T11:30:41.692Z" }, + { url = "https://files.pythonhosted.org/packages/dd/01/43f7b4eb61db3e565574c4c5714685d042fb652f9eef7e5a3de6aafa943a/jiter-0.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:28e4fdf2d7ebfc935523e50d1efa3970043cfaa161674fe66f9642409d001dfe", size = 188069, upload-time = "2025-10-17T11:30:43.23Z" }, + { url = "https://files.pythonhosted.org/packages/a6/bc/950dd7f170c6394b6fdd73f989d9e729bd98907bcc4430ef080a72d06b77/jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:0d4d6993edc83cf75e8c6828a8d6ce40a09ee87e38c7bfba6924f39e1337e21d", size = 302626, upload-time = "2025-10-17T11:31:09.645Z" }, + { url = "https://files.pythonhosted.org/packages/3a/65/43d7971ca82ee100b7b9b520573eeef7eabc0a45d490168ebb9a9b5bb8b2/jiter-0.11.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f78d151c83a87a6cf5461d5ee55bc730dd9ae227377ac6f115b922989b95f838", size = 297034, upload-time = "2025-10-17T11:31:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/19/4c/000e1e0c0c67e96557a279f8969487ea2732d6c7311698819f977abae837/jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9022974781155cd5521d5cb10997a03ee5e31e8454c9d999dcdccd253f2353f", size = 337328, upload-time = "2025-10-17T11:31:12.399Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/71408b02c6133153336d29fa3ba53000f1e1a3f78bb2fc2d1a1865d2e743/jiter-0.11.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18c77aaa9117510d5bdc6a946baf21b1f0cfa58ef04d31c8d016f206f2118960", size = 343697, upload-time = "2025-10-17T11:31:13.773Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "kayba-tracing" +version = "0.9.7" +source = { editable = "sdk/python" } +dependencies = [ + { name = "mlflow" }, +] + +[package.metadata] +requires-dist = [{ name = "mlflow", specifier = ">=3.1.0" }] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, +] + +[[package]] +name = "langchain-anthropic" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "anthropic", version = "0.76.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "langchain-core", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pydantic", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/b6/ac5ee84e15bf79844c9c791f99a614c7ec7e1a63c2947e55977be01a81b4/langchain_anthropic-1.3.1.tar.gz", hash = "sha256:4f3d7a4a7729ab1aeaf62d32c87d4d227c1b5421668ca9e3734562b383470b07", size = 708940, upload-time = "2026-01-05T21:07:19.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/4f/7a5b32764addf4b757545b89899b9d76688176f19e4ee89868e3b8bbfd0f/langchain_anthropic-1.3.1-py3-none-any.whl", hash = "sha256:1fc28cf8037c30597ee6172fc2ff9e345efe8149a8c2a39897b1eebba2948322", size = 46328, upload-time = "2026-01-05T21:07:18.261Z" }, +] + +[[package]] +name = "langchain-anthropic" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "anthropic", version = "0.84.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "langchain-core", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pydantic", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/4e/7c1ffac126f5e62b0b9066f331f91ae69361e73476fd3ca1b19f8d8a3cc3/langchain_anthropic-1.3.4.tar.gz", hash = "sha256:000ed4c2d6fb8842b4ffeed22a74a3e84f9e9bcb63638e4abbb4a1d8ffa07211", size = 671858, upload-time = "2026-02-24T13:54:01.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/cf/b7c7b7270efbb3db2edbf14b09ba9110a41628f3a85a11cae9527a35641c/langchain_anthropic-1.3.4-py3-none-any.whl", hash = "sha256:cd112dcc8049aef09f58b3c4338b2c9db5ee98105e08664954a4e40d8bf120b9", size = 47454, upload-time = "2026-02-24T13:54:00.53Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.2.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/a7/4c992456dae89a8704afec03e3c2a0149ccc5f29c1cbdd5f4aa77628e921/langchain_core-1.2.16.tar.gz", hash = "sha256:055a4bfe7d62f4ac45ed49fd759ee2e6bdd15abf998fbeea695fda5da2de6413", size = 835286, upload-time = "2026-02-25T16:27:30.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/a1/57d5feaa11dc2ebb40f3bc3d7bf4294b6703e152e56edea9d4c622475a6a/langchain_core-1.2.16-py3-none-any.whl", hash = "sha256:2768add9aa97232a7712580f678e0ba045ee1036c71fe471355be0434fcb6e30", size = 502219, upload-time = "2026-02-25T16:27:29.379Z" }, +] + +[[package]] +name = "langchain-litellm" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "httpx" }, + { name = "langchain-core" }, + { name = "litellm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/bd/96f2fbaf6274d97b463d787691f1ba9298f977c40361bbfc86f622f793e5/langchain_litellm-0.6.1.tar.gz", hash = "sha256:e1dae0c547ad577235998c40700e88a1fec74741c2e010831feff37d57ecb229", size = 332978, upload-time = "2026-03-01T20:35:33.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/ac/cf509fa91e90fea069e40c8d9fea2f89eb0b0acf8a6e33e76ec17c1fc4f5/langchain_litellm-0.6.1-py3-none-any.whl", hash = "sha256:c62b49b3a151d1cee912ba79156cac120d12e572f4ade27557eaeb37a67d6571", size = 24862, upload-time = "2026-03-01T20:35:34.444Z" }, +] + +[[package]] +name = "langchain-openai" +version = "1.1.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "langchain-core", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "openai", version = "2.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "tiktoken", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ae/1dbeb49ab8f098f78ec52e21627e705e5d7c684dc8826c2c34cc2746233a/langchain_openai-1.1.9.tar.gz", hash = "sha256:fdee25dcf4b0685d8e2f59856f4d5405431ef9e04ab53afe19e2e8360fed8234", size = 1004828, upload-time = "2026-02-10T21:03:21.615Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a1/8a20d19f69d022c10d34afa42d972cc50f971b880d0eb4a828cf3dd824a8/langchain_openai-1.1.9-py3-none-any.whl", hash = "sha256:ca2482b136c45fb67c0db84a9817de675e0eb8fb2203a33914c1b7a96f273940", size = 85769, upload-time = "2026-02-10T21:03:20.333Z" }, +] + +[[package]] +name = "langchain-openai" +version = "1.1.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "langchain-core", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "openai", version = "2.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "tiktoken", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/0f/01147f842499338ae3b0dd0a351fb83006d9ed623cf3a999bd68ba5bbe2d/langchain_openai-1.1.10.tar.gz", hash = "sha256:ca6fae7cf19425acc81814efed59c7d205ec9a1f284fd1d08aae9bda85d6501b", size = 1059755, upload-time = "2026-02-17T18:03:44.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/17/3785cbcdc81c451179247e4176d2697879cb4f45ab2c59d949ca574e072d/langchain_openai-1.1.10-py3-none-any.whl", hash = "sha256:d91b2c09e9fbc70f7af45345d3aa477744962d41c73a029beb46b4f83b824827", size = 87205, upload-time = "2026-02-17T18:03:43.502Z" }, +] + +[[package]] +name = "langgraph" +version = "1.0.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/92/14df6fefba28c10caf1cb05aa5b8c7bf005838fe32a86d903b6c7cc4018d/langgraph-1.0.10.tar.gz", hash = "sha256:73bd10ee14a8020f31ef07e9cd4c1a70c35cc07b9c2b9cd637509a10d9d51e29", size = 511644, upload-time = "2026-02-27T21:04:38.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/60/260e0c04620a37ba8916b712766c341cc5fc685dabc6948c899494bbc2ae/langgraph-1.0.10-py3-none-any.whl", hash = "sha256:7c298bef4f6ea292fcf9824d6088fe41a6727e2904ad6066f240c4095af12247", size = 160920, upload-time = "2026-02-27T21:04:35.932Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/44/a8df45d1e8b4637e29789fa8bae1db022c953cc7ac80093cfc52e923547e/langgraph_checkpoint-4.0.1.tar.gz", hash = "sha256:b433123735df11ade28829e40ce25b9be614930cd50245ff2af60629234befd9", size = 158135, upload-time = "2026-02-27T21:06:16.092Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/4c/09a4a0c42f5d2fc38d6c4d67884788eff7fd2cfdf367fdf7033de908b4c0/langgraph_checkpoint-4.0.1-py3-none-any.whl", hash = "sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034", size = 50453, upload-time = "2026-02-27T21:06:14.293Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/06/dd61a5c2dce009d1b03b1d56f2a85b3127659fdddf5b3be5d8f1d60820fb/langgraph_prebuilt-1.0.8.tar.gz", hash = "sha256:0cd3cf5473ced8a6cd687cc5294e08d3de57529d8dd14fdc6ae4899549efcf69", size = 164442, upload-time = "2026-02-19T18:14:39.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/41/ec966424ad3f2ed3996d24079d3342c8cd6c0bd0653c12b2a917a685ec6c/langgraph_prebuilt-1.0.8-py3-none-any.whl", hash = "sha256:d16a731e591ba4470f3e313a319c7eee7dbc40895bcf15c821f985a3522a7ce0", size = 35648, upload-time = "2026-02-19T18:14:37.611Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/bd/ca8ae5c6a34be6d4f7aa86016e010ff96b3a939456041565797952e3014d/langgraph_sdk-0.3.9.tar.gz", hash = "sha256:8be8958529b3f6d493ec248fdb46e539362efda75784654a42a7091d22504e0e", size = 184287, upload-time = "2026-02-24T18:39:03.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/4c/7a7510260fbda788efd13bf4650d3e7d80988118441ac811ec78e0aa03ac/langgraph_sdk-0.3.9-py3-none-any.whl", hash = "sha256:94654294250c920789b6ed0d8a70c0117fed5736b61efc24ff647157359453c5", size = 90511, upload-time = "2026-02-24T18:39:02.012Z" }, +] + +[[package]] +name = "langsmith" +version = "0.7.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "uuid-utils" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/01/c26b1d3a68764acd050cbb98f3ca922a25b3e4ece5768ee868f56206b4d4/langsmith-0.7.9.tar.gz", hash = "sha256:c6dfcc4cb8fea249714ac60a1963faa84cc59ded9cd1882794ffce8a8d1d1588", size = 1136295, upload-time = "2026-02-27T22:37:59.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/c9/2d5e5f654f97a4d38a0ff1b3004751c2cd81ceca05d603174e49f942b196/langsmith-0.7.9-py3-none-any.whl", hash = "sha256:e73478f4c4ae9b7407e0fcdced181f9f8b0e024c62a1552dbf0667ef6b19e82d", size = 344099, upload-time = "2026-02-27T22:37:57.497Z" }, +] + +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + +[[package]] +name = "litellm" +version = "1.83.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai", version = "2.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "openai", version = "2.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pydantic" }, + { name = "python-dotenv", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/92/6ce9737554994ca8e536e5f4f6a87cc7c4774b656c9eb9add071caf7d54b/litellm-1.83.0.tar.gz", hash = "sha256:860bebc76c4bb27b4cf90b4a77acd66dba25aced37e3db98750de8a1766bfb7a", size = 17333062, upload-time = "2026-03-31T05:08:25.331Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/2c/a670cc050fcd6f45c6199eb99e259c73aea92edba8d5c2fc1b3686d36217/litellm-1.83.0-py3-none-any.whl", hash = "sha256:88c536d339248f3987571493015784671ba3f193a328e1ea6780dbebaa2094a8", size = 15610306, upload-time = "2026-03-31T05:08:21.987Z" }, +] + +[[package]] +name = "logfire" +version = "4.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "executing" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, + { name = "rich", version = "14.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "rich", version = "14.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/40/3d09fe09cfa63753feada2d41dd909ce0741dd5731014a4b3eb31bdee977/logfire-4.29.0.tar.gz", hash = "sha256:18a306a0b5744aee8ad0a8f5d6b3a47a6d8951c340eaecc42dc5d0224f4bdca0", size = 1057563, upload-time = "2026-03-13T15:30:24.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/aa/fb8102ea48924fbbb9dfced7bada5717875801808ad53f9a60b6b4fec440/logfire-4.29.0-py3-none-any.whl", hash = "sha256:8dd7fdf6bed21459b8893eaa290d61977b9ebcc901844e365ddee868b5d8bca8", size = 302227, upload-time = "2026-03-13T15:30:20.742Z" }, +] + +[[package]] +name = "logfire-api" +version = "4.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/a4/ed2d823b4ad9a4c9dad1959c3399705c90ed3d96e6faaea5b897deb0f17c/logfire_api-4.29.0.tar.gz", hash = "sha256:55430c554cf198dcbddee390eca259a10a26d5f7e3527d51f859ddc31a83c840", size = 76407, upload-time = "2026-03-13T15:30:25.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/cc/62df4abc3e4650c25b81a8e39a1d498d3246c43f3aa4bfab7a73689317b4/logfire_api-4.29.0-py3-none-any.whl", hash = "sha256:48a1361b818357f5a37c71f9683f97e626e5df6c17f35212bfc1f19dddc6771c", size = 121457, upload-time = "2026-03-13T15:30:22.652Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[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, upload-time = "2025-08-11T12:57:52.854Z" } +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, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markdownify" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/bc/c8c8eea5335341306b0fa7e1cb33c5e1c8d24ef70ddd684da65f41c49c92/markdownify-1.2.2.tar.gz", hash = "sha256:b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09", size = 18816, upload-time = "2025-11-16T19:21:18.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ce/f1e3e9d959db134cedf06825fae8d5b294bd368aacdd0831a3975b7c4d55/markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a", size = 15724, upload-time = "2025-11-16T19:21:17.622Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pillow", version = "12.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, +] + +[[package]] +name = "mcp" +version = "1.26.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/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[[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, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mlflow" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "alembic" }, + { name = "cryptography" }, + { name = "docker" }, + { name = "flask" }, + { name = "flask-cors" }, + { name = "graphene" }, + { name = "gunicorn", marker = "sys_platform != 'win32'" }, + { name = "huey" }, + { name = "matplotlib" }, + { name = "mlflow-skinny" }, + { name = "mlflow-tracing" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "skops" }, + { name = "sqlalchemy" }, + { name = "waitress", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/34/e328c073cd32c186fb242a957e5bade82433c06bc45b7d1695bf4d02f166/mlflow-3.11.1.tar.gz", hash = "sha256:84e54c4be91b5b2a19039a2673fe688b1d7307ceddacc08af51f8df05b19ee56", size = 9797469, upload-time = "2026-04-07T14:26:58.463Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/62/96826c340354638dfedcbdbcd35d67754566bd45f6592300e0c215c80e30/mlflow-3.11.1-py3-none-any.whl", hash = "sha256:8f6bf1238ac04f97664c229dd480380c5c254a78bdb3c0e433e3a0397508b1af", size = 10479141, upload-time = "2026-04-07T14:26:55.709Z" }, +] + +[[package]] +name = "mlflow-skinny" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "click" }, + { name = "cloudpickle" }, + { name = "databricks-sdk" }, + { name = "fastapi" }, + { name = "gitpython" }, + { name = "importlib-metadata" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "python-dotenv", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlparse" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/77/fe2027ddad9e52ed1ac360fbc262169e6366f6678632e350cbd0d901bb9b/mlflow_skinny-3.11.1.tar.gz", hash = "sha256:86ce63491349f6713afc8a4ef0bf77a8314d0e79e03753cb150d6c860a0b0475", size = 2642799, upload-time = "2026-04-07T14:26:43.818Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/a7/e61ec397b34dc3c9e91572f45e41617f429d5c524d38a4e1aa2316ee1b5e/mlflow_skinny-3.11.1-py3-none-any.whl", hash = "sha256:82ffd5f6980320b4ac19f741e7a754faa1d01707e632b002ea68e04fd25a0535", size = 3171551, upload-time = "2026-04-07T14:26:41.762Z" }, +] + +[[package]] +name = "mlflow-tracing" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "databricks-sdk" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/77/73af163432f3c66e2d213045250972e504a6683c76f63dd1abfba441a16a/mlflow_tracing-3.11.1.tar.gz", hash = "sha256:cb63cee16385d081467ec5bee4807fe1af59ddfdf04be4c79e7a7813b1002193", size = 1314550, upload-time = "2026-04-07T14:26:32.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/ab/d980c84e7df4224ab8db2457afbe135b430f371ca081a37cf89f8ef18ca1/mlflow_tracing-3.11.1-py3-none-any.whl", hash = "sha256:fa82df64dacf8293b714ae666440fe7c1902c6470c024df389bb91e9de3106d9", size = 1575790, upload-time = "2026-04-07T14:26:30.804Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "multiprocess" +version = "0.70.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/fd/2ae3826f5be24c6ed87266bc4e59c46ea5b059a103f3d7e7eb76a52aeecb/multiprocess-0.70.18.tar.gz", hash = "sha256:f9597128e6b3e67b23956da07cf3d2e5cba79e2f4e0fba8d7903636663ec6d0d", size = 1798503, upload-time = "2025-04-17T03:11:27.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/d8/0cba6cf51a1a31f20471fbc823a716170c73012ddc4fb85d706630ed6e8f/multiprocess-0.70.18-py310-none-any.whl", hash = "sha256:60c194974c31784019c1f459d984e8f33ee48f10fcf42c309ba97b30d9bd53ea", size = 134948, upload-time = "2025-04-17T03:11:20.223Z" }, + { url = "https://files.pythonhosted.org/packages/4b/88/9039f2fed1012ef584751d4ceff9ab4a51e5ae264898f0b7cbf44340a859/multiprocess-0.70.18-py311-none-any.whl", hash = "sha256:5aa6eef98e691281b3ad923be2832bf1c55dd2c859acd73e5ec53a66aae06a1d", size = 144462, upload-time = "2025-04-17T03:11:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/5f922792be93b82ec6b5f270bbb1ef031fd0622847070bbcf9da816502cc/multiprocess-0.70.18-py312-none-any.whl", hash = "sha256:9b78f8e5024b573730bfb654783a13800c2c0f2dfc0c25e70b40d184d64adaa2", size = 150287, upload-time = "2025-04-17T03:11:22.69Z" }, + { url = "https://files.pythonhosted.org/packages/ee/25/7d7e78e750bc1aecfaf0efbf826c69a791d2eeaf29cf20cba93ff4cced78/multiprocess-0.70.18-py313-none-any.whl", hash = "sha256:871743755f43ef57d7910a38433cfe41319e72be1bbd90b79c7a5ac523eb9334", size = 151917, upload-time = "2025-04-17T03:11:24.044Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c3/ca84c19bd14cdfc21c388fdcebf08b86a7a470ebc9f5c3c084fc2dbc50f7/multiprocess-0.70.18-py38-none-any.whl", hash = "sha256:dbf705e52a154fe5e90fb17b38f02556169557c2dd8bb084f2e06c2784d8279b", size = 132636, upload-time = "2025-04-17T03:11:24.936Z" }, + { url = "https://files.pythonhosted.org/packages/6c/28/dd72947e59a6a8c856448a5e74da6201cb5502ddff644fbc790e4bd40b9a/multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8", size = 133478, upload-time = "2025-04-17T03:11:26.253Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "ollama" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" }, +] + +[[package]] +name = "openai" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "distro", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "httpx", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "jiter", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pydantic", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "sniffio", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "tqdm", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/6c/e4c964fcf1d527fdf4739e7cc940c60075a4114d50d03871d5d5b1e13a88/openai-2.16.0.tar.gz", hash = "sha256:42eaa22ca0d8ded4367a77374104d7a2feafee5bd60a107c3c11b5243a11cd12", size = 629649, upload-time = "2026-01-27T23:28:02.579Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/83/0315bf2cfd75a2ce8a7e54188e9456c60cec6c0cf66728ed07bd9859ff26/openai-2.16.0-py3-none-any.whl", hash = "sha256:5f46643a8f42899a84e80c38838135d7038e7718333ce61396994f887b09a59b", size = 1068612, upload-time = "2026-01-27T23:28:00.356Z" }, +] + +[[package]] +name = "openai" +version = "2.24.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "distro", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "httpx", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "jiter", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pydantic", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "sniffio", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "tqdm", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, +] + +[[package]] +name = "orderly-set" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/88/39c83c35d5e97cc203e9e77a4f93bf87ec89cf6a22ac4818fdcc65d66584/orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce", size = 27414, upload-time = "2025-07-10T20:10:55.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, + { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, + { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { 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, upload-time = "2025-09-29T23:39:08.659Z" }, + { 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, upload-time = "2025-09-29T23:19:59.765Z" }, + { 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, upload-time = "2025-09-29T23:20:14.098Z" }, + { 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, upload-time = "2025-09-29T23:20:26.76Z" }, + { 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, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { 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, upload-time = "2025-09-29T23:21:05.024Z" }, + { 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, upload-time = "2025-09-29T23:21:15.979Z" }, + { 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, upload-time = "2025-09-29T23:21:27.165Z" }, + { 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, upload-time = "2025-09-29T23:21:40.532Z" }, + { 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, upload-time = "2025-09-29T23:21:55.77Z" }, + { 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, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { 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, upload-time = "2025-09-29T23:22:24.343Z" }, + { 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, upload-time = "2025-09-29T23:22:37.762Z" }, + { 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, upload-time = "2025-09-29T23:22:51.688Z" }, + { 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, upload-time = "2025-09-29T23:23:05.042Z" }, + { 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, upload-time = "2025-09-29T23:23:28.57Z" }, + { 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, upload-time = "2025-09-29T23:24:24.876Z" }, + { 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, upload-time = "2025-09-29T23:25:52.486Z" }, + { 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, upload-time = "2025-09-29T23:26:33.204Z" }, + { 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, upload-time = "2025-09-29T23:27:15.384Z" }, + { 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, upload-time = "2025-09-29T23:27:51.625Z" }, + { 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, upload-time = "2025-09-29T23:28:21.289Z" }, + { 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, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { 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, upload-time = "2025-09-29T23:29:31.47Z" }, + { 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, upload-time = "2025-09-29T23:29:54.591Z" }, + { 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, upload-time = "2025-09-29T23:30:21.003Z" }, + { 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, upload-time = "2025-09-29T23:30:43.391Z" }, + { 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, upload-time = "2025-09-29T23:31:10.009Z" }, + { 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, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pfzy" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/5a/32b50c077c86bfccc7bed4881c5a2b823518f5450a30e639db5d3711952e/pfzy-0.3.4.tar.gz", hash = "sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1", size = 8396, upload-time = "2022-01-28T02:26:17.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d7/8ff98376b1acc4503253b685ea09981697385ce344d4e3935c2af49e044d/pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96", size = 8537, upload-time = "2022-01-28T02:26:16.047Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" }, + { url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" }, + { url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" }, + { url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" }, + { url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" }, + { url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" }, + { url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" }, + { url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, + { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, +] + +[[package]] +name = "playwright" +version = "1.58.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/c9/9c6061d5703267f1baae6a4647bfd1862e386fbfdb97d889f6f6ae9e3f64/playwright-1.58.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:96e3204aac292ee639edbfdef6298b4be2ea0a55a16b7068df91adac077cc606", size = 42251098, upload-time = "2026-01-30T15:09:24.028Z" }, + { url = "https://files.pythonhosted.org/packages/e0/40/59d34a756e02f8c670f0fee987d46f7ee53d05447d43cd114ca015cb168c/playwright-1.58.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:70c763694739d28df71ed578b9c8202bb83e8fe8fb9268c04dd13afe36301f71", size = 41039625, upload-time = "2026-01-30T15:09:27.558Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ee/3ce6209c9c74a650aac9028c621f357a34ea5cd4d950700f8e2c4b7fe2c4/playwright-1.58.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:185e0132578733d02802dfddfbbc35f42be23a45ff49ccae5081f25952238117", size = 42251098, upload-time = "2026-01-30T15:09:30.461Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/009958cbf23fac551a940d34e3206e6c7eed2b8c940d0c3afd1feb0b0589/playwright-1.58.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:c95568ba1eda83812598c1dc9be60b4406dffd60b149bc1536180ad108723d6b", size = 46235268, upload-time = "2026-01-30T15:09:33.787Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a6/0e66ad04b6d3440dae73efb39540c5685c5fc95b17c8b29340b62abbd952/playwright-1.58.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9999948f1ab541d98812de25e3a8c410776aa516d948807140aff797b4bffa", size = 45964214, upload-time = "2026-01-30T15:09:36.751Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/236e60ab9f6d62ed0fd32150d61f1f494cefbf02304c0061e78ed80c1c32/playwright-1.58.0-py3-none-win32.whl", hash = "sha256:1e03be090e75a0fabbdaeab65ce17c308c425d879fa48bb1d7986f96bfad0b99", size = 36815998, upload-time = "2026-01-30T15:09:39.627Z" }, + { url = "https://files.pythonhosted.org/packages/41/f8/5ec599c5e59d2f2f336a05b4f318e733077cd5044f24adb6f86900c3e6a7/playwright-1.58.0-py3-none-win_amd64.whl", hash = "sha256:a2bf639d0ce33b3ba38de777e08697b0d8f3dc07ab6802e4ac53fb65e3907af8", size = 36816005, upload-time = "2026-01-30T15:09:42.449Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c4/cc0229fea55c87d6c9c67fe44a21e2cd28d1d558a5478ed4d617e9fb0c93/playwright-1.58.0-py3-none-win_arm64.whl", hash = "sha256:32ffe5c303901a13a0ecab91d1c3f74baf73b84f4bedbb6b935f5bc11cc98e1b", size = 33085919, upload-time = "2026-01-30T15:09:45.71Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "portalocker" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891, upload-time = "2024-07-13T23:15:34.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/fb/a70a4214956182e0d7a9099ab17d50bfcba1056188e9b14f35b9e2b62a0d/portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf", size = 18423, upload-time = "2024-07-13T23:15:32.602Z" }, +] + +[[package]] +name = "posthog" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "backoff", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "distro", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "python-dateutil", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "requests", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "six", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/dd/ca6d5a79614af27ededc0dca85e77f42f7704e29f8314819d7ce92b9a7f3/posthog-7.7.0.tar.gz", hash = "sha256:b4f2b1a616e099961f6ab61a5a2f88de62082c26801699e556927d21c00737ef", size = 160766, upload-time = "2026-01-27T21:15:41.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/3f/41b426ed9ab161d630edec84bacb6664ae62b6e63af1165919c7e11c17d1/posthog-7.7.0-py3-none-any.whl", hash = "sha256:955f42097bf147459653b9102e5f7f9a22e4b6fc9f15003447bd1137fafbc505", size = 185353, upload-time = "2026-01-27T21:15:40.051Z" }, +] + +[[package]] +name = "posthog" +version = "7.9.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "backoff", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "distro", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "requests", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "six", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/92ec2f7e598a969d3f58cad96c187fbf3d1b38b4b0d1e05c403054553dae/posthog-7.9.6.tar.gz", hash = "sha256:4e0ecb63885ce522d6c7ad4593871771995931764ae83914c364db0ad5de2bbf", size = 175454, upload-time = "2026-03-02T21:29:01.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/5b/3ece09ecbbbfb2f783e510b54d7170c1322a93bd404aa9b923a84827b5fa/posthog-7.9.6-py3-none-any.whl", hash = "sha256:b1ceda033c9a6660c5d21e2b1c0b4113aaa0969ff02914bf23942c99f602b0f7", size = 201145, upload-time = "2026-03-02T21:29:00.136Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "prettytable" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/45/b0847d88d6cfeb4413566738c8bbf1e1995fad3d42515327ff32cc1eb578/prettytable-3.17.0.tar.gz", hash = "sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0", size = 67892, upload-time = "2025-11-14T17:33:20.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl", hash = "sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287", size = 34433, upload-time = "2025-11-14T17:33:19.093Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/02/8832cde80e7380c600fbf55090b6ab7b62bd6825dbedde6d6657c15a1f8e/proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", size = 56929, upload-time = "2026-02-02T17:34:49.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc", size = 50480, upload-time = "2026-02-02T17:34:47.339Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pyarrow" +version = "23.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, + { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" }, + { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" }, + { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" }, + { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" }, + { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" }, + { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "genai-prices" }, + { name = "griffelib" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/97/d57ee44976c349658ea7c645c5c2e1a26830e4b60fdeeee2669d4aaef6eb/pydantic_ai_slim-1.70.0.tar.gz", hash = "sha256:3df0c0e92f72c35e546d24795bce1f4d38f81da2d10addd2e9f255b2d2c83c91", size = 445474, upload-time = "2026-03-18T04:24:34.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/8c/8545d28d0b3a9957aa21393cfdab8280bb854362360b296cd486ed1713ec/pydantic_ai_slim-1.70.0-py3-none-any.whl", hash = "sha256:162907092a562b3160d9ef0418d317ec941c5c0e6dd6e0aa0dbb53b5a5cd3450", size = 576244, upload-time = "2026-03-18T04:24:27.301Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/27/f7a71ca2a3705e7c24fd777959cf5515646cc5f23b5b16c886a2ed373340/pydantic_graph-1.70.0.tar.gz", hash = "sha256:3f76d9137369ef8748b0e8a6df1a08262118af20a32bc139d23e5c0509c6b711", size = 58578, upload-time = "2026-03-18T04:24:37.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fd/19c42b60c37dfdbbf5b76c7b218e8309b43dac501f7aaf2025527ca05023/pydantic_graph-1.70.0-py3-none-any.whl", hash = "sha256:6083c1503a2587990ee1b8a15915106e3ddabc8f3f11fbc4a108a7d7496af4a5", size = 72351, upload-time = "2026-03-18T04:24:30.291Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pyee" +version = "13.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyobjc" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-accessibility", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-accounts", marker = "platform_release >= '12.0'" }, + { name = "pyobjc-framework-addressbook" }, + { name = "pyobjc-framework-adservices", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-adsupport", marker = "platform_release >= '18.0'" }, + { name = "pyobjc-framework-applescriptkit" }, + { name = "pyobjc-framework-applescriptobjc", marker = "platform_release >= '10.0'" }, + { name = "pyobjc-framework-applicationservices" }, + { name = "pyobjc-framework-apptrackingtransparency", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-arkit", marker = "platform_release >= '25.0'" }, + { name = "pyobjc-framework-audiovideobridging", marker = "platform_release >= '12.0'" }, + { name = "pyobjc-framework-authenticationservices", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-automaticassessmentconfiguration", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-automator" }, + { name = "pyobjc-framework-avfoundation", marker = "platform_release >= '11.0'" }, + { name = "pyobjc-framework-avkit", marker = "platform_release >= '13.0'" }, + { name = "pyobjc-framework-avrouting", marker = "platform_release >= '22.0'" }, + { name = "pyobjc-framework-backgroundassets", marker = "platform_release >= '22.0'" }, + { name = "pyobjc-framework-browserenginekit", marker = "platform_release >= '23.4'" }, + { name = "pyobjc-framework-businesschat", marker = "platform_release >= '18.0'" }, + { name = "pyobjc-framework-calendarstore", marker = "platform_release >= '9.0'" }, + { name = "pyobjc-framework-callkit", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-carbon" }, + { name = "pyobjc-framework-cfnetwork" }, + { name = "pyobjc-framework-cinematic", marker = "platform_release >= '23.0'" }, + { name = "pyobjc-framework-classkit", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-cloudkit", marker = "platform_release >= '14.0'" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-collaboration", marker = "platform_release >= '9.0'" }, + { name = "pyobjc-framework-colorsync", marker = "platform_release >= '17.0'" }, + { name = "pyobjc-framework-compositorservices", marker = "platform_release >= '25.0'" }, + { name = "pyobjc-framework-contacts", marker = "platform_release >= '15.0'" }, + { name = "pyobjc-framework-contactsui", marker = "platform_release >= '15.0'" }, + { name = "pyobjc-framework-coreaudio" }, + { name = "pyobjc-framework-coreaudiokit" }, + { name = "pyobjc-framework-corebluetooth", marker = "platform_release >= '14.0'" }, + { name = "pyobjc-framework-coredata" }, + { name = "pyobjc-framework-corehaptics", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-corelocation", marker = "platform_release >= '10.0'" }, + { name = "pyobjc-framework-coremedia", marker = "platform_release >= '11.0'" }, + { name = "pyobjc-framework-coremediaio", marker = "platform_release >= '11.0'" }, + { name = "pyobjc-framework-coremidi" }, + { name = "pyobjc-framework-coreml", marker = "platform_release >= '17.0'" }, + { name = "pyobjc-framework-coremotion", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-coreservices" }, + { name = "pyobjc-framework-corespotlight", marker = "platform_release >= '17.0'" }, + { name = "pyobjc-framework-coretext" }, + { name = "pyobjc-framework-corewlan", marker = "platform_release >= '10.0'" }, + { name = "pyobjc-framework-cryptotokenkit", marker = "platform_release >= '14.0'" }, + { name = "pyobjc-framework-datadetection", marker = "platform_release >= '21.0'" }, + { name = "pyobjc-framework-devicecheck", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-devicediscoveryextension", marker = "platform_release >= '24.0'" }, + { name = "pyobjc-framework-dictionaryservices", marker = "platform_release >= '9.0'" }, + { name = "pyobjc-framework-discrecording" }, + { name = "pyobjc-framework-discrecordingui" }, + { name = "pyobjc-framework-diskarbitration" }, + { name = "pyobjc-framework-dvdplayback" }, + { name = "pyobjc-framework-eventkit", marker = "platform_release >= '12.0'" }, + { name = "pyobjc-framework-exceptionhandling" }, + { name = "pyobjc-framework-executionpolicy", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-extensionkit", marker = "platform_release >= '22.0'" }, + { name = "pyobjc-framework-externalaccessory", marker = "platform_release >= '17.0'" }, + { name = "pyobjc-framework-fileprovider", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-fileproviderui", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-findersync", marker = "platform_release >= '14.0'" }, + { name = "pyobjc-framework-fsevents", marker = "platform_release >= '9.0'" }, + { name = "pyobjc-framework-fskit", marker = "platform_release >= '24.4'" }, + { name = "pyobjc-framework-gamecenter", marker = "platform_release >= '12.0'" }, + { name = "pyobjc-framework-gamecontroller", marker = "platform_release >= '13.0'" }, + { name = "pyobjc-framework-gamekit", marker = "platform_release >= '12.0'" }, + { name = "pyobjc-framework-gameplaykit", marker = "platform_release >= '15.0'" }, + { name = "pyobjc-framework-gamesave", marker = "platform_release >= '25.0'" }, + { name = "pyobjc-framework-healthkit", marker = "platform_release >= '22.0'" }, + { name = "pyobjc-framework-imagecapturecore", marker = "platform_release >= '10.0'" }, + { name = "pyobjc-framework-inputmethodkit", marker = "platform_release >= '9.0'" }, + { name = "pyobjc-framework-installerplugins" }, + { name = "pyobjc-framework-instantmessage", marker = "platform_release >= '9.0'" }, + { name = "pyobjc-framework-intents", marker = "platform_release >= '16.0'" }, + { name = "pyobjc-framework-intentsui", marker = "platform_release >= '21.0'" }, + { name = "pyobjc-framework-iobluetooth" }, + { name = "pyobjc-framework-iobluetoothui" }, + { name = "pyobjc-framework-iosurface", marker = "platform_release >= '10.0'" }, + { name = "pyobjc-framework-ituneslibrary", marker = "platform_release >= '10.0'" }, + { name = "pyobjc-framework-kernelmanagement", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-latentsemanticmapping" }, + { name = "pyobjc-framework-launchservices" }, + { name = "pyobjc-framework-libdispatch", marker = "platform_release >= '12.0'" }, + { name = "pyobjc-framework-libxpc", marker = "platform_release >= '12.0'" }, + { name = "pyobjc-framework-linkpresentation", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-localauthentication", marker = "platform_release >= '14.0'" }, + { name = "pyobjc-framework-localauthenticationembeddedui", marker = "platform_release >= '21.0'" }, + { name = "pyobjc-framework-mailkit", marker = "platform_release >= '21.0'" }, + { name = "pyobjc-framework-mapkit", marker = "platform_release >= '13.0'" }, + { name = "pyobjc-framework-mediaaccessibility", marker = "platform_release >= '13.0'" }, + { name = "pyobjc-framework-mediaextension", marker = "platform_release >= '24.0'" }, + { name = "pyobjc-framework-medialibrary", marker = "platform_release >= '13.0'" }, + { name = "pyobjc-framework-mediaplayer", marker = "platform_release >= '16.0'" }, + { name = "pyobjc-framework-mediatoolbox", marker = "platform_release >= '13.0'" }, + { name = "pyobjc-framework-metal", marker = "platform_release >= '15.0'" }, + { name = "pyobjc-framework-metalfx", marker = "platform_release >= '22.0'" }, + { name = "pyobjc-framework-metalkit", marker = "platform_release >= '15.0'" }, + { name = "pyobjc-framework-metalperformanceshaders", marker = "platform_release >= '17.0'" }, + { name = "pyobjc-framework-metalperformanceshadersgraph", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-metrickit", marker = "platform_release >= '21.0'" }, + { name = "pyobjc-framework-mlcompute", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-modelio", marker = "platform_release >= '15.0'" }, + { name = "pyobjc-framework-multipeerconnectivity", marker = "platform_release >= '14.0'" }, + { name = "pyobjc-framework-naturallanguage", marker = "platform_release >= '18.0'" }, + { name = "pyobjc-framework-netfs", marker = "platform_release >= '10.0'" }, + { name = "pyobjc-framework-network", marker = "platform_release >= '18.0'" }, + { name = "pyobjc-framework-networkextension", marker = "platform_release >= '15.0'" }, + { name = "pyobjc-framework-notificationcenter", marker = "platform_release >= '14.0'" }, + { name = "pyobjc-framework-opendirectory", marker = "platform_release >= '10.0'" }, + { name = "pyobjc-framework-osakit" }, + { name = "pyobjc-framework-oslog", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-passkit", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-pencilkit", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-phase", marker = "platform_release >= '21.0'" }, + { name = "pyobjc-framework-photos", marker = "platform_release >= '15.0'" }, + { name = "pyobjc-framework-photosui", marker = "platform_release >= '15.0'" }, + { name = "pyobjc-framework-preferencepanes" }, + { name = "pyobjc-framework-pushkit", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-framework-quicklookthumbnailing", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-replaykit", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-safariservices", marker = "platform_release >= '16.0'" }, + { name = "pyobjc-framework-safetykit", marker = "platform_release >= '22.0'" }, + { name = "pyobjc-framework-scenekit", marker = "platform_release >= '11.0'" }, + { name = "pyobjc-framework-screencapturekit", marker = "platform_release >= '21.4'" }, + { name = "pyobjc-framework-screensaver" }, + { name = "pyobjc-framework-screentime", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-scriptingbridge", marker = "platform_release >= '9.0'" }, + { name = "pyobjc-framework-searchkit" }, + { name = "pyobjc-framework-security" }, + { name = "pyobjc-framework-securityfoundation" }, + { name = "pyobjc-framework-securityinterface" }, + { name = "pyobjc-framework-securityui", marker = "platform_release >= '24.4'" }, + { name = "pyobjc-framework-sensitivecontentanalysis", marker = "platform_release >= '23.0'" }, + { name = "pyobjc-framework-servicemanagement", marker = "platform_release >= '10.0'" }, + { name = "pyobjc-framework-sharedwithyou", marker = "platform_release >= '22.0'" }, + { name = "pyobjc-framework-sharedwithyoucore", marker = "platform_release >= '22.0'" }, + { name = "pyobjc-framework-shazamkit", marker = "platform_release >= '21.0'" }, + { name = "pyobjc-framework-social", marker = "platform_release >= '12.0'" }, + { name = "pyobjc-framework-soundanalysis", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-speech", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-spritekit", marker = "platform_release >= '13.0'" }, + { name = "pyobjc-framework-storekit", marker = "platform_release >= '11.0'" }, + { name = "pyobjc-framework-symbols", marker = "platform_release >= '23.0'" }, + { name = "pyobjc-framework-syncservices" }, + { name = "pyobjc-framework-systemconfiguration" }, + { name = "pyobjc-framework-systemextensions", marker = "platform_release >= '19.0'" }, + { name = "pyobjc-framework-threadnetwork", marker = "platform_release >= '22.0'" }, + { name = "pyobjc-framework-uniformtypeidentifiers", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-usernotifications", marker = "platform_release >= '18.0'" }, + { name = "pyobjc-framework-usernotificationsui", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-videosubscriberaccount", marker = "platform_release >= '18.0'" }, + { name = "pyobjc-framework-videotoolbox", marker = "platform_release >= '12.0'" }, + { name = "pyobjc-framework-virtualization", marker = "platform_release >= '20.0'" }, + { name = "pyobjc-framework-vision", marker = "platform_release >= '17.0'" }, + { name = "pyobjc-framework-webkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/06/d77639ba166cc09aed2d32ae204811b47bc5d40e035cdc9bff7fff72ec5f/pyobjc-12.1.tar.gz", hash = "sha256:686d6db3eb3182fac9846b8ce3eedf4c7d2680b21b8b8d6e6df054a17e92a12d", size = 11345, upload-time = "2025-11-14T10:07:28.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/00/1085de7b73abf37ec27ad59f7a1d7a406e6e6da45720bced2e198fdf1ddf/pyobjc-12.1-py3-none-any.whl", hash = "sha256:6f8c36cf87b1159d2ca1aa387ffc3efcd51cc3da13ef47c65f45e6d9fbccc729", size = 4226, upload-time = "2025-11-14T09:30:25.185Z" }, +] + +[[package]] +name = "pyobjc-core" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, +] + +[[package]] +name = "pyobjc-framework-accessibility" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/87/8ca40428d05a668fecc638f2f47dba86054dbdc35351d247f039749de955/pyobjc_framework_accessibility-12.1.tar.gz", hash = "sha256:5ff362c3425edc242d49deec11f5f3e26e565cefb6a2872eda59ab7362149772", size = 29800, upload-time = "2025-11-14T10:08:31.949Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/95/9ea0d1c16316b4b5babf4b0515e9a133ac64269d3ec031f15ee9c7c2a8c1/pyobjc_framework_accessibility-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:537691a0b28fedb8385cd093df069a6e5d7e027629671fc47b50210404eca20b", size = 11335, upload-time = "2025-11-14T09:35:30.81Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/aa9625b1b064f7d3e1bbc0b6b40cf92d1d46c7f798e0b345594d626f5510/pyobjc_framework_accessibility-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:44d872d8a1f9d1569da0590c5a9185d2c02dc2e08e410c84a03aa54ca6e05c2c", size = 11352, upload-time = "2025-11-14T09:35:32.967Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/ff4c720d6140f7a20eaed15d5430af1fc8be372998674b82931993177261/pyobjc_framework_accessibility-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4b9e2079ad0da736ba32a10e63698ff1db9667b5f6342a81220aa86cfa0de8c8", size = 11521, upload-time = "2025-11-14T09:35:35.112Z" }, + { url = "https://files.pythonhosted.org/packages/98/ce/21a076746ada1c03015ce23ee87aa3a3f052885ec386296d4d90c4fb0eb2/pyobjc_framework_accessibility-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0a14c794af7f38d8b59f6d7b03f708e61473a42d4a43663e7a2a6355121d11f7", size = 11414, upload-time = "2025-11-14T09:35:36.92Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a195f213d7bbcd765d216a90904a2104199da734bae81c10da9736ebd55d/pyobjc_framework_accessibility-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:bc517a0eff3989ea98197858fbe4bbb4c673e171f4acbb94dc8cf94415b11e0b", size = 11594, upload-time = "2025-11-14T09:35:38.763Z" }, +] + +[[package]] +name = "pyobjc-framework-accounts" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/10/f6fe336c7624d6753c1f6edac102310ce4434d49b548c479e8e6420d4024/pyobjc_framework_accounts-12.1.tar.gz", hash = "sha256:76d62c5e7b831eb8f4c9ca6abaf79d9ed961dfffe24d89a041fb1de97fe56a3e", size = 15202, upload-time = "2025-11-14T10:08:33.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/70/5f9214250f92fbe2e07f35778875d2771d612f313af2a0e4bacba80af28e/pyobjc_framework_accounts-12.1-py2.py3-none-any.whl", hash = "sha256:e1544ad11a2f889a7aaed649188d0e76d58595a27eec07ca663847a7adb21ae5", size = 5104, upload-time = "2025-11-14T09:35:40.246Z" }, +] + +[[package]] +name = "pyobjc-framework-addressbook" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/28/0404af2a1c6fa8fd266df26fb6196a8f3fb500d6fe3dab94701949247bea/pyobjc_framework_addressbook-12.1.tar.gz", hash = "sha256:c48b740cf981103cef1743d0804a226d86481fcb839bd84b80e9a586187e8000", size = 44359, upload-time = "2025-11-14T10:08:37.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/33/da709c69cbb60df9522cd614d5c23c15b649b72e5d62fed1048e75c70e7b/pyobjc_framework_addressbook-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7893dd784322f4674299fb3ca40cb03385e5eddb78defd38f08c0b730813b56c", size = 12894, upload-time = "2025-11-14T09:35:47.498Z" }, + { url = "https://files.pythonhosted.org/packages/62/eb/de0d539bbf31685050dd9fe8894bd2dbc1632bf5311fc74c2c3c46ce61d0/pyobjc_framework_addressbook-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f03312faeb3c381e040f965b288379468d567b1449c1cfe66d150885b48510a3", size = 12910, upload-time = "2025-11-14T09:35:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/e7/59/720da201349f67bca9e6b577fea1a8a3344e88a6527c48933be898c9559d/pyobjc_framework_addressbook-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3b6931f78e01a215df3d9a27d1a10aab04659e636b0836ac448f8dd7fc56a581", size = 13064, upload-time = "2025-11-14T09:35:51.664Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/7a0648f3b56f16eab76e349e873f21cc5d33864d9915bb33ade9a100d1c0/pyobjc_framework_addressbook-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e4e24094fa293f158ed21fcd57414b759dc1220c23efec4ee8a7672d726b3576", size = 12968, upload-time = "2025-11-14T09:35:53.639Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e1/96093b6180e6af5f98b04de159f30d2d0cdde4caac1967f371ccbea662f2/pyobjc_framework_addressbook-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:184bc73e38bd062dce1eb97eb2f14be322f2421daf78efe2747aedb886d93eb0", size = 13132, upload-time = "2025-11-14T09:35:55.947Z" }, +] + +[[package]] +name = "pyobjc-framework-adservices" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/04/1c3d3e0a1ac981664f30b33407dcdf8956046ecde6abc88832cf2aa535f4/pyobjc_framework_adservices-12.1.tar.gz", hash = "sha256:7a31fc8d5c6fd58f012db87c89ba581361fc905114bfb912e0a3a87475c02183", size = 11793, upload-time = "2025-11-14T10:08:39.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/13/f7796469b25f50750299c4b0e95dc2f75c7c7fc4c93ef2c644f947f10529/pyobjc_framework_adservices-12.1-py2.py3-none-any.whl", hash = "sha256:9ca3c55e35b2abb3149a0bce5de9a1f7e8ee4f8642036910ca8586ab2e161538", size = 3492, upload-time = "2025-11-14T09:35:57.344Z" }, +] + +[[package]] +name = "pyobjc-framework-adsupport" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/77/f26a2e9994d4df32e9b3680c8014e350b0f1c78d7673b3eba9de2e04816f/pyobjc_framework_adsupport-12.1.tar.gz", hash = "sha256:9a68480e76de567c339dca29a8c739d6d7b5cad30e1cd585ff6e49ec2fc283dd", size = 11645, upload-time = "2025-11-14T10:08:41.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/1a/3e90d5a09953bde7b60946cd09cca1411aed05dea855cb88cb9e944c7006/pyobjc_framework_adsupport-12.1-py2.py3-none-any.whl", hash = "sha256:97dcd8799dd61f047bb2eb788bbde81f86e95241b5e5173a3a61cfc05b5598b1", size = 3401, upload-time = "2025-11-14T09:35:59.039Z" }, +] + +[[package]] +name = "pyobjc-framework-applescriptkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/f1/e0c07b2a9eb98f1a2050f153d287a52a92f873eeddb41b74c52c144d8767/pyobjc_framework_applescriptkit-12.1.tar.gz", hash = "sha256:cb09f88cf0ad9753dedc02720065818f854b50e33eb4194f0ea34de6d7a3eb33", size = 11451, upload-time = "2025-11-14T10:08:43.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/70/6c399c6ebc37a4e48acf63967e0a916878aedfe420531f6d739215184c0c/pyobjc_framework_applescriptkit-12.1-py2.py3-none-any.whl", hash = "sha256:b955fc017b524027f635d92a8a45a5fd9fbae898f3e03de16ecd94aa4c4db987", size = 4352, upload-time = "2025-11-14T09:36:00.705Z" }, +] + +[[package]] +name = "pyobjc-framework-applescriptobjc" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/4b/e4d1592207cbe17355e01828bdd11dd58f31356108f6a49f5e0484a5df50/pyobjc_framework_applescriptobjc-12.1.tar.gz", hash = "sha256:dce080ed07409b0dda2fee75d559bd312ea1ef0243a4338606440f282a6a0f5f", size = 11588, upload-time = "2025-11-14T10:08:45.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/5f/9ce6706399706930eb29c5308037109c30cfb36f943a6df66fdf38cc842a/pyobjc_framework_applescriptobjc-12.1-py2.py3-none-any.whl", hash = "sha256:79068f982cc22471712ce808c0a8fd5deea11258fc8d8c61968a84b1962a3d10", size = 4454, upload-time = "2025-11-14T09:36:02.276Z" }, +] + +[[package]] +name = "pyobjc-framework-applicationservices" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coretext" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247, upload-time = "2025-11-14T10:08:52.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/a7/55fa88def5c02732c4b747606ff1cbce6e1f890734bbd00f5596b21eaa02/pyobjc_framework_applicationservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c8f6e2fb3b3e9214ab4864ef04eee18f592b46a986c86ea0113448b310520532", size = 32835, upload-time = "2025-11-14T09:36:11.855Z" }, + { url = "https://files.pythonhosted.org/packages/fc/21/79e42ee836f1010f5fe9e97d2817a006736bd287c15a3674c399190a2e77/pyobjc_framework_applicationservices-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bd1f4dbb38234a24ae6819f5e22485cf7dd3dd4074ff3bf9a9fdb4c01a3b4a38", size = 32859, upload-time = "2025-11-14T09:36:15.208Z" }, + { url = "https://files.pythonhosted.org/packages/66/3a/0f1d4dcf2345e875e5ea9761d5a70969e241d24089133d21f008dde596f5/pyobjc_framework_applicationservices-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8a5d2845249b6a85ba9e320a9848468c3f8cd6f59605a9a43f406a7810eaa830", size = 33115, upload-time = "2025-11-14T09:36:18.384Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/3196b40fec68b4413c92875311f17ccf4c3ff7d2e53676f8fc18ad29bd18/pyobjc_framework_applicationservices-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f43c9a24ad97a9121276d4d571aa04a924282c80d7291cfb3b29839c3e2013a8", size = 32997, upload-time = "2025-11-14T09:36:21.58Z" }, + { url = "https://files.pythonhosted.org/packages/fd/bb/dab21d2210d3ef7dd0616df7e8ea89b5d8d62444133a25f76e649a947168/pyobjc_framework_applicationservices-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1f72e20009a4ebfd5ed5b23dc11c1528ad6b55cc63ee71952ddb2a5e5f1cb7da", size = 33238, upload-time = "2025-11-14T09:36:24.751Z" }, +] + +[[package]] +name = "pyobjc-framework-apptrackingtransparency" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/de/f24348982ecab0cb13067c348fc5fbc882c60d704ca290bada9a2b3e594b/pyobjc_framework_apptrackingtransparency-12.1.tar.gz", hash = "sha256:e25bf4e4dfa2d929993ee8e852b28fdf332fa6cde0a33328fdc3b2f502fa50ec", size = 12407, upload-time = "2025-11-14T10:08:54.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/b2/90120b93ecfb099b6af21696c26356ad0f2182bdef72b6cba28aa6472ca6/pyobjc_framework_apptrackingtransparency-12.1-py2.py3-none-any.whl", hash = "sha256:23a98ade55495f2f992ecf62c3cbd8f648cbd68ba5539c3f795bf66de82e37ca", size = 3879, upload-time = "2025-11-14T09:36:26.425Z" }, +] + +[[package]] +name = "pyobjc-framework-arkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/8b/843fe08e696bca8e7fc129344965ab6280f8336f64f01ba0a8862d219c3f/pyobjc_framework_arkit-12.1.tar.gz", hash = "sha256:0c5c6b702926179700b68ba29b8247464c3b609fd002a07a3308e72cfa953adf", size = 35814, upload-time = "2025-11-14T10:08:57.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/1e/64c55b409243b3eb9abc7a99e7b27ad4e16b9e74bc4b507fb7e7b81fd41a/pyobjc_framework_arkit-12.1-py2.py3-none-any.whl", hash = "sha256:f6d39e28d858ee03f052d6780a552247e682204382dbc090f1d3192fa1b21493", size = 8302, upload-time = "2025-11-14T09:36:28.127Z" }, +] + +[[package]] +name = "pyobjc-framework-audiovideobridging" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/51/f81581e7a3c5cb6c9254c6f1e1ee1d614930493761dec491b5b0d49544b9/pyobjc_framework_audiovideobridging-12.1.tar.gz", hash = "sha256:6230ace6bec1f38e8a727c35d054a7be54e039b3053f98e6dd8d08d6baee2625", size = 38457, upload-time = "2025-11-14T10:09:01.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/8e/a28badfcc6c731696e3d3a8a83927bd844d992f9152f903c2fee355702ca/pyobjc_framework_audiovideobridging-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:010021502649e2cca4e999a7c09358d48c6b0ed83530bbc0b85bba6834340e4b", size = 11052, upload-time = "2025-11-14T09:36:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/d6436115ebb623dbc14283f5e76577245fa6460995e9f7981e79e97003d3/pyobjc_framework_audiovideobridging-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9901a88b6c8dbc982d8605c6b1ff0330ff80647a0a96a8187b6784249eb42dc", size = 11065, upload-time = "2025-11-14T09:36:36.69Z" }, + { url = "https://files.pythonhosted.org/packages/97/ca/d6740b0f666dca9fc28d4e08358a7a2fffaf879cf9c49d2c99c470b83ef8/pyobjc_framework_audiovideobridging-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0c57fdf1762f616d10549c0eddf84e59c193800f4a7932aaa7d5f13c123609c0", size = 11239, upload-time = "2025-11-14T09:36:38.992Z" }, + { url = "https://files.pythonhosted.org/packages/98/9a/f4b435523c297cdf25bfe0d0a8bb25ae0d3fa19813c2365cf1e93f462948/pyobjc_framework_audiovideobridging-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:88f97bf62cba0d07f623650a7b2a58f73aedcc03b523e2bcd5653042dd50c152", size = 11130, upload-time = "2025-11-14T09:36:40.918Z" }, + { url = "https://files.pythonhosted.org/packages/da/96/33c5aec0940ff3f81ad11b3a154d3cae94803d48376f1436392c4484b6ff/pyobjc_framework_audiovideobridging-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:84d466e0c2fbf466fd5ca9209139e321ddf3f96bbd987308c73bb4a243ab80b2", size = 11302, upload-time = "2025-11-14T09:36:42.734Z" }, +] + +[[package]] +name = "pyobjc-framework-authenticationservices" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/18/86218de3bf67fc1d810065f353d9df70c740de567ebee8550d476cb23862/pyobjc_framework_authenticationservices-12.1.tar.gz", hash = "sha256:cef71faeae2559f5c0ff9a81c9ceea1c81108e2f4ec7de52a98c269feff7a4b6", size = 58683, upload-time = "2025-11-14T10:09:06.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/1d/e9f296fe1ee9a074ff6c45ce9eb109fc3b45696de000f373265c8e42fd47/pyobjc_framework_authenticationservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6fd5ce10fe5359cbbfe03eb12cab3e01992b32ab65653c579b00ac93cf674985", size = 20738, upload-time = "2025-11-14T09:36:51.094Z" }, + { url = "https://files.pythonhosted.org/packages/23/2f/7016b3ca344b079932abe56d7d6216c88cac715d81ca687753aed4b749f7/pyobjc_framework_authenticationservices-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4491a2352cd53a38c7d057d674b1aa40d05eddb8dd7a1a2f415d9f2858b52d40", size = 20746, upload-time = "2025-11-14T09:36:53.762Z" }, + { url = "https://files.pythonhosted.org/packages/5b/63/f2d1137e542b2badb5803e01628a61e9df8853b773513a6a066524c77903/pyobjc_framework_authenticationservices-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a3957039eae3a82ada418ee475a347619e42ba10c45a57cd6ca83b1a0e61c2ad", size = 20994, upload-time = "2025-11-14T09:36:56.153Z" }, + { url = "https://files.pythonhosted.org/packages/a2/93/13232a82318153ec392a46c0f674baeb64ce0aaab05683d4c129ac0fafec/pyobjc_framework_authenticationservices-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:3ee69de818ce91c3bea6f87deba59ab8392a2c17c48f3d6fce0639c0e548bb0c", size = 20753, upload-time = "2025-11-14T09:36:59.075Z" }, + { url = "https://files.pythonhosted.org/packages/d3/95/c941a19224a132b206948e1d329a1e708e41e013ef0d316162af7cfc54c6/pyobjc_framework_authenticationservices-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b14997d96887127f393434d42e3e108eeca2116ca935dd7e37e91c709a93b422", size = 21032, upload-time = "2025-11-14T09:37:01.358Z" }, +] + +[[package]] +name = "pyobjc-framework-automaticassessmentconfiguration" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/24/080afe8189c47c4bb3daa191ccfd962400ca31a67c14b0f7c2d002c2e249/pyobjc_framework_automaticassessmentconfiguration-12.1.tar.gz", hash = "sha256:2b732c02d9097682ca16e48f5d3b10056b740bc091e217ee4d5715194c8970b1", size = 21895, upload-time = "2025-11-14T10:09:08.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/b2/fbec3d649bf275d7a9604e5f56015be02ef8dcf002f4ae4d760436b8e222/pyobjc_framework_automaticassessmentconfiguration-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c2e22ea67d7e6d6a84d968169f83d92b59857a49ab12132de07345adbfea8a62", size = 9332, upload-time = "2025-11-14T09:37:07.083Z" }, + { url = "https://files.pythonhosted.org/packages/52/85/42cf8718bbfef47e67228a39d4f25b86b6fa9676f5ca5904af21ae42ad43/pyobjc_framework_automaticassessmentconfiguration-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:467739e70ddbc259bf453056cc9ce4ed96de8e6aad8122fa4035d2e6ecf9fc9c", size = 9344, upload-time = "2025-11-14T09:37:09.02Z" }, + { url = "https://files.pythonhosted.org/packages/09/ec/a889dd812adfa446238853cf3cf6a7a2691e3096247a7ef75970d135e5bb/pyobjc_framework_automaticassessmentconfiguration-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b4ea4b00f70bf242a5d8ce9c420987239dbc74285588c141ac1e0d6bd71fcd4c", size = 9501, upload-time = "2025-11-14T09:37:10.684Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/b7a59d77cf0f3dfe8676ecd0ab22dca215df11a0f1623cb0dbac29bb30d2/pyobjc_framework_automaticassessmentconfiguration-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f5f1818c6f77daf64d954878bbbda6b3f5e41e23b599210da08fefed1f1d5981", size = 9392, upload-time = "2025-11-14T09:37:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b4/bc5de9b5cce1d243823b283e0942bb353f72998c01688fb3b3da9061a731/pyobjc_framework_automaticassessmentconfiguration-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2e84dee31c3cb7dda4cded047f8b2080378da5c13e8682e45852be5e34b647ed", size = 9541, upload-time = "2025-11-14T09:37:14.358Z" }, +] + +[[package]] +name = "pyobjc-framework-automator" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/08/362bf6ac2bba393c46cf56078d4578b692b56857c385e47690637a72f0dd/pyobjc_framework_automator-12.1.tar.gz", hash = "sha256:7491a99347bb30da3a3f744052a03434ee29bee3e2ae520576f7e796740e4ba7", size = 186068, upload-time = "2025-11-14T10:09:20.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/36/2e8c36ddf20d501f9d344ed694e39021190faffc44b596f3a430bf437174/pyobjc_framework_automator-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4df9aec77f0fbca66cd3534d1b8398fe6f3e3c2748c0fc12fec2546c7f2e3ffd", size = 10034, upload-time = "2025-11-14T09:37:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/1f/cd/666e44c8deb41e5c9dc5930abf8379edd80bff14eb4d0a56380cdbbbbf9a/pyobjc_framework_automator-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cdda7b8c48c0f8e15cbb97600ac848fd76cf9837ca3353286a7c02281e9c17a3", size = 10045, upload-time = "2025-11-14T09:37:22.179Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/75fa03ad8673336689bd663ba153b378e070f159122d8478deb0940039c0/pyobjc_framework_automator-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e9962ea45875fda6a648449015ccc26cc1229fdbd0166556a7271c60ba6d9011", size = 10192, upload-time = "2025-11-14T09:37:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/c6/be/97fcdb60072f443ec360d2aa07e45469125eed57e0158d50f00ef5431240/pyobjc_framework_automator-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fb6a177cac056f2ecacaae1d4815f4e10529025cb13184fdee297989b55846f7", size = 10092, upload-time = "2025-11-14T09:37:26.574Z" }, + { url = "https://files.pythonhosted.org/packages/06/7b/af089d11c6bdc9773e4e0f68b1beabe523d663290080e6ec2e853226a8bb/pyobjc_framework_automator-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:275ed04d339c5a5849a4be8ef82c2035be07ab92ccbf69007f544bcfabe060ad", size = 10240, upload-time = "2025-11-14T09:37:28.232Z" }, +] + +[[package]] +name = "pyobjc-framework-avfoundation" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coreaudio" }, + { name = "pyobjc-framework-coremedia" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/42/c026ab308edc2ed5582d8b4b93da6b15d1b6557c0086914a4aabedd1f032/pyobjc_framework_avfoundation-12.1.tar.gz", hash = "sha256:eda0bb60be380f9ba2344600c4231dd58a3efafa99fdc65d3673ecfbb83f6fcb", size = 310047, upload-time = "2025-11-14T10:09:40.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/00/ca471e5dd33f040f69320832e45415d00440260bf7f8221a9df4c4662659/pyobjc_framework_avfoundation-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bf634f89265b4d93126153200d885b6de4859ed6b3bc65e69ff75540bc398406", size = 83375, upload-time = "2025-11-14T09:37:47.262Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d4/ade88067deff45858b457648dd82c9363977eb1915efd257232cd06bdac1/pyobjc_framework_avfoundation-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f8ac7f7e0884ac8f12009cdb9d4fefc2f269294ab2ccfd84520a560859b69cec", size = 83413, upload-time = "2025-11-14T09:37:53.759Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3a/fa699d748d6351fa0aeca656ea2f9eacc36e31203dfa56bc13c8a3d26d7d/pyobjc_framework_avfoundation-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:51aba2c6816badfb1fb5a2de1b68b33a23f065bf9e3b99d46ede0c8c774ac7a4", size = 83860, upload-time = "2025-11-14T09:38:00.051Z" }, + { url = "https://files.pythonhosted.org/packages/0c/65/a79cf3b8935a78329ac1107056b91868a581096a90ab6ddff5fd28db4947/pyobjc_framework_avfoundation-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9a3ffd1ae90bd72dbcf2875aa9254369e805b904140362a7338ebf1af54201a6", size = 83629, upload-time = "2025-11-14T09:38:06.697Z" }, + { url = "https://files.pythonhosted.org/packages/8a/03/4125204a17cd7b4de1fdfc38b280a47d0d8f8691a4ee306ebb41b58ff030/pyobjc_framework_avfoundation-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:394c99876b9a38db4851ddf8146db363556895c12e9c711ccd3c3f907ac8e273", size = 83962, upload-time = "2025-11-14T09:38:13.153Z" }, +] + +[[package]] +name = "pyobjc-framework-avkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/a9/e44db1a1f26e2882c140f1d502d508b1f240af9048909dcf1e1a687375b4/pyobjc_framework_avkit-12.1.tar.gz", hash = "sha256:a5c0ddb0cb700f9b09c8afeca2c58952d554139e9bb078236d2355b1fddfb588", size = 28473, upload-time = "2025-11-14T10:09:43.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/34/e77b18f7ed0bd707afd388702e910bdf2d0acee39d1139e8619c916d3eb4/pyobjc_framework_avkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eef2c0a51465de025a4509db05ef18ca2b678bb00ee0a8fbad7fd470edfd58f9", size = 11613, upload-time = "2025-11-14T09:38:19.78Z" }, + { url = "https://files.pythonhosted.org/packages/11/f2/4a55fdc8baca23dd315dab39479203396db54468a4c5a3e2480748ac68af/pyobjc_framework_avkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c0241548fc7ca3fcd335da05c3dd15d7314fe58debd792317a725d8ae9cf90fa", size = 11620, upload-time = "2025-11-14T09:38:21.904Z" }, + { url = "https://files.pythonhosted.org/packages/d7/37/76d67c86db80f13f0746b493ae025482cb407b875f3138fc6a6e1fd3d5e3/pyobjc_framework_avkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:869fd54ccdac097abe36d7d4ef8945c80b9c886d881173f590b382f6c743ff12", size = 11824, upload-time = "2025-11-14T09:38:23.777Z" }, + { url = "https://files.pythonhosted.org/packages/29/4e/bd28968f538f5b4f806431c782556aaa5c17567c83edb6df0ef83c7a26ca/pyobjc_framework_avkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f49ee90e4f8737ae5dea7579016cdf344b64092810bf5b5acf0cb9c1c6a0d328", size = 11614, upload-time = "2025-11-14T09:38:25.919Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e7/3efb6c782d09abedb74fdecdb374c0b16ccdb43b8da55f47953a4cacf3a6/pyobjc_framework_avkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:19d46d8da214d8fad03f0a8edd384762dea55933c0c094425a34ac6e53eacb71", size = 11827, upload-time = "2025-11-14T09:38:27.716Z" }, +] + +[[package]] +name = "pyobjc-framework-avrouting" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/83/15bf6c28ec100dae7f92d37c9e117b3b4ee6b4873db062833e16f1cfd6c4/pyobjc_framework_avrouting-12.1.tar.gz", hash = "sha256:6a6c5e583d14f6501df530a9d0559a32269a821fc8140e3646015f097155cd1c", size = 20031, upload-time = "2025-11-14T10:09:45.701Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/54/fa24f666525c1332a11b2de959c9877b0fe08f00f29ecf96964b24246c13/pyobjc_framework_avrouting-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c0fb0d3d260527320377a70c87688ca5e4a208b09fddcae2b4257d7fe9b1e18", size = 8450, upload-time = "2025-11-14T09:38:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a4/cdbbe5745a49c9c5f5503dbbdd1b90084d4be83bd8503c998db160bb378e/pyobjc_framework_avrouting-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:18c62af1ce9ac99b04c36f66959ca64530d51b62aa0e6f00400dea600112e370", size = 8465, upload-time = "2025-11-14T09:38:37.638Z" }, + { url = "https://files.pythonhosted.org/packages/29/d7/c709d277e872495f452fe797c619d9b202cd388b655ccf7196724dbbb600/pyobjc_framework_avrouting-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e5a1d2e4e431aae815e38b75dbe644aa1fd495f8ec1e2194fc175132d7cfc1d3", size = 8630, upload-time = "2025-11-14T09:38:39.284Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0a/9e9bf48c70f129c1fa42e84e091901b6aa6d11074365d93aa22a42d13ba6/pyobjc_framework_avrouting-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:defaad8e98793dfaceb7e36eba3da9bf92d0840207d39e39b018ce6eb41d80f8", size = 8525, upload-time = "2025-11-14T09:38:41.001Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/56ab32b061b4a51f661998ef96ca91a34aee86527e6a4d5f4f10db906066/pyobjc_framework_avrouting-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c5f80ba96f5f874193fc0d9656aa6b4ed0df43c7c88ecfbf6cd4760d75776157", size = 8687, upload-time = "2025-11-14T09:38:43.215Z" }, +] + +[[package]] +name = "pyobjc-framework-backgroundassets" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/d1/e917fba82790495152fd3508c5053827658881cf7e9887ba60def5e3f221/pyobjc_framework_backgroundassets-12.1.tar.gz", hash = "sha256:8da34df9ae4519c360c429415477fdaf3fbba5addbc647b3340b8783454eb419", size = 26210, upload-time = "2025-11-14T10:09:48.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/34/bbba61f0e8ecb0fe0da7aa2c9ea15f7cb0dca2fb2914fcdcd77b782b5c11/pyobjc_framework_backgroundassets-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2c11cb98650c1a4bc68eeb4b040541ba96613434c5957e98e9bb363413b23c91", size = 10786, upload-time = "2025-11-14T09:38:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/04/9b/872f9ff0593ffb9dbc029dc775390b0e45fe3278068b28aade8060503003/pyobjc_framework_backgroundassets-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a089a71b2db471f5af703e35f7a61060164d61eb60a3f482076826dfa5697c7c", size = 10803, upload-time = "2025-11-14T09:38:49.996Z" }, + { url = "https://files.pythonhosted.org/packages/cc/44/4afc2e8bcf16919b1ab82eaf88067469ea255b0a3390d353fec1002dbd0a/pyobjc_framework_backgroundassets-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e8c560f1aaa7a4bf6e336806749ce0a20f2a792ab924d9424714e299a59b3edf", size = 11058, upload-time = "2025-11-14T09:38:51.743Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/80cd655122c20fd29edd3b2b609e6be006cef4bdc830d71944399c6abcd5/pyobjc_framework_backgroundassets-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:57d77b1babd450b18e32e852a47dd1095329323e1bed9f258b46c43e20e6d0fc", size = 10854, upload-time = "2025-11-14T09:38:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/11/24/4048476f84c0566c1e146dbbd20a637bda14df5c1e52dc907e23b0329ab2/pyobjc_framework_backgroundassets-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:acaa091ff12acb24536745803af95e10d535b22e2e123fd2dd5920f3d47338ee", size = 11061, upload-time = "2025-11-14T09:38:55.043Z" }, +] + +[[package]] +name = "pyobjc-framework-browserenginekit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coreaudio" }, + { name = "pyobjc-framework-coremedia" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/b9/39f9de1730e6f8e73be0e4f0c6087cd9439cbe11645b8052d22e1fb8e69b/pyobjc_framework_browserenginekit-12.1.tar.gz", hash = "sha256:6a1a34a155778ab55ab5f463e885f2a3b4680231264e1fe078e62ddeccce49ed", size = 29120, upload-time = "2025-11-14T10:09:51.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e0/8d2cebbfcfd6aacb805ae0ae7ba931f6a39140540b2e1e96719e7be28359/pyobjc_framework_browserenginekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d15766bb841b081447015c9626e2a766febfe651f487893d29c5d72bef976b94", size = 11545, upload-time = "2025-11-14T09:39:00.988Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2c/d39ab696b0316e1faf112a3aee24ef3bcb5fb42eb5db18ba2d74264a41a8/pyobjc_framework_browserenginekit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1aa2da131bbdf81748894c18d253cd2711dc535f1711263c6c604e20cdc094a6", size = 11567, upload-time = "2025-11-14T09:39:02.811Z" }, + { url = "https://files.pythonhosted.org/packages/0e/dd/624d273beea036ec20e16f8bdaaca6b062da647b785dedf90fa2a92a8cc0/pyobjc_framework_browserenginekit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:657d78bb5c1a51097560cb3219692321640d0d5c8e57e9160765e1ecfb3fe7ef", size = 11738, upload-time = "2025-11-14T09:39:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/13/4d/a340f75fc6daa482d9d3470fe449da0d8e1263a6f77803f2b1185b3a69af/pyobjc_framework_browserenginekit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ad7896751accf7a6f866e64e8155f97b6cf0fc0e6efd64e9940346d8fbf0ec66", size = 11620, upload-time = "2025-11-14T09:39:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fa/5c0278bfebee573d97fd78ee0f41c9e8cb8f7a79ed7e4bd6a8f8ee00abe4/pyobjc_framework_browserenginekit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c52a3b0000e67fbaa51eef0b455d90b1140e3f6a0014945227cedf242fa57dcc", size = 11805, upload-time = "2025-11-14T09:39:09.033Z" }, +] + +[[package]] +name = "pyobjc-framework-businesschat" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/da/bc09b6ed19e9ea38ecca9387c291ca11fa680a8132d82b27030f82551c23/pyobjc_framework_businesschat-12.1.tar.gz", hash = "sha256:f6fa3a8369a1a51363e1757530128741d9d09ed90692a1d6777a4c0fbad25868", size = 12055, upload-time = "2025-11-14T10:09:53.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/88/4c727424b05efa33ed7f6c45e40333e5a8a8dc5bb238e34695addd68463b/pyobjc_framework_businesschat-12.1-py2.py3-none-any.whl", hash = "sha256:f66ce741507b324de3c301d72ba0cfa6aaf7093d7235972332807645c118cc29", size = 3474, upload-time = "2025-11-14T09:39:10.771Z" }, +] + +[[package]] +name = "pyobjc-framework-calendarstore" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/41/ae955d1c44dcc18b5b9df45c679e9a08311a0f853b9d981bca760cf1eef2/pyobjc_framework_calendarstore-12.1.tar.gz", hash = "sha256:f9a798d560a3c99ad4c0d2af68767bc5695d8b1aabef04d8377861cd1d6d1670", size = 52272, upload-time = "2025-11-14T10:09:58.48Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/70/f68aebdb7d3fa2dec2e9da9e9cdaa76d370de326a495917dbcde7bb7711e/pyobjc_framework_calendarstore-12.1-py2.py3-none-any.whl", hash = "sha256:18533e0fcbcdd29ee5884dfbd30606710f65df9b688bf47daee1438ee22e50cc", size = 5285, upload-time = "2025-11-14T09:39:12.473Z" }, +] + +[[package]] +name = "pyobjc-framework-callkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c0/1859d4532d39254df085309aff55b85323576f00a883626325af40da4653/pyobjc_framework_callkit-12.1.tar.gz", hash = "sha256:fd6dc9688b785aab360139d683be56f0844bf68bf5e45d0eb770cb68221083cc", size = 29171, upload-time = "2025-11-14T10:10:01.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/b7/b3a498b14751b4be6af5272c9be9ded718aa850ebf769b052c7d610a142a/pyobjc_framework_callkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:12adc0ace464a057f8908187698e1d417c6c53619797a69d096f4329bffb1089", size = 11334, upload-time = "2025-11-14T09:39:18.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/30/f434921c17a59d8db06783189ca98ccf291d5366be364f96439e987c1b13/pyobjc_framework_callkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b8909402f8690ea2fe8fa7c0256b5c491435f20881832808b86433f526ff28f8", size = 11347, upload-time = "2025-11-14T09:39:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b8/c6a52c3c2e1e0bd23a84fef0d2cb089c456d62add59f87d8510ffe871068/pyobjc_framework_callkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9ec6635b6a6fecde6e5252ceff76c71d699ed8e0f3ebc6fd220a351dc653040b", size = 11558, upload-time = "2025-11-14T09:39:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/e3/db/e8bcdde2b9cf109ebdf389e730900de7acf792664aa0a7fbc630cd61a82a/pyobjc_framework_callkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a2438a252ff428bca1c1d1db2fca921d2cc572ee5c582f000a713fb61b29324f", size = 11333, upload-time = "2025-11-14T09:39:24.326Z" }, + { url = "https://files.pythonhosted.org/packages/2b/14/4bb4718a4dab3040c23d91c01283ae46cbfd4b709692ef98dae92e4a3247/pyobjc_framework_callkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b6a1767e7391652ef75eb46d12d49f31f591063da45357aad2c4e0d40f8fe702", size = 11556, upload-time = "2025-11-14T09:39:26.174Z" }, +] + +[[package]] +name = "pyobjc-framework-carbon" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/0f/9ab8e518a4e5ac4a1e2fdde38a054c32aef82787ff7f30927345c18b7765/pyobjc_framework_carbon-12.1.tar.gz", hash = "sha256:57a72807db252d5746caccc46da4bd20ff8ea9e82109af9f72735579645ff4f0", size = 37293, upload-time = "2025-11-14T10:10:04.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/9e/91853c8f98b9d5bccf464113908620c94cc12c2a3e4625f3ce172e3ea4bc/pyobjc_framework_carbon-12.1-py2.py3-none-any.whl", hash = "sha256:f8b719b3c7c5cf1d61ac7c45a8a70b5e5e5a83fa02f5194c2a48a7e81a3d1b7f", size = 4625, upload-time = "2025-11-14T09:39:27.937Z" }, +] + +[[package]] +name = "pyobjc-framework-cfnetwork" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/6a/f5f0f191956e187db85312cbffcc41bf863670d121b9190b4a35f0d36403/pyobjc_framework_cfnetwork-12.1.tar.gz", hash = "sha256:2d16e820f2d43522c793f55833fda89888139d7a84ca5758548ba1f3a325a88d", size = 44383, upload-time = "2025-11-14T10:10:08.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/0b/28034e63f3a25b30ede814469c3f57d44268cbced19664c84a8664200f9d/pyobjc_framework_cfnetwork-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:92760da248c757085fc39bce4388a0f6f0b67540e51edf60a92ad60ca907d071", size = 19135, upload-time = "2025-11-14T09:39:36.382Z" }, + { url = "https://files.pythonhosted.org/packages/f4/36/d6b95a5b156de5e2c071ecb7f7056f0badb3a0d09e0dbcf0d8d35743f822/pyobjc_framework_cfnetwork-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86cc3f650d3169cd8ce4a1438219aa750accac0efc29539920ab0a7e75e25ab4", size = 19135, upload-time = "2025-11-14T09:39:39.95Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/ff66133af4592e123320337f443aa6e36993cc48d6c10f6e7436e01678b1/pyobjc_framework_cfnetwork-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5ff3e246e5186b9bad23b2e4e856ca87eaa9329f5904643c5484510059a07e24", size = 19412, upload-time = "2025-11-14T09:39:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/931cda003b627cc04c8e5bf9efecc391006305462192414b3d29eb16b5fd/pyobjc_framework_cfnetwork-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b94c190bdfdf0c8f3f6f7bf8e19ccc2847ecb67adab0068f8d12a25ab7df3c1a", size = 19185, upload-time = "2025-11-14T09:39:45.245Z" }, + { url = "https://files.pythonhosted.org/packages/ac/92/5843dd96da7711e72dae489bf91441d91c4dc15f17f34b89b04f2c22aee2/pyobjc_framework_cfnetwork-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8c5313e146d436de05afae2ab203cfa1966f56d34661939629e2b932efd8da1a", size = 19402, upload-time = "2025-11-14T09:39:47.497Z" }, +] + +[[package]] +name = "pyobjc-framework-cinematic" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-avfoundation" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coremedia" }, + { name = "pyobjc-framework-metal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/4e/f4cc7f9f7f66df0290c90fe445f1ff5aa514c6634f5203fe049161053716/pyobjc_framework_cinematic-12.1.tar.gz", hash = "sha256:795068c30447548c0e8614e9c432d4b288b13d5614622ef2f9e3246132329b06", size = 21215, upload-time = "2025-11-14T10:10:10.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/a0/cd85c827ce5535c08d936e5723c16ee49f7ff633f2e9881f4f58bf83e4ce/pyobjc_framework_cinematic-12.1-py2.py3-none-any.whl", hash = "sha256:c003543bb6908379680a93dfd77a44228686b86c118cf3bc930f60241d0cd141", size = 5031, upload-time = "2025-11-14T09:39:49.003Z" }, +] + +[[package]] +name = "pyobjc-framework-classkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/ef/67815278023b344a79c7e95f748f647245d6f5305136fc80615254ad447c/pyobjc_framework_classkit-12.1.tar.gz", hash = "sha256:8d1e9dd75c3d14938ff533d88b72bca2d34918e4461f418ea323bfb2498473b4", size = 26298, upload-time = "2025-11-14T10:10:13.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/5e/cf43c647af872499fc8e80cc6ac6e9ad77d9c77861dc2e62bdd9b01473ce/pyobjc_framework_classkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c027a3cd9be5fee3f605589118b8b278297c384a271f224c1a98b224e0c087e6", size = 8877, upload-time = "2025-11-14T09:39:54.979Z" }, + { url = "https://files.pythonhosted.org/packages/a5/47/f89917b4683a8f61c64d5d30d64ed0a5c1cfd9f0dd9dfb099b3465c73bcf/pyobjc_framework_classkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0ac959a4e91a40865f12f041c083fa8862672f13e596c983f2b99afc8c67bc4e", size = 8890, upload-time = "2025-11-14T09:39:56.65Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/8a0dc753e73001026663fe8556895b23fbf6c238a705bfc86d8ce191eee3/pyobjc_framework_classkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:61fdac9e3bad384b47725587b77f932dbed71d0ae63b749eddfa390791eed4a2", size = 9043, upload-time = "2025-11-14T09:39:58.684Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0b/7f25a43b0820a220a00c4a334d93c36cfa9e4248764054d6f9901eacbbd4/pyobjc_framework_classkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:5d0a5cd026c51a22d13eb75404f8317089aabb3faef723aeafc4ca9a0c17e66e", size = 8952, upload-time = "2025-11-14T09:40:00.405Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/d33b868da5c646e8251521f3e523510eb85b34f329bb9267506d306acbd5/pyobjc_framework_classkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c95cd6a4f598e877197a93cc202d40d0d830bf09be5a2b15942e5a1b03e29cd4", size = 9115, upload-time = "2025-11-14T09:40:02.088Z" }, +] + +[[package]] +name = "pyobjc-framework-cloudkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-accounts" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coredata" }, + { name = "pyobjc-framework-corelocation" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/09/762ee4f3ae8568b8e0e5392c705bc4aa1929aa454646c124ca470f1bf9fc/pyobjc_framework_cloudkit-12.1.tar.gz", hash = "sha256:1dddd38e60863f88adb3d1d37d3b4ccb9cbff48c4ef02ab50e36fa40c2379d2f", size = 53730, upload-time = "2025-11-14T10:10:17.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/71/cbef7179bf1a594558ea27f1e5ad18f5c17ef71a8a24192aae16127bc849/pyobjc_framework_cloudkit-12.1-py2.py3-none-any.whl", hash = "sha256:875e37bf1a2ce3d05c2492692650104f2d908b56b71a0aedf6620bc517c6c9ca", size = 11090, upload-time = "2025-11-14T09:40:04.207Z" }, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, +] + +[[package]] +name = "pyobjc-framework-collaboration" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/21/77fe64b39eae98412de1a0d33e9c735aa9949d53fff6b2d81403572b410b/pyobjc_framework_collaboration-12.1.tar.gz", hash = "sha256:2afa264d3233fc0a03a56789c6fefe655ffd81a2da4ba1dc79ea0c45931ad47b", size = 14299, upload-time = "2025-11-14T10:13:04.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/66/1507de01f1e2b309f8e11553a52769e4e2e9939ed770b5b560ef5bc27bc1/pyobjc_framework_collaboration-12.1-py2.py3-none-any.whl", hash = "sha256:182d6e6080833b97f9bef61738ae7bacb509714538f0d7281e5f0814c804b315", size = 4907, upload-time = "2025-11-14T09:42:55.781Z" }, +] + +[[package]] +name = "pyobjc-framework-colorsync" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/b4/706e4cc9db25b400201fc90f3edfaa1ab2d51b400b19437b043a68532078/pyobjc_framework_colorsync-12.1.tar.gz", hash = "sha256:d69dab7df01245a8c1bd536b9231c97993a5d1a2765d77692ce40ebbe6c1b8e9", size = 25269, upload-time = "2025-11-14T10:13:07.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/e1/82e45c712f43905ee1e6d585180764e8fa6b6f1377feb872f9f03c8c1fb8/pyobjc_framework_colorsync-12.1-py2.py3-none-any.whl", hash = "sha256:41e08d5b9a7af4b380c9adab24c7ff59dfd607b3073ae466693a3e791d8ffdc9", size = 6020, upload-time = "2025-11-14T09:42:57.504Z" }, +] + +[[package]] +name = "pyobjc-framework-compositorservices" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-metal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/c5/0ba31d7af7e464b7f7ece8c2bd09112bdb0b7260848402e79ba6aacc622c/pyobjc_framework_compositorservices-12.1.tar.gz", hash = "sha256:028e357bbee7fbd3723339a321bbe14e6da5a772708a661a13eea5f17c89e4ab", size = 23292, upload-time = "2025-11-14T10:13:10.392Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/34/5a2de8d531dbb88023898e0b5d2ce8edee14751af6c70e6103f6aa31a669/pyobjc_framework_compositorservices-12.1-py2.py3-none-any.whl", hash = "sha256:9ef22d4eacd492e13099b9b8936db892cdbbef1e3d23c3484e0ed749f83c4984", size = 5910, upload-time = "2025-11-14T09:42:59.154Z" }, +] + +[[package]] +name = "pyobjc-framework-contacts" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/a0/ce0542d211d4ea02f5cbcf72ee0a16b66b0d477a4ba5c32e00117703f2f0/pyobjc_framework_contacts-12.1.tar.gz", hash = "sha256:89bca3c5cf31404b714abaa1673577e1aaad6f2ef49d4141c6dbcc0643a789ad", size = 42378, upload-time = "2025-11-14T10:13:14.203Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/c8/2c4638c0d06447886a34070eebb9ba57407d4dd5f0fcb7ab642568272b88/pyobjc_framework_contacts-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2e5ce33b686eb9c0a39351938a756442ea8dea88f6ae2f16bff5494a8569c687", size = 12165, upload-time = "2025-11-14T09:43:05.119Z" }, + { url = "https://files.pythonhosted.org/packages/25/43/e322dd14c77eada5a4f327f5bc094061c90efabc774b30396d1155a69c44/pyobjc_framework_contacts-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62d985098aa86a86d23bff408aac47389680da4edc61f6acf10b2197efcbd0e0", size = 12177, upload-time = "2025-11-14T09:43:06.957Z" }, + { url = "https://files.pythonhosted.org/packages/0a/37/53eba15f2e31950056c63b78732b73379ddbf946c5e6681f3b2773dcf282/pyobjc_framework_contacts-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ab1d78f363dfede16bd5d951327332564bae86f68834d1e657dd18fe4dc12082", size = 12346, upload-time = "2025-11-14T09:43:08.865Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8b/3200f69b77ea85fe69caa1afea444387b5e41bf44ceff11e772954d8a0d5/pyobjc_framework_contacts-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:65576c359eb31c5a5ef95e0c6714686a94bb154a508d791885ff7c33dbc8afa3", size = 12259, upload-time = "2025-11-14T09:43:10.705Z" }, + { url = "https://files.pythonhosted.org/packages/a2/81/0da71a88273aa73841cd3669431c30be627600162ec89cd170759dbffeaf/pyobjc_framework_contacts-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1fac7feca7428047abf3f094fab678c4d0413296f34c30085119850509bc2905", size = 12410, upload-time = "2025-11-14T09:43:12.667Z" }, +] + +[[package]] +name = "pyobjc-framework-contactsui" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-contacts" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/0c/7bb7f898456a81d88d06a1084a42e374519d2e40a668a872b69b11f8c1f9/pyobjc_framework_contactsui-12.1.tar.gz", hash = "sha256:aaeca7c9e0c9c4e224d73636f9a558f9368c2c7422155a41fd4d7a13613a77c1", size = 18769, upload-time = "2025-11-14T10:13:16.301Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/ab/319aa52dfe6f836f4dc542282c2c13996222d4f5c9ea7ff8f391b12dac83/pyobjc_framework_contactsui-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:057f40d2f6eb1b169a300675ec75cc7a747cddcbcee8ece133e652a7086c5ab5", size = 7888, upload-time = "2025-11-14T09:43:18.502Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9c/c9a71681e2ad8695222dbdbbe740af22cc354e9130df6108f9bfe90a4100/pyobjc_framework_contactsui-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2ee2eccb633bc772ecb49dba7199546154efc2db5727992229cdf84b3f6ac84f", size = 7907, upload-time = "2025-11-14T09:43:20.409Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/abdb4c5f53323edc1e02bd0916133c4e6b82ad268eded668ef7b40a1e6c9/pyobjc_framework_contactsui-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c9d64bbc4cfae0f082627b57f7e29e71b924af970f344b106b17fb68e13f7da0", size = 8056, upload-time = "2025-11-14T09:43:22Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d4/fe84efe4301a4367a2ab427214f20e13bfb3a64dc5e29649acc15022c0ad/pyobjc_framework_contactsui-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:eb06b422ce8d422dce2c9af49a2bd093f78761e5aa3f1c866582a4c60cf31f79", size = 7961, upload-time = "2025-11-14T09:43:23.819Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/3ed9be7e479b13e4fd483c704c4833008ff8e63ee3acd66922f2f7a60292/pyobjc_framework_contactsui-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1bbb9bee9535505398771886ac43399400ffc9a84836e845e6d9708ac88e2d5d", size = 8120, upload-time = "2025-11-14T09:43:25.362Z" }, +] + +[[package]] +name = "pyobjc-framework-coreaudio" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/d1/0b884c5564ab952ff5daa949128c64815300556019c1bba0cf2ca752a1a0/pyobjc_framework_coreaudio-12.1.tar.gz", hash = "sha256:a9e72925fcc1795430496ce0bffd4ddaa92c22460a10308a7283ade830089fe1", size = 75077, upload-time = "2025-11-14T10:13:22.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/48/05b5192122e23140cf583eac99ccc5bf615591d6ff76483ba986c38ee750/pyobjc_framework_coreaudio-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a5ad6309779663f846ab36fe6c49647e470b7e08473c3e48b4f004017bdb68a4", size = 36908, upload-time = "2025-11-14T09:43:36.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ce/45808618fefc760e2948c363e0a3402ff77690c8934609cd07b19bc5b15f/pyobjc_framework_coreaudio-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3d8ef424850c8ae2146f963afaec6c4f5bf0c2e412871e68fb6ecfb209b8376f", size = 36935, upload-time = "2025-11-14T09:43:39.414Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f6/0d74d9464bfb4f39451abf745174ec0c4d5c5ebf1c2fcb7556263ae3f75a/pyobjc_framework_coreaudio-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6552624df39dbc68ff9328f244ba56f59234ecbde8455db1e617a71bc4f3dd3a", size = 38390, upload-time = "2025-11-14T09:43:43.194Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f2/c5ca32d01c9d892bf189cfe9b17deaf996db3b4013f8a8ba9b0d22730d70/pyobjc_framework_coreaudio-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:78ea67483a5deb21625c189328152008d278fe1da4304da9fcc1babd12627038", size = 37012, upload-time = "2025-11-14T09:43:46.54Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/c3d660cef1ef874f42057a74931a7a05f581f6a647f5209bef96b372db86/pyobjc_framework_coreaudio-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8d81b0d0296ab4571a4ff302e5cdb52386e486eb8749e99b95b9141438558ca2", size = 38485, upload-time = "2025-11-14T09:43:49.883Z" }, +] + +[[package]] +name = "pyobjc-framework-coreaudiokit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coreaudio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/1c/5c7e39b9361d4eec99b9115b593edd9825388acd594cb3b4519f8f1ac12c/pyobjc_framework_coreaudiokit-12.1.tar.gz", hash = "sha256:b83624f8de3068ab2ca279f786be0804da5cf904ff9979d96007b69ef4869e1e", size = 20137, upload-time = "2025-11-14T10:13:24.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/d7/f171c04c6496afeaad2ab658b0c810682c8407127edc94d4b3f3b90c2bb1/pyobjc_framework_coreaudiokit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:97d5dd857e73d5b597cfc980972b021314b760e2f5bdde7bbba0334fbf404722", size = 7273, upload-time = "2025-11-14T09:43:55.411Z" }, + { url = "https://files.pythonhosted.org/packages/81/9a/6cb91461b07c38b2db7918ee756f05fd704120b75ddc1a759e04af50351b/pyobjc_framework_coreaudiokit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dc1589cda7a4ae0560bf73e1a0623bb710de09ef030d585035f8a428a3e8d6a1", size = 7284, upload-time = "2025-11-14T09:43:57.109Z" }, + { url = "https://files.pythonhosted.org/packages/21/d8/1418fb222c6502ce2a99c415982895b510f6c48bdf60ca0dbed9897d96df/pyobjc_framework_coreaudiokit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6ec70b69d21925e02602cc22c5e0132daedc15ce65b7e3cc863fdb5f13cc23e3", size = 7446, upload-time = "2025-11-14T09:43:58.714Z" }, + { url = "https://files.pythonhosted.org/packages/92/65/36f017784df7ca5ad7741f1624c89410d62d0ebdeb437be32f7a1286a6df/pyobjc_framework_coreaudiokit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a2f9839a4bd05db2e7d12659af4cab32ec17dfee89fff83bbe9faee558e77a08", size = 7349, upload-time = "2025-11-14T09:44:00.625Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fe/f012a1e3b92991819ae3319408cd77b2e7019be14d2b751d6ff613a8fe83/pyobjc_framework_coreaudiokit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0bf793729bf95bb2c667eba315ba4a6ab359f930efd1a5ea686392478abb687f", size = 7503, upload-time = "2025-11-14T09:44:02.166Z" }, +] + +[[package]] +name = "pyobjc-framework-corebluetooth" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/25/d21d6cb3fd249c2c2aa96ee54279f40876a0c93e7161b3304bf21cbd0bfe/pyobjc_framework_corebluetooth-12.1.tar.gz", hash = "sha256:8060c1466d90bbb9100741a1091bb79975d9ba43911c9841599879fc45c2bbe0", size = 33157, upload-time = "2025-11-14T10:13:28.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/56/01fef62a479cdd6ff9ee40b6e062a205408ff386ce5ba56d7e14a71fcf73/pyobjc_framework_corebluetooth-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe72c9732ee6c5c793b9543f08c1f5bdd98cd95dfc9d96efd5708ec9d6eeb213", size = 13209, upload-time = "2025-11-14T09:44:08.203Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6c/831139ebf6a811aed36abfdfad846bc380dcdf4e6fb751a310ce719ddcfd/pyobjc_framework_corebluetooth-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a894f695e6c672f0260327103a31ad8b98f8d4fb9516a0383db79a82a7e58dc", size = 13229, upload-time = "2025-11-14T09:44:10.463Z" }, + { url = "https://files.pythonhosted.org/packages/09/3c/3a6fe259a9e0745aa4612dee86b61b4fd7041c44b62642814e146b654463/pyobjc_framework_corebluetooth-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1daf07a0047c3ed89fab84ad5f6769537306733b6a6e92e631581a0f419e3f32", size = 13409, upload-time = "2025-11-14T09:44:12.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/41/90640a4db62f0bf0611cf8a161129c798242116e2a6a44995668b017b106/pyobjc_framework_corebluetooth-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:15ba5207ca626dffe57ccb7c1beaf01f93930159564211cb97d744eaf0d812aa", size = 13222, upload-time = "2025-11-14T09:44:14.345Z" }, + { url = "https://files.pythonhosted.org/packages/86/99/8ed2f0ca02b9abe204966142bd8c4501cf6da94234cc320c4c0562c467e8/pyobjc_framework_corebluetooth-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e5385195bd365a49ce70e2fb29953681eefbe68a7b15ecc2493981d2fb4a02b1", size = 13408, upload-time = "2025-11-14T09:44:16.558Z" }, +] + +[[package]] +name = "pyobjc-framework-coredata" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/c5/8cd46cd4f1b7cf88bdeed3848f830ea9cdcc4e55cd0287a968a2838033fb/pyobjc_framework_coredata-12.1.tar.gz", hash = "sha256:1e47d3c5e51fdc87a90da62b97cae1bc49931a2bb064db1305827028e1fc0ffa", size = 124348, upload-time = "2025-11-14T10:13:36.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/29/fe24dc81e0f154805534923a56fe572c3b296092f086cf5a239fccc2d46a/pyobjc_framework_coredata-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a3ee3581ca23ead0b152257e98622fe0bf7e7948f30a62a25a17cafe28fe015e", size = 16409, upload-time = "2025-11-14T09:44:23.582Z" }, + { url = "https://files.pythonhosted.org/packages/f8/12/a22773c3a590d4923c74990d6714c4463bd1e183daaa67d6b00c9f325b33/pyobjc_framework_coredata-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79f68577a7e96c57559ec844a129a5edce6827cdfafe49bf31524a488d715a37", size = 16420, upload-time = "2025-11-14T09:44:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/a6/32/9595f0c8727d6ac312d18d23fc4a327c34c6ab873d2b760bbc40cf063726/pyobjc_framework_coredata-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:279b39bdb2a9c5e4d0377c1e81263b7d137bf2be37e15d6b5b2403598596f0e3", size = 16576, upload-time = "2025-11-14T09:44:28.266Z" }, + { url = "https://files.pythonhosted.org/packages/66/2e/238dedc9499b4cccb963dccdfbbc420ace33a01fb9e1221a79c3044fecce/pyobjc_framework_coredata-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:07d19e7db06e1ad21708cf01fc8014d5f1b73efd373a99af6ff882c1bfb8497b", size = 16479, upload-time = "2025-11-14T09:44:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/e1/55/a044857da51644bce6d1914156db5190443653ab9ce6806864728d06d017/pyobjc_framework_coredata-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ac49d45b372f768bd577a26b503dd04e553ffebd3aa96c653b1c88a3f2733552", size = 16636, upload-time = "2025-11-14T09:44:32.952Z" }, +] + +[[package]] +name = "pyobjc-framework-corehaptics" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/2f/74a3da79d9188b05dd4be4428a819ea6992d4dfaedf7d629027cf1f57bfc/pyobjc_framework_corehaptics-12.1.tar.gz", hash = "sha256:521dd2986c8a4266d583dd9ed9ae42053b11ae7d3aa89bf53fbee88307d8db10", size = 22164, upload-time = "2025-11-14T10:13:38.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/f4/f469d6a9cac7c195f3d08fa65f94c32dd1dcf97a54b481be648fb3a7a5f3/pyobjc_framework_corehaptics-12.1-py2.py3-none-any.whl", hash = "sha256:a3b07d36ddf5c86a9cdaa411ab53d09553d26ea04fc7d4f82d21a84f0fc05fc0", size = 5382, upload-time = "2025-11-14T09:44:34.725Z" }, +] + +[[package]] +name = "pyobjc-framework-corelocation" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/79/b75885e0d75397dc2fe1ed9ca80be2b64c18b817f5fb924277cb1bf7b163/pyobjc_framework_corelocation-12.1.tar.gz", hash = "sha256:3674e9353f949d91dde6230ad68f6d5748a7f0424751e08a2c09d06050d66231", size = 53511, upload-time = "2025-11-14T10:13:43.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/57/1b670890fbf650f1a00afe5ee897ea3856a4a1417c2304c633ee2e978ed0/pyobjc_framework_corelocation-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8c35ad29a062fea7d417fd8997a9309660ba7963f2847c004e670efbe6bb5b00", size = 12721, upload-time = "2025-11-14T09:44:41.185Z" }, + { url = "https://files.pythonhosted.org/packages/9f/09/3da1947a5908d70461596eda5a0dc486ae807dc1c5a1ce2bf98567b474be/pyobjc_framework_corelocation-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:616eec0ccfcdcff7696bccf88c1aa39935387e595b22dd4c14842567aa0986a6", size = 12736, upload-time = "2025-11-14T09:44:42.977Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/e5e11ec90500ce2c809a46113d3ebd70dd4b4ce450072db9a85f86e9a30f/pyobjc_framework_corelocation-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0a80ba8e8d9120eb80486235c483a0c734cb451265e5aa81bcf315f0e644eb00", size = 12867, upload-time = "2025-11-14T09:44:44.89Z" }, + { url = "https://files.pythonhosted.org/packages/38/ef/cd24f05a406c4f8478117f7bf54a9a7753b6485b3fc645a5d0530b1fa34b/pyobjc_framework_corelocation-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:3ed12521c457e484944fd91b1d19643d00596d3b0ea3455984c9e918a9c65138", size = 12720, upload-time = "2025-11-14T09:44:46.846Z" }, + { url = "https://files.pythonhosted.org/packages/72/f5/f08ea0a1eacc0e45260a4395412af2f501f93aa91c7efc0cadd39ee75717/pyobjc_framework_corelocation-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:43aa6d5c273c5efa0960dbb05ae7165948f12a889cb0fdcba2e0099d98f4c78d", size = 12862, upload-time = "2025-11-14T09:44:48.688Z" }, +] + +[[package]] +name = "pyobjc-framework-coremedia" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/7d/5ad600ff7aedfef8ba8f51b11d9aaacdf247b870bd14045d6e6f232e3df9/pyobjc_framework_coremedia-12.1.tar.gz", hash = "sha256:166c66a9c01e7a70103f3ca44c571431d124b9070612ef63a1511a4e6d9d84a7", size = 89566, upload-time = "2025-11-14T10:13:49.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/ae/f773cdc33c34a3f9ce6db829dbf72661b65c28ea9efaec8940364185b977/pyobjc_framework_coremedia-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:161a627f5c8cd30a5ebb935189f740e21e6cd94871a9afd463efdb5d51e255fa", size = 29396, upload-time = "2025-11-14T09:44:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ea/aee26a475b4af8ed4152d3c50b1b8955241b8e95ae789aa9ee296953bc6a/pyobjc_framework_coremedia-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:98e885b7a092083fceaef0a7fc406a01ba7bcd3318fb927e59e055931c99cac8", size = 29414, upload-time = "2025-11-14T09:45:01.336Z" }, + { url = "https://files.pythonhosted.org/packages/db/9d/5ff10ee0ff539e125c96b8cff005457558766f942919814c968c3367cc32/pyobjc_framework_coremedia-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:d2b84149c1b3e65ec9050a3e5b617e6c0b4cdad2ab622c2d8c5747a20f013e16", size = 29477, upload-time = "2025-11-14T09:45:04.218Z" }, + { url = "https://files.pythonhosted.org/packages/08/e2/b890658face1290c8b6b6b53a1159c822bece248f883e42302548bef38da/pyobjc_framework_coremedia-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:737ec6e0b63414f42f7188030c85975d6d2124fbf6b15b52c99b6cc20250af4d", size = 29447, upload-time = "2025-11-14T09:45:07.17Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/16981d0ee04b182481ce1e497b5e0326bad6d698fe0265bb7db72b1b26b5/pyobjc_framework_coremedia-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6a9419e0d143df16a1562520a13a389417386e2a53031530af6da60c34058ced", size = 29500, upload-time = "2025-11-14T09:45:10.506Z" }, +] + +[[package]] +name = "pyobjc-framework-coremediaio" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/8e/23baee53ccd6c011c965cff62eb55638b4088c3df27d2bf05004105d6190/pyobjc_framework_coremediaio-12.1.tar.gz", hash = "sha256:880b313b28f00b27775d630174d09e0b53d1cdbadb74216618c9dd5b3eb6806a", size = 51100, upload-time = "2025-11-14T10:13:54.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/0c/9425c53c9a8c26e468e065ba12ef076bab20197ff7c82052a6dddd46d42b/pyobjc_framework_coremediaio-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1108f8a278928fbca465f95123ea4a56456bd6571c1dc8b91793e6c61d624517", size = 17277, upload-time = "2025-11-14T09:45:17.457Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d1/0267ec27841ee96458e6b669ce5b0c67d040ef3d5de90fa4e945ff989c48/pyobjc_framework_coremediaio-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:85ae768294ec307d5b502c075aeae1c53a731afc2f7f0307c9bef785775e26a6", size = 17249, upload-time = "2025-11-14T09:45:20.42Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4e/bd0114aa052aaffc250b0c00567b42df8c7cb35517488c3238bcc964d016/pyobjc_framework_coremediaio-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6136a600a1435b9e798427984088a7bd5e68778e1bcf48a23a0eb9bc946a06f0", size = 17573, upload-time = "2025-11-14T09:45:22.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/fd/cdf26be5b15ee2f2a73c320a62393e03ab15966ee8262540f918f0c7b181/pyobjc_framework_coremediaio-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a5ca5763f185f48fedafec82f794dca53c55d2e52058d1b11baa43dd4ab0cd16", size = 17266, upload-time = "2025-11-14T09:45:24.719Z" }, + { url = "https://files.pythonhosted.org/packages/18/75/be0bfb86497f98915c7d015e3c21d199a1be8780ed08c171832b27593eac/pyobjc_framework_coremediaio-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8aaeb44fdf9382dda30ff5f53ba6e291c1b514b7ab651f7b31d7fb4c27bfd309", size = 17561, upload-time = "2025-11-14T09:45:26.897Z" }, +] + +[[package]] +name = "pyobjc-framework-coremidi" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/96/2d583060a71a73c8a7e6d92f2a02675621b63c1f489f2639e020fae34792/pyobjc_framework_coremidi-12.1.tar.gz", hash = "sha256:3c6f1fd03997c3b0f20ab8545126b1ce5f0cddcc1587dffacad876c161da8c54", size = 55587, upload-time = "2025-11-14T10:13:58.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/2d/99520f6f1685e4cad816e55cbf6d85f8ce6ea908107950e2d37dc17219d8/pyobjc_framework_coremidi-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e84ffc1de59691c04201b0872e184fe55b5589f3a14876bd14460f3b5f3cd109", size = 24317, upload-time = "2025-11-14T09:45:34.92Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2a/093ec8366d5f9e6c45e750310121ea572b8696518c51c4bbcf1623c01cf1/pyobjc_framework_coremidi-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:69720f38cfeea4299f31cb3e15d07e5d43e55127605f95e001794c7850c1c637", size = 24333, upload-time = "2025-11-14T09:45:37.577Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cf/f03a0b44d1cfcfa9837cdfd6385c1e7d1e42301076d376329a44b6cbec03/pyobjc_framework_coremidi-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:06e5bce0a28bac21f09bcfedda46d93b2152c138764380314d99f2370a8c00f2", size = 24493, upload-time = "2025-11-14T09:45:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/29/4d/7d8d6ee42a2c6ebc89fb78fa6a2924de255f76ba7907656c26cc5847fc92/pyobjc_framework_coremidi-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b49442cf533923952f56049be407edbe2ab2ece04ae1c94ca1e28d500f9f5754", size = 24371, upload-time = "2025-11-14T09:45:43.514Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e5/56239a9e05fe62ad7cf00844c9a89db249281dc6b72238dfdcaa783896b0/pyobjc_framework_coremidi-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:194bc4da148ace8b71117c227562cad39a2708d296f569839f56d83e8801b25b", size = 24536, upload-time = "2025-11-14T09:45:46.504Z" }, +] + +[[package]] +name = "pyobjc-framework-coreml" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/2d/baa9ea02cbb1c200683cb7273b69b4bee5070e86f2060b77e6a27c2a9d7e/pyobjc_framework_coreml-12.1.tar.gz", hash = "sha256:0d1a4216891a18775c9e0170d908714c18e4f53f9dc79fb0f5263b2aa81609ba", size = 40465, upload-time = "2025-11-14T10:14:02.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/39/4defef0deb25c5d7e3b7826d301e71ac5b54ef901b7dac4db1adc00f172d/pyobjc_framework_coreml-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:10dc8e8db53d7631ebc712cad146e3a9a9a443f4e1a037e844149a24c3c42669", size = 11356, upload-time = "2025-11-14T09:45:52.271Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3f/3749964aa3583f8c30d9996f0d15541120b78d307bb3070f5e47154ef38d/pyobjc_framework_coreml-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:48fa3bb4a03fa23e0e36c93936dca2969598e4102f4b441e1663f535fc99cd31", size = 11371, upload-time = "2025-11-14T09:45:54.105Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c8/cf20ea91ae33f05f3b92dec648c6f44a65f86d1a64c1d6375c95b85ccb7c/pyobjc_framework_coreml-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:71de5b37e6a017e3ed16645c5d6533138f24708da5b56c35c818ae49d0253ee1", size = 11600, upload-time = "2025-11-14T09:45:55.976Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5c/510ae8e3663238d32e653ed6a09ac65611dd045a7241f12633c1ab48bb9b/pyobjc_framework_coreml-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a04a96e512ecf6999aa9e1f60ad5635cb9d1cd839be470341d8d1541797baef6", size = 11418, upload-time = "2025-11-14T09:45:57.75Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1a/b7367819381b07c440fa5797d2b0487e31f09aa72079a693ceab6875fa0a/pyobjc_framework_coreml-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7762b3dd2de01565b7cf3049ce1e4c27341ba179d97016b0b7607448e1c39865", size = 11593, upload-time = "2025-11-14T09:45:59.623Z" }, +] + +[[package]] +name = "pyobjc-framework-coremotion" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/eb/abef7d405670cf9c844befc2330a46ee59f6ff7bac6f199bf249561a2ca6/pyobjc_framework_coremotion-12.1.tar.gz", hash = "sha256:8e1b094d34084cc8cf07bedc0630b4ee7f32b0215011f79c9e3cd09d205a27c7", size = 33851, upload-time = "2025-11-14T10:14:05.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/75/89fa4aab818aeca21ac0a60b7ceb89a9e685df0ddd3828d36a6f84a0cff0/pyobjc_framework_coremotion-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a77908ab83c422030f913a2a761d196359ab47f6d1e7c76f21de2c6c05ea2f5f", size = 10406, upload-time = "2025-11-14T09:46:05.076Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dd/9a4cc56c55f7ffece2e100664503cb27b4f4265d57656d050a3af1c71d94/pyobjc_framework_coremotion-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b7b0d47b5889ca0b6e3a687bd1f83a13d3bb59c07a1c4c37dcca380ede5d6e81", size = 10423, upload-time = "2025-11-14T09:46:07.051Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4d/660b47e9e0bc10ae87f85bede39e3f922b8382e0f6ac273058183d0bdc2f/pyobjc_framework_coremotion-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:531ea82945266d78e23d1f35de0cae2391e18677ed54120b90a4b9dd19f32596", size = 10570, upload-time = "2025-11-14T09:46:09.047Z" }, + { url = "https://files.pythonhosted.org/packages/21/b0/a1809fc3eea18db15d20bd2225f4d5e1cfc74f38b252e0cb1e3f2563bcfa/pyobjc_framework_coremotion-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e7ce95dfa7e33b5762e0a800d76ef9c6a34b827c700d7e80c3740b7cd05168a5", size = 10484, upload-time = "2025-11-14T09:46:10.751Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c4/167729d032e27985d1a6ba5e60c8045c43b9392624e8c605a24f2e22cf14/pyobjc_framework_coremotion-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d0aedcf8157c1428c7d2df8edae159b9de226d4df719c5bac8a96b648950b63e", size = 10629, upload-time = "2025-11-14T09:46:12.782Z" }, +] + +[[package]] +name = "pyobjc-framework-coreservices" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-fsevents" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/b3/52338a3ff41713f7d7bccaf63bef4ba4a8f2ce0c7eaff39a3629d022a79a/pyobjc_framework_coreservices-12.1.tar.gz", hash = "sha256:fc6a9f18fc6da64c166fe95f2defeb7ac8a9836b3b03bb6a891d36035260dbaa", size = 366150, upload-time = "2025-11-14T10:14:28.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/6c/33984caaf497fc5a6f86350d7ca4fac8abeb2bc33203edc96955a21e8c05/pyobjc_framework_coreservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8751dc2edcb7cfa248bf8a274c4d6493e8d53ef28a843827a4fc9a0a8b04b8be", size = 30206, upload-time = "2025-11-14T09:46:22.732Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6f/4a6eb2f2bbdbf66a1b35f272d8504ce6f098947f9343df474f0d15a2b507/pyobjc_framework_coreservices-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:96574fb24d2b8b507901ef7be7fcb70b7f49e110bd050a411b90874cc18c7c7b", size = 30226, upload-time = "2025-11-14T09:46:25.565Z" }, + { url = "https://files.pythonhosted.org/packages/60/6e/78a831834dc7f84a2d61efb47d212239f3ae3d16aa5512f1265a8f6c0162/pyobjc_framework_coreservices-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:227fb4144a87c6c97a5f737fb0c666293b33e54f0ffb500f2c420da6c110ba2d", size = 30229, upload-time = "2025-11-14T09:46:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b6/c4100905d92f1187f74701ab520da95a235c09e94a71e5872462660ac022/pyobjc_framework_coreservices-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c650e1083fb313b9c8df4be8d582c266aa1b99c75ed5d7e45e3a91a7b8a128b2", size = 30255, upload-time = "2025-11-14T09:46:31.492Z" }, + { url = "https://files.pythonhosted.org/packages/d2/79/df730603028dbd34aa61dbe0396cc23715520195726686bb5e5832429f56/pyobjc_framework_coreservices-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dff0cb6ccbd39ea45b01a50955d757172567de5c164f6e8e241bf4e7639b0946", size = 30269, upload-time = "2025-11-14T09:46:34.469Z" }, +] + +[[package]] +name = "pyobjc-framework-corespotlight" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/d0/88ca73b0cf23847af463334989dd8f98e44f801b811e7e1d8a5627ec20b4/pyobjc_framework_corespotlight-12.1.tar.gz", hash = "sha256:57add47380cd0bbb9793f50a4a4b435a90d4ebd2a33698e058cb353ddfb0d068", size = 38002, upload-time = "2025-11-14T10:14:31.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/3b/d3031eddff8029859de6d92b1f741625b1c233748889141a6a5a89b96f0e/pyobjc_framework_corespotlight-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bfcea64ab3250e2886d202b8731be3817b5ac0c8c9f43e77d0d5a0b6602e71a7", size = 9996, upload-time = "2025-11-14T09:46:47.157Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/419ae27bdd17701404301ede1969daadeef6ef6dd8b4a8110a90a1d77df1/pyobjc_framework_corespotlight-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:37003bfea415ff21859d44403c3a13ac55f90b6dca92c69b81b61d96cee0c7be", size = 10012, upload-time = "2025-11-14T09:46:48.826Z" }, + { url = "https://files.pythonhosted.org/packages/a8/84/ebe1acb365958604465f83710772c1a08854f472896e607f7eedb5944e1b/pyobjc_framework_corespotlight-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ede26027cfa577e6748b7dd0615e8a1bb379e48ad2324489b2c8d242cdf6fce8", size = 10152, upload-time = "2025-11-14T09:46:51.025Z" }, + { url = "https://files.pythonhosted.org/packages/21/cf/11cafe42bc7209bd96d71323beb60d6d1cdb069eb651f120323b3ef9c8d4/pyobjc_framework_corespotlight-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:986ac40755e15aa3a562aac687b22c882de2b4b0fa58fbd419cc3487a0df1507", size = 10069, upload-time = "2025-11-14T09:46:53Z" }, + { url = "https://files.pythonhosted.org/packages/10/95/a64f847413834ced69c29d63b60aeb084174d81d57f748475be03fbfcdc2/pyobjc_framework_corespotlight-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0041b9a10d7f6c4a8d05f2ed281194a3d8bc5b2d0ceca4f4a9d9a8ce064fd68e", size = 10215, upload-time = "2025-11-14T09:46:54.703Z" }, +] + +[[package]] +name = "pyobjc-framework-coretext" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124, upload-time = "2025-11-14T10:14:38.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/0f/ddf45bf0e3ba4fbdc7772de4728fd97ffc34a0b5a15e1ab1115b202fe4ae/pyobjc_framework_coretext-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d246fa654bdbf43bae3969887d58f0b336c29b795ad55a54eb76397d0e62b93c", size = 30108, upload-time = "2025-11-14T09:47:04.228Z" }, + { url = "https://files.pythonhosted.org/packages/20/a2/a3974e3e807c68e23a9d7db66fc38ac54f7ecd2b7a9237042006699a76e1/pyobjc_framework_coretext-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7cbb2c28580e6704ce10b9a991ccd9563a22b3a75f67c36cf612544bd8b21b5f", size = 30110, upload-time = "2025-11-14T09:47:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5d/85e059349e9cfbd57269a1f11f56747b3ff5799a3bcbd95485f363c623d8/pyobjc_framework_coretext-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:14100d1e39efb30f57869671fb6fce8d668f80c82e25e7930fb364866e5c0dab", size = 30697, upload-time = "2025-11-14T09:47:10.932Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/adf9d306e9ead108167ab7a974ab7d171dbacf31c72fad63e12585f58023/pyobjc_framework_coretext-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:782a1a9617ea267c05226e9cd81a8dec529969a607fe1e037541ee1feb9524e9", size = 30095, upload-time = "2025-11-14T09:47:13.893Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ca/6321295f47a47b0fca7de7e751ddc0ddc360413f4e506335fe9b0f0fb085/pyobjc_framework_coretext-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7afe379c5a870fa3e66e6f65231c3c1732d9ccd2cd2a4904b2cd5178c9e3c562", size = 30702, upload-time = "2025-11-14T09:47:17.292Z" }, +] + +[[package]] +name = "pyobjc-framework-corewlan" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/71/739a5d023566b506b3fd3d2412983faa95a8c16226c0dcd0f67a9294a342/pyobjc_framework_corewlan-12.1.tar.gz", hash = "sha256:a9d82ec71ef61f37e1d611caf51a4203f3dbd8caf827e98128a1afaa0fd2feb5", size = 32417, upload-time = "2025-11-14T10:14:41.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/31/3e9cf2c0ac3c979062958eae7a275b602515c9c76fd30680e1ee0fea82ae/pyobjc_framework_corewlan-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5cba04c0550fc777767cd3a5471e4ed837406ab182d7d5c273bc5ce6ea237bfe", size = 9958, upload-time = "2025-11-14T09:47:22.474Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/b691e4d1730c16f8ea2f883712054961a3e45f40e1471c0edfc30f061c07/pyobjc_framework_corewlan-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aac949646953effdd36d2d21bc0ab645e58bb25deafe86c6e600b3cdcfc2228b", size = 9968, upload-time = "2025-11-14T09:47:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/88/2e/dbba1674e1629839f479c9d14b90c37ed3b5f76d3b6b3ad56af48951c45b/pyobjc_framework_corewlan-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:dae63c36affcc933c9161980e4fe7333e0c59c968174a00a75cb5f6e4ede10c6", size = 10115, upload-time = "2025-11-14T09:47:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e2/e89ea1ee92de17ec53087868d0466f6fd8174488b613a46528a3642aa41d/pyobjc_framework_corewlan-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:336536ecfd503118f79c8337cc983bbf0768e3ba4ac142e0cf8db1408c644965", size = 10010, upload-time = "2025-11-14T09:47:27.827Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/e695f432dbfcd0fbfa416db21471091e94e921094a795b87cb9ebea423e5/pyobjc_framework_corewlan-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fe6373e83e12be6854f7c1f054e2f68b41847fd739aa578d3c5478bd3fd4014f", size = 10162, upload-time = "2025-11-14T09:47:29.82Z" }, +] + +[[package]] +name = "pyobjc-framework-cryptotokenkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/7c/d03ff4f74054578577296f33bc669fce16c7827eb1a553bb372b5aab30ca/pyobjc_framework_cryptotokenkit-12.1.tar.gz", hash = "sha256:c95116b4b7a41bf5b54aff823a4ef6f4d9da4d0441996d6d2c115026a42d82f5", size = 32716, upload-time = "2025-11-14T10:14:45.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/c7/aecba253cf21303b2c9f3ce03fc0e987523609d7839ea8e0a688ae816c96/pyobjc_framework_cryptotokenkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ef51a86c1d0125fabdfad0b3efa51098fb03660d8dad2787d82e8b71c9f189de", size = 12633, upload-time = "2025-11-14T09:47:35.707Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/3e24abc92a8ee8ee11386d4d9dfb2d6961d10814474053a8ebccfaff0d97/pyobjc_framework_cryptotokenkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e65a8e4558e6cf1e46a9b4a52fcbf7b2ddd17958d675e9047d8a9f131d0a4d33", size = 12650, upload-time = "2025-11-14T09:47:37.633Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/418afc27429922e73a05bd22198c71e1f6b3badebd73cad208eb9e922f64/pyobjc_framework_cryptotokenkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:cc9aa75e418376e92b1540d1edfa0c8097a027a1a241717983d0223cdad8e9ca", size = 12834, upload-time = "2025-11-14T09:47:40.27Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cc/32c8e34c6c54e487b993eaabe70d997096fcc1d82176207f967858f2987b/pyobjc_framework_cryptotokenkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:94fa4b3903a1a39fe1d5874a5ae5b67471f488925c485a7e9c3575fbf9eba43e", size = 12632, upload-time = "2025-11-14T09:47:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7e/57c569f4f71dfcb65b049fbb0aace19da0ed756eef7f440950098f8de498/pyobjc_framework_cryptotokenkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:05d40859a40ba4ed3dd8befabefc02aa224336c660b2f33ebf14d5397a30ffb3", size = 12839, upload-time = "2025-11-14T09:47:44.133Z" }, +] + +[[package]] +name = "pyobjc-framework-datadetection" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/97/9b03832695ec4d3008e6150ddfdc581b0fda559d9709a98b62815581259a/pyobjc_framework_datadetection-12.1.tar.gz", hash = "sha256:95539e46d3bc970ce890aa4a97515db10b2690597c5dd362996794572e5d5de0", size = 12323, upload-time = "2025-11-14T10:14:46.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/1c/5d2f941501e84da8fef8ef3fd378b5c083f063f083f97dd3e8a07f0404b3/pyobjc_framework_datadetection-12.1-py2.py3-none-any.whl", hash = "sha256:4dc8e1d386d655b44b2681a4a2341fb2fc9addbf3dda14cb1553cd22be6a5387", size = 3497, upload-time = "2025-11-14T09:47:45.826Z" }, +] + +[[package]] +name = "pyobjc-framework-devicecheck" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/af/c676107c40d51f55d0a42043865d7246db821d01241b518ea1d3b3ef1394/pyobjc_framework_devicecheck-12.1.tar.gz", hash = "sha256:567e85fc1f567b3fe64ac1cdc323d989509331f64ee54fbcbde2001aec5adbdb", size = 12885, upload-time = "2025-11-14T10:14:48.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/d8/1f1b13fa4775b6474c9ad0f4b823953eaeb6c11bd6f03fa8479429b36577/pyobjc_framework_devicecheck-12.1-py2.py3-none-any.whl", hash = "sha256:ffd58148bdef4a1ee8548b243861b7d97a686e73808ca0efac5bef3c430e4a15", size = 3684, upload-time = "2025-11-14T09:47:47.25Z" }, +] + +[[package]] +name = "pyobjc-framework-devicediscoveryextension" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/b0/e6e2ed6a7f4b689746818000a003ff7ab9c10945df66398ae8d323ae9579/pyobjc_framework_devicediscoveryextension-12.1.tar.gz", hash = "sha256:60e12445fad97ff1f83472255c943685a8f3a9d95b3126d887cfe769b7261044", size = 14718, upload-time = "2025-11-14T10:14:50.723Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/0c/005fe8db1e19135f493a3de8c8d38031e1ad2d626de4ef89f282acf4aff7/pyobjc_framework_devicediscoveryextension-12.1-py2.py3-none-any.whl", hash = "sha256:d6d6b606d27d4d88efc0bed4727c375e749149b360290c3ad2afc52337739a1b", size = 4321, upload-time = "2025-11-14T09:47:48.78Z" }, +] + +[[package]] +name = "pyobjc-framework-dictionaryservices" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-coreservices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/c0/daf03cdaf6d4e04e0cf164db358378c07facd21e4e3f8622505d72573e2c/pyobjc_framework_dictionaryservices-12.1.tar.gz", hash = "sha256:354158f3c55d66681fa903c7b3cb05a435b717fa78d0cef44d258d61156454a7", size = 10573, upload-time = "2025-11-14T10:14:53.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/13/ab308e934146cfd54691ddad87e572cd1edb6659d795903c4c75904e2d7d/pyobjc_framework_dictionaryservices-12.1-py2.py3-none-any.whl", hash = "sha256:578854eec17fa473ac17ab30050a7bbb2ab69f17c5c49b673695254c3e88ad4b", size = 3930, upload-time = "2025-11-14T09:47:50.782Z" }, +] + +[[package]] +name = "pyobjc-framework-discrecording" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/87/8bd4544793bfcdf507174abddd02b1f077b48fab0004b3db9a63142ce7e9/pyobjc_framework_discrecording-12.1.tar.gz", hash = "sha256:6defc8ea97fb33b4d43870c673710c04c3dc48be30cdf78ba28191a922094990", size = 55607, upload-time = "2025-11-14T10:14:58.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/70/14a5aa348a5eba16e8773bb56698575cf114aa55aa303037b7000fc53959/pyobjc_framework_discrecording-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:865f1551e58459da6073360afc8f2cc452472c676ba83dcaa9b0c44e7775e4b5", size = 14566, upload-time = "2025-11-14T09:47:57.503Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/0064a48b24694597890cb065f5d33f719eed2cfff2878f43f310f27485cc/pyobjc_framework_discrecording-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c682c458622db9b4ea8363335ee38f5dd98db6691680041a3fda73e26714346", size = 14567, upload-time = "2025-11-14T09:47:59.78Z" }, + { url = "https://files.pythonhosted.org/packages/de/78/b8b3f063ecda49d600548eeee0c29b47a0b7635623a68609038326bfa7e7/pyobjc_framework_discrecording-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:36e1ba4d37fe310bad2fbfeadd43c8ef001cfae9a2a0484d7318504c5dbefa3f", size = 14745, upload-time = "2025-11-14T09:48:02.271Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f1/61b7d8a35fb654ece97b539912452334665abf0a1fa9e83cda809c674c9e/pyobjc_framework_discrecording-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a60e2cab88fdf923f2017effb248f7c32819fbe494a6d17acfa71754b44ff68c", size = 14632, upload-time = "2025-11-14T09:48:04.41Z" }, + { url = "https://files.pythonhosted.org/packages/59/f5/e3db465b3087a3d3550dc9b4a90b33fa281d19da24dd0a5b591eeddbbe64/pyobjc_framework_discrecording-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3345fcb139f1646c2aef41be6344c5b944817ea4df85d7f61db27781a90d77a6", size = 14808, upload-time = "2025-11-14T09:48:06.496Z" }, +] + +[[package]] +name = "pyobjc-framework-discrecordingui" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-discrecording" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/63/8667f5bb1ecb556add04e86b278cb358dc1f2f03862705cae6f09097464c/pyobjc_framework_discrecordingui-12.1.tar.gz", hash = "sha256:6793d4a1a7f3219d063f39d87f1d4ebbbb3347e35d09194a193cfe16cba718a8", size = 16450, upload-time = "2025-11-14T10:15:00.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/4e/76016130c27b98943c5758a05beab3ba1bc9349ee881e1dfc509ea954233/pyobjc_framework_discrecordingui-12.1-py2.py3-none-any.whl", hash = "sha256:6544ef99cad3dee95716c83cb207088768b6ecd3de178f7e1b17df5997689dfd", size = 4702, upload-time = "2025-11-14T09:48:08.01Z" }, +] + +[[package]] +name = "pyobjc-framework-diskarbitration" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/42/f75fcabec1a0033e4c5235cc8225773f610321d565b63bf982c10c6bbee4/pyobjc_framework_diskarbitration-12.1.tar.gz", hash = "sha256:6703bc5a09b38a720c9ffca356b58f7e99fa76fc988c9ec4d87112344e63dfc2", size = 17121, upload-time = "2025-11-14T10:15:02.223Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/65/c1f54c47af17cb6b923eab85e95f22396c52f90ee8f5b387acffad9a99ea/pyobjc_framework_diskarbitration-12.1-py2.py3-none-any.whl", hash = "sha256:54caf3079fe4ae5ac14466a9b68923ee260a1a88a8290686b4a2015ba14c2db6", size = 4877, upload-time = "2025-11-14T09:48:09.945Z" }, +] + +[[package]] +name = "pyobjc-framework-dvdplayback" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/dd/7859a58e8dd336c77f83feb76d502e9623c394ea09322e29a03f5bc04d32/pyobjc_framework_dvdplayback-12.1.tar.gz", hash = "sha256:279345d4b5fb2c47dd8e5c2fd289e644b6648b74f5c25079805eeb61bfc4a9cd", size = 32332, upload-time = "2025-11-14T10:15:05.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/7d/22c07c28fab1f15f0d364806e39a6ca63c737c645fe7e98e157878b5998c/pyobjc_framework_dvdplayback-12.1-py2.py3-none-any.whl", hash = "sha256:af911cc222272a55b46a1a02a46a355279aecfd8132231d8d1b279e252b8ad4c", size = 8243, upload-time = "2025-11-14T09:48:11.824Z" }, +] + +[[package]] +name = "pyobjc-framework-eventkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/42/4ec97e641fdcf30896fe76476181622954cb017117b1429f634d24816711/pyobjc_framework_eventkit-12.1.tar.gz", hash = "sha256:7c1882be2f444b1d0f71e9a0cd1e9c04ad98e0261292ab741fc9de0b8bbbbae9", size = 28538, upload-time = "2025-11-14T10:15:07.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/35/142f43227627d6324993869d354b9e57eb1e88c4e229e2271592254daf25/pyobjc_framework_eventkit-12.1-py2.py3-none-any.whl", hash = "sha256:3d2d36d5bd9e0a13887a6ac7cdd36675985ebe2a9cb3cdf8cec0725670c92c60", size = 6820, upload-time = "2025-11-14T09:48:14.035Z" }, +] + +[[package]] +name = "pyobjc-framework-exceptionhandling" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/17/5c9d4164f7ccf6b9100be0ad597a7857395dd58ea492cba4f0e9c0b77049/pyobjc_framework_exceptionhandling-12.1.tar.gz", hash = "sha256:7f0719eeea6695197fce0e7042342daa462683dc466eb6a442aad897032ab00d", size = 16694, upload-time = "2025-11-14T10:15:10.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/ad/8e05acf3635f20ea7d878be30d58a484c8b901a8552c501feb7893472f86/pyobjc_framework_exceptionhandling-12.1-py2.py3-none-any.whl", hash = "sha256:2f1eae14cf0162e53a0888d9ffe63f047501fe583a23cdc9c966e89f48cf4713", size = 7113, upload-time = "2025-11-14T09:48:15.685Z" }, +] + +[[package]] +name = "pyobjc-framework-executionpolicy" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/11/db765e76e7b00e1521d7bb3a61ae49b59e7573ac108da174720e5d96b61b/pyobjc_framework_executionpolicy-12.1.tar.gz", hash = "sha256:682866589365cd01d3a724d8a2781794b5cba1e152411a58825ea52d7b972941", size = 12594, upload-time = "2025-11-14T10:15:12.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/2c/f10352398f10f244401ab8f53cabd127dc3f5dbbfc8de83464661d716671/pyobjc_framework_executionpolicy-12.1-py2.py3-none-any.whl", hash = "sha256:c3a9eca3bd143cf202787dd5e3f40d954c198f18a5e0b8b3e2fcdd317bf33a52", size = 3739, upload-time = "2025-11-14T09:48:17.35Z" }, +] + +[[package]] +name = "pyobjc-framework-extensionkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/d4/e9b1f74d29ad9dea3d60468d59b80e14ed3a19f9f7a25afcbc10d29c8a1e/pyobjc_framework_extensionkit-12.1.tar.gz", hash = "sha256:773987353e8aba04223dbba3149253db944abfb090c35318b3a770195b75da6d", size = 18694, upload-time = "2025-11-14T10:15:14.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/d9/8064dad6114a489e5439cc20d9fb0dd64cfc406d875b4a3c87015b3f6266/pyobjc_framework_extensionkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7e01d705c7ac6d080ae34a81db6d9b81875eabefa63fd6eafbfa30f676dd780b", size = 7932, upload-time = "2025-11-14T09:48:23.653Z" }, + { url = "https://files.pythonhosted.org/packages/f5/75/63c304543fc3c5c0755521ab0535e3f81f6ab8de656a02598e23f687cb6c/pyobjc_framework_extensionkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8f2a87bd4fbb8d14900bbe9c979b23b7532b23685c0f5022671b26db4fa3e515", size = 7946, upload-time = "2025-11-14T09:48:25.803Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/2dab02d8726abf586f253fbddc2d0d9b2abd5dbb4b24272eb48c886741fc/pyobjc_framework_extensionkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:570e8a89116380a27dd8df7ce28cd5f7296eb785aea4cb7dc6447954005360c2", size = 8086, upload-time = "2025-11-14T09:48:27.715Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ec/a02ddac5ea7439dc4deb488ba551e27565920b8864c2f71611159794a1b5/pyobjc_framework_extensionkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b002bd4ee7aa951298f8bdd41e2a59d172050975499f94a26caff263b5fadca4", size = 8004, upload-time = "2025-11-14T09:48:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/15/21/2fad7badad0bb25c22bff840563041a3f9e10aee4da7232bdbbff1b48138/pyobjc_framework_extensionkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d14ebebffe05d33d189bf2bec5b676721790cf041b7ee628bfd05bcda4c148cc", size = 8141, upload-time = "2025-11-14T09:48:31.37Z" }, +] + +[[package]] +name = "pyobjc-framework-externalaccessory" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/35/86c097ae2fdf912c61c1276e80f3e090a3fc898c75effdf51d86afec456b/pyobjc_framework_externalaccessory-12.1.tar.gz", hash = "sha256:079f770a115d517a6ab87db1b8a62ca6cdf6c35ae65f45eecc21b491e78776c0", size = 20958, upload-time = "2025-11-14T10:15:16.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/52/984034396089766b6e5ff3be0f93470e721c420fa9d1076398557532234f/pyobjc_framework_externalaccessory-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dedbf7a09375ac19668156c1417bd7829565b164a246b714e225b9cbb6a351ad", size = 8932, upload-time = "2025-11-14T09:48:37.393Z" }, + { url = "https://files.pythonhosted.org/packages/2d/bf/9e368e16edb94d9507c1034542379b943e0d9c3bcc0ce8062ac330216317/pyobjc_framework_externalaccessory-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:34858f06cd75fe4e358555961a6898eb8778fd2931058fd660fcd5d6cf31b162", size = 8944, upload-time = "2025-11-14T09:48:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/71/5b/643a00fe334485b4100d7a68330b6c6c349fe27434e0dc0fdf2065984555/pyobjc_framework_externalaccessory-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5551915fa82ff1eea8e5810f74c1298e5327aefe4ac90abeb9a7abd69ff33a22", size = 9100, upload-time = "2025-11-14T09:48:41.57Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e4/b7f1c8b977e64b495a5f268f9f6d82ed71152268542a7e676c26c647a6b0/pyobjc_framework_externalaccessory-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:22efc5bf68f5f0ef39f4308ef06403c42544f5fc75f6eeb137a87af99357dda1", size = 8999, upload-time = "2025-11-14T09:48:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/02/23/c038dd6c9dee7067dd51e430f5019a39f68102aade47ae9a89f64eb913d6/pyobjc_framework_externalaccessory-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3a0f21fe660ee89b98d357ce3df9ff546f19161b6f569cc93888e6bcbd1d7f22", size = 9178, upload-time = "2025-11-14T09:48:45.398Z" }, +] + +[[package]] +name = "pyobjc-framework-fileprovider" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/9a/724b1fae5709f8860f06a6a2a46de568f9bb8bdb2e2aae45b4e010368f51/pyobjc_framework_fileprovider-12.1.tar.gz", hash = "sha256:45034e0d00ae153c991aa01cb1fd41874650a30093e77ba73401dcce5534c8ad", size = 43071, upload-time = "2025-11-14T10:15:19.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/f5/56f0751a2988b2caca89d6800c8f29246828d1a7498bb676ef1ab28000b7/pyobjc_framework_fileprovider-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:89b140ea8369512ddf4164b007cbe35b4d97d1dcb8affa12a7264c0ab8d56e45", size = 21003, upload-time = "2025-11-14T09:48:53.128Z" }, + { url = "https://files.pythonhosted.org/packages/31/92/23deb9d12690a69599dd7a66f3f5a5a3c09824147d148759a33c5c2933fc/pyobjc_framework_fileprovider-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a1a7a6ac3af1e93d23f5644b4c7140dc7edf5ff79419cc0bd25ce7001afc1cf6", size = 21018, upload-time = "2025-11-14T09:48:55.504Z" }, + { url = "https://files.pythonhosted.org/packages/a4/99/cec0a13ca8da9283d1a1bbaeeabdff7903be5c85cfb27a2bb7cc121cb529/pyobjc_framework_fileprovider-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6d6744c8c4f915b6193a982365d947b63286cea605f990a2aaa3bb37069471f2", size = 21300, upload-time = "2025-11-14T09:48:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/b1c6e0927d22d0c125c8a62cd2342c4613e3aabf13cb0e66ea62fe85fff1/pyobjc_framework_fileprovider-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:520b8c83b1ce63e0f668ea1683e3843f2e5379c0af76dceb19d5d540d584ff54", size = 21062, upload-time = "2025-11-14T09:49:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/25/14/1a05c99849e6abb778f601eeb93e27f2fbbbb8f4ffaab42c8aa02ff62406/pyobjc_framework_fileprovider-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:de9aaea1308e37f7537dd2a8e89f151d4eaee2b0db5d248dc85cc1fd521adaaa", size = 21331, upload-time = "2025-11-14T09:49:02.803Z" }, +] + +[[package]] +name = "pyobjc-framework-fileproviderui" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-fileprovider" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/00/234f9b93f75255845df81d9d5ea20cb83ecb5c0a4e59147168b622dd0b9d/pyobjc_framework_fileproviderui-12.1.tar.gz", hash = "sha256:15296429d9db0955abc3242b2920b7a810509a85118dbc185f3ac8234e5a6165", size = 12437, upload-time = "2025-11-14T10:15:22.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/65/cc4397511bd0af91993d6302a2aed205296a9ad626146eefdfc8a9624219/pyobjc_framework_fileproviderui-12.1-py2.py3-none-any.whl", hash = "sha256:521a914055089e28631018bd78df4c4f7416e98b4150f861d4a5bc97d5b1ffe4", size = 3715, upload-time = "2025-11-14T09:49:04.213Z" }, +] + +[[package]] +name = "pyobjc-framework-findersync" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/63/c8da472e0910238a905bc48620e005a1b8ae7921701408ca13e5fb0bfb4b/pyobjc_framework_findersync-12.1.tar.gz", hash = "sha256:c513104cef0013c233bf8655b527df665ce6f840c8bc0b3781e996933d4dcfa6", size = 13507, upload-time = "2025-11-14T10:15:24.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9f/ec7f393e3e2fd11cbdf930d884a0ba81078bdb61920b3cba4f264de8b446/pyobjc_framework_findersync-12.1-py2.py3-none-any.whl", hash = "sha256:e07abeca52c486cf14927f617afc27afa7a3828b99fab3ad02355105fb29203e", size = 4889, upload-time = "2025-11-14T09:49:05.763Z" }, +] + +[[package]] +name = "pyobjc-framework-fsevents" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/17/21f45d2bca2efc72b975f2dfeae7a163dbeabb1236c1f188578403fd4f09/pyobjc_framework_fsevents-12.1.tar.gz", hash = "sha256:a22350e2aa789dec59b62da869c1b494a429f8c618854b1383d6473f4c065a02", size = 26487, upload-time = "2025-11-14T10:15:26.796Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/e3/2c5eeea390c0b053e2d73b223af3ec87a3e99a8106e8d3ee79942edb0822/pyobjc_framework_fsevents-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a2949358513fd7bc622fb362b5c4af4fc24fc6307320070ca410885e5e13d975", size = 13141, upload-time = "2025-11-14T09:49:11.947Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/f06d14020eb9ec10c0e36f5e3f836f8541b989dcde9f53ea172852a7c864/pyobjc_framework_fsevents-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b30c72239a9ced4e4604fcf265a1efee788cb47850982dd80fcbaafa7ee64f9", size = 13143, upload-time = "2025-11-14T09:49:14.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/3a/10c1576da38f7e39d6adb592f54fa1b058c859c7d38d03b0cdaf25e12f8d/pyobjc_framework_fsevents-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:05220368b0685783e0ae00c885e167169d47ff5cf66de7172ca8074682dfc330", size = 13511, upload-time = "2025-11-14T09:49:16.423Z" }, + { url = "https://files.pythonhosted.org/packages/90/f6/d6ea1ce944adb3e2c77abc84470a825854428c72e71efe5742bad1c1b1cd/pyobjc_framework_fsevents-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:90819f2fe0516443f679273b128c212d9e6802570f2f1c8a1e190fed76e2dc48", size = 13033, upload-time = "2025-11-14T09:49:18.658Z" }, + { url = "https://files.pythonhosted.org/packages/be/73/62129609d6ef33987351297d052d25ff042d2d9a3876767915e8dc75d183/pyobjc_framework_fsevents-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:028f6a3195c6a00ca29baef31019cb2ca0c54e799072f0f0246b391dc6c4c1d3", size = 13495, upload-time = "2025-11-14T09:49:20.545Z" }, +] + +[[package]] +name = "pyobjc-framework-fskit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/55/d00246d6e6d9756e129e1d94bc131c99eece2daa84b2696f6442b8a22177/pyobjc_framework_fskit-12.1.tar.gz", hash = "sha256:ec54e941cdb0b7d800616c06ca76a93685bd7119b8aa6eb4e7a3ee27658fc7ba", size = 42372, upload-time = "2025-11-14T10:15:30.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/a9/0c47469fe80fa14bc698bb0a5b772b44283cc3aca0f67e7f70ab45e09b24/pyobjc_framework_fskit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:50972897adea86508cfee33ec4c23aa91dede97e9da1640ea2fe74702b065be1", size = 20250, upload-time = "2025-11-14T09:49:28.065Z" }, + { url = "https://files.pythonhosted.org/packages/ce/99/eb30b8b99a4d62ff90b8aa66c6074bf6e2732705a3a8f086ba623fcc642f/pyobjc_framework_fskit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:528b988ea6af1274c81ff698f802bb55a12e32633862919dd4b303ec3b941fae", size = 20258, upload-time = "2025-11-14T09:49:30.893Z" }, + { url = "https://files.pythonhosted.org/packages/50/b6/0579127ff0ad03f6b8f26a7e856e5c9998c9b0efb7ac944b27e23136acf7/pyobjc_framework_fskit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:55e3e00e51bc33d43ed57efb9ceb252abfceba0bd563dae07c7b462da7add849", size = 20491, upload-time = "2025-11-14T09:49:33.249Z" }, + { url = "https://files.pythonhosted.org/packages/7f/4a/10a5d0a35ab18129289e0dfa2ab56469af2f1a9b2c8eeccd814d9c171e63/pyobjc_framework_fskit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d856df1b12ef79803e11904571411ffe5720ceb8840f489ca7ec977c1d789e57", size = 20291, upload-time = "2025-11-14T09:49:35.636Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/cd618c1ea92f2bc8450bc3caa9c3f01ab54536a8d437b4df22f075b9d654/pyobjc_framework_fskit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1fc9ccf7a0f483ce98274ed89bc91226c3f1aaa32cb380b4fdd8b258317cc8fb", size = 20538, upload-time = "2025-11-14T09:49:37.962Z" }, +] + +[[package]] +name = "pyobjc-framework-gamecenter" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/f8/b5fd86f6b722d4259228922e125b50e0a6975120a1c4d957e990fb84e42c/pyobjc_framework_gamecenter-12.1.tar.gz", hash = "sha256:de4118f14c9cf93eb0316d49da410faded3609ce9cd63425e9ef878cebb7ea72", size = 31473, upload-time = "2025-11-14T10:15:33.38Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/ee/b496cc4248c5b901e159d6d9a437da9b86a3105fc3999a66744ba2b2c884/pyobjc_framework_gamecenter-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e8d6d10b868be7c00c2d5a0944cc79315945735dcf17eaa3fec1a7986d26be9b", size = 18868, upload-time = "2025-11-14T09:49:44.767Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b4/d89eaeae9057e5fc6264ad47247739160650dfd02b1e85a84d45036f25f9/pyobjc_framework_gamecenter-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c885eae6ad29abb8d3ad17a9068c920f778622bff5401df31842fdbcebdd84", size = 18873, upload-time = "2025-11-14T09:49:47.072Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/e5fe5a8f80288e61d70b6f9ccf05cffe6f1809736c11f172570af24216f6/pyobjc_framework_gamecenter-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9112d7aa8807d4b18a3f7190f310d60380640faaf405a1d0a9fd066c6420ae5b", size = 19154, upload-time = "2025-11-14T09:49:49.26Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fb/5b4f1bd82e324f2fb598d3131f626744b6fbc9f87feda894bc854058de66/pyobjc_framework_gamecenter-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c452f65aaa102c11196193f44d41061ce33a66be2e9cf79d890d8eb611f84aa9", size = 18923, upload-time = "2025-11-14T09:49:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/22/93/96305e0e96610a489604d15746a14f648b70dad44a8a7ca8a89ec31e12f4/pyobjc_framework_gamecenter-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:55352b0b4cf6803b3489a9dc63b6c177df462fbc4fee7902a4576af067e41714", size = 19214, upload-time = "2025-11-14T09:49:53.675Z" }, +] + +[[package]] +name = "pyobjc-framework-gamecontroller" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/14/353bb1fe448cd833839fd199ab26426c0248088753e63c22fe19dc07530f/pyobjc_framework_gamecontroller-12.1.tar.gz", hash = "sha256:64ed3cc4844b67f1faeb540c7cc8d512c84f70b3a4bafdb33d4663a2b2a2b1d8", size = 54554, upload-time = "2025-11-14T10:15:37.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/28/9f03d0ef7c78340441f78b19fb2d2c952af04a240da5ed30c7cf2d0d0f4e/pyobjc_framework_gamecontroller-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:878aa6590c1510e91bfc8710d6c880e7a8f3656a7b7b6f4f3af487a6f677ccd5", size = 20949, upload-time = "2025-11-14T09:50:01.608Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7c/4553f7c37eedef4cd2e6f0d9b6c63da556ed2fbe7dd2a79735654e082932/pyobjc_framework_gamecontroller-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2105b4309222e538b9bccf906d24f083c3cbf1cd1c18b3ae6876e842e84d2163", size = 20956, upload-time = "2025-11-14T09:50:04.123Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ed/19e27404ce87256642431a60914ef2cb0578142727981714d494970e21c3/pyobjc_framework_gamecontroller-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a772cc9fbe09bcc601abcc36855a70cbad4640bd3349c1d611c09fcc7e45b73b", size = 21226, upload-time = "2025-11-14T09:50:06.462Z" }, + { url = "https://files.pythonhosted.org/packages/38/0a/4386a2436b7ae4df62c30b8a96d89be15c6c9e302b89fc7e7cd19ba3429c/pyobjc_framework_gamecontroller-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:3404a6488bb498989304aa87ce6217c973505a627b6eb9ae7884fd804569b8e4", size = 21005, upload-time = "2025-11-14T09:50:08.894Z" }, + { url = "https://files.pythonhosted.org/packages/c1/94/7e45309ddb873b7ea4ac172e947021a9ecdb7dc0b58415d1574abcd87cce/pyobjc_framework_gamecontroller-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f4a16cd469aec142ec8e199d52a797f771441b3ea7198d21f6d75c2cc218b4e6", size = 21266, upload-time = "2025-11-14T09:50:11.271Z" }, +] + +[[package]] +name = "pyobjc-framework-gamekit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/7b/d625c0937557f7e2e64200fdbeb867d2f6f86b2f148b8d6bfe085e32d872/pyobjc_framework_gamekit-12.1.tar.gz", hash = "sha256:014d032c3484093f1409f8f631ba8a0fd2ff7a3ae23fd9d14235340889854c16", size = 63833, upload-time = "2025-11-14T10:15:42.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/05/1c49e1030dc9f2812fa8049442158be76c32f271075f4571f94e4389ea86/pyobjc_framework_gamekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2eee796d5781157f2c5684f7ef4c2a7ace9d9b408a26a9e7e92e8adf5a3f63d7", size = 22493, upload-time = "2025-11-14T09:50:19.129Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7d/65b16b18dc15283d6f56df5ebf30ae765eaf1f8e67e6eb30539581fe9749/pyobjc_framework_gamekit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ad14393ac496a4cb8008b6172d536f5c07fc11bb7b00fb541b044681cf9e4a34", size = 22505, upload-time = "2025-11-14T09:50:21.989Z" }, + { url = "https://files.pythonhosted.org/packages/98/19/433595ff873684e0df73067b32aba6fc4b360d3ed552444115285a5d969a/pyobjc_framework_gamekit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97e41b4800be30cb3e6a88007b6f741cb18935467d1631537ac23b918659900e", size = 22798, upload-time = "2025-11-14T09:50:24.583Z" }, + { url = "https://files.pythonhosted.org/packages/05/39/4a9a51cae1ced9d0f74ca6c68e7304b9b1c2d184fed11b736947535ba59f/pyobjc_framework_gamekit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:14080fdea98ec01c3e06260f1f5b31aaf59c78c2872fe8b843e17fd0ce151fa4", size = 22536, upload-time = "2025-11-14T09:50:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/282f10f5ebd427ec1774ef639a467e5b26c5174f473e8da24ac084139a7c/pyobjc_framework_gamekit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9867991539dfc70b52f0ee8ce19bc661d0706c7f64c35417e97ca7c90e3158c0", size = 22845, upload-time = "2025-11-14T09:50:30.287Z" }, +] + +[[package]] +name = "pyobjc-framework-gameplaykit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-spritekit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/11/c310bbc2526f95cce662cc1f1359bb11e2458eab0689737b4850d0f6acb7/pyobjc_framework_gameplaykit-12.1.tar.gz", hash = "sha256:935ebd806d802888969357946245d35a304c530c86f1ffe584e2cf21f0a608a8", size = 41511, upload-time = "2025-11-14T10:15:46.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/1f/e5fe404f92ec0f9c8c37b00d6cb3ba96ee396c7f91b0a41a39b64bfc2743/pyobjc_framework_gameplaykit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:309b0d7479f702830c9be92dbe5855ac2557a9d23f05f063caf9d9fdb85ff5f0", size = 13150, upload-time = "2025-11-14T09:50:36.884Z" }, + { url = "https://files.pythonhosted.org/packages/08/c9/d90505bed51b487d7a8eff54a51dda0d9b8e2d76740a99924b5067b58062/pyobjc_framework_gameplaykit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:947911902e0caf1d82dedae8842025891d57e91504714a7732dc7c4f80d486a1", size = 13164, upload-time = "2025-11-14T09:50:39.251Z" }, + { url = "https://files.pythonhosted.org/packages/ad/42/9d5ac9a4398f1d1566ce83f16f68aeaa174137de78bec4515ed927c24530/pyobjc_framework_gameplaykit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3218de7a56ac63a47ab7c50ce30592d626759196c937d20426a0ea74091e0614", size = 13383, upload-time = "2025-11-14T09:50:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/38/a5/e10365b7287eb4a8e83275f04942d085f8e87da0a65c375df14a78df23c8/pyobjc_framework_gameplaykit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:786036bdf266faf196b29b23e123faf76df5f3e90f113e2a7cdd4d04af071dc2", size = 13170, upload-time = "2025-11-14T09:50:43.238Z" }, + { url = "https://files.pythonhosted.org/packages/a3/65/eb00ab56a00f048d1638bb819f61d3e8221d72088947070ac9367bc17efa/pyobjc_framework_gameplaykit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d58c0cc671ac8b80a4bf702efabbb9c0a42020999b87efed162b71830db005a9", size = 13363, upload-time = "2025-11-14T09:50:45.394Z" }, +] + +[[package]] +name = "pyobjc-framework-gamesave" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/1f/8d05585c844535e75dbc242dd6bdfecfc613d074dcb700362d1c908fb403/pyobjc_framework_gamesave-12.1.tar.gz", hash = "sha256:eb731c97aa644e78a87838ed56d0e5bdbaae125bdc8854a7772394877312cc2e", size = 12654, upload-time = "2025-11-14T10:15:48.344Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/ec/93d48cb048a1b35cea559cc9261b07f0d410078b3af029121302faa410d0/pyobjc_framework_gamesave-12.1-py2.py3-none-any.whl", hash = "sha256:432e69f8404be9290d42c89caba241a3156ed52013947978ac54f0f032a14ffd", size = 3689, upload-time = "2025-11-14T09:50:47.263Z" }, +] + +[[package]] +name = "pyobjc-framework-healthkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/67/436630d00ba1028ea33cc9df2fc28e081481433e5075600f2ea1ff00f45e/pyobjc_framework_healthkit-12.1.tar.gz", hash = "sha256:29c5e5de54b41080b7a4b0207698ac6f600dcb9149becc9c6b3a69957e200e5c", size = 91802, upload-time = "2025-11-14T10:15:54.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/87/bb1c438de51c4fa733a99ce4d3301e585f14d7efd94031a97707c0be2b46/pyobjc_framework_healthkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:15b6fc958ff5de42888b18dffdec839cb36d2dd8b82076ed2f21a51db5271109", size = 20799, upload-time = "2025-11-14T09:50:54.531Z" }, + { url = "https://files.pythonhosted.org/packages/40/f8/4bbaf71a11a99649a4aa9f4ac28d94a2bf357cd4c88fba91439000301cf0/pyobjc_framework_healthkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c57ba8e3cce620665236d9f6b77482c9cfb16fe3372c8b6bbabc50222fb1b790", size = 20812, upload-time = "2025-11-14T09:50:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ef/4461f34f42e8f78b941161df7045d27e48d73d203847a21921b5a36ffe68/pyobjc_framework_healthkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b2a0890d920015b40afe8ecda6c541840d20b4ae6c7f2daaa9efbaafae8cc1bc", size = 20980, upload-time = "2025-11-14T09:50:59.644Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6f/99933449e0cb8d6424de8e709fe423427efc634f75930885a723debcce11/pyobjc_framework_healthkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1f10a3abf6d5a326192e96343e7e1d9d16efa0cf4b39266335e385455680bc69", size = 20867, upload-time = "2025-11-14T09:51:02.359Z" }, + { url = "https://files.pythonhosted.org/packages/63/ad/7ea9a3bc54c092efb5dbf9b571dd6a1a064712ce434e80c42e2830f88bb5/pyobjc_framework_healthkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:54f02b673b2ea8ec8cfa17cac0c377435cbf89a15d5539d4699fa8b12abc42de", size = 21039, upload-time = "2025-11-14T09:51:04.699Z" }, +] + +[[package]] +name = "pyobjc-framework-imagecapturecore" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/a1/39347381fc7d3cd5ab942d86af347b25c73f0ddf6f5227d8b4d8f5328016/pyobjc_framework_imagecapturecore-12.1.tar.gz", hash = "sha256:c4776c59f4db57727389d17e1ffd9c567b854b8db52198b3ccc11281711074e5", size = 46397, upload-time = "2025-11-14T10:15:58.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/13/632957b284dec3743d73fb30dbdf03793b3cf1b4c62e61e6484d870f3879/pyobjc_framework_imagecapturecore-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a2777e17ff71fb5a327a897e48c5c7b5a561723a80f990d26e6ed5a1b8748816", size = 16012, upload-time = "2025-11-14T09:51:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/f9/32/2d936320147f299d83c14af4eb8e28821d226f2920d2df3f7a3b3daf61dc/pyobjc_framework_imagecapturecore-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2ae57b54e7b92e2efb40b7346e12d7767f42ed2bcf8f050cd9a88a9926a1e387", size = 16025, upload-time = "2025-11-14T09:51:14.387Z" }, + { url = "https://files.pythonhosted.org/packages/09/5a/7bfa64b0561c7eb858dac9b2e0e3a50000e9dc50416451e8ae40b316eb8f/pyobjc_framework_imagecapturecore-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:08f8ed5434ee5cc7605e71227c284c0c3fa0a32a6d83e1862e7870543a65a630", size = 16213, upload-time = "2025-11-14T09:51:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/50/fc/feb035f2866050737f8315958e31cfe2bf5d6d4d046a7268d28b94cd8155/pyobjc_framework_imagecapturecore-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b7a7feeb0b53f5b0e0305c5c41f6b722d5f8cfca506c49678902244cd339ac10", size = 16028, upload-time = "2025-11-14T09:51:18.573Z" }, + { url = "https://files.pythonhosted.org/packages/38/58/58c3d369d90077eff896c234755ac6814b3fa9f00caeca2ec391555b1a22/pyobjc_framework_imagecapturecore-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1fcfcc907673331cc4be3ea63fce6e1346620ac74661a19566dfcdf855bb8eee", size = 16207, upload-time = "2025-11-14T09:51:20.616Z" }, +] + +[[package]] +name = "pyobjc-framework-inputmethodkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/b8/d33dd8b7306029bbbd80525bf833fc547e6a223c494bf69a534487283a28/pyobjc_framework_inputmethodkit-12.1.tar.gz", hash = "sha256:f63b6fe2fa7f1412eae63baea1e120e7865e3b68ccfb7d8b0a4aadb309f2b278", size = 23054, upload-time = "2025-11-14T10:16:01.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c2/59bea66405784b25f5d4e821467ba534a0b92dfc98e07257c971e2a8ed73/pyobjc_framework_inputmethodkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0b7d813d46a060572fc0c14ef832e4fe538ebf64e5cab80ee955191792ce0110", size = 9506, upload-time = "2025-11-14T09:51:26.924Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ec/502019d314729e7e82a7fa187dd52b6f99a6097ac0ab6dc675ccd60b5677/pyobjc_framework_inputmethodkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b5c7458082e3f7e8bb115ed10074ad862cc6566da7357540205d3cd1e24e2b9f", size = 9523, upload-time = "2025-11-14T09:51:30.751Z" }, + { url = "https://files.pythonhosted.org/packages/47/68/76a75461de5b9c195a6b5081179578fef7136f19ffc4990f6591cabae591/pyobjc_framework_inputmethodkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a4e782edd8e59b1ea81ea688d27edbf98cc5c8262e081cb772cf8c36c74733df", size = 9694, upload-time = "2025-11-14T09:51:32.616Z" }, + { url = "https://files.pythonhosted.org/packages/76/f8/6915cc42826e1178c18cc9232edda15ef5d1f57950eef8fd6f8752853b9c/pyobjc_framework_inputmethodkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:3b27c166574ad08d196129c979c5eec891cd630d249c75a970e26f3949578cb9", size = 9574, upload-time = "2025-11-14T09:51:34.366Z" }, + { url = "https://files.pythonhosted.org/packages/97/36/6d3debe09cf1fbcb40b15cc29e7cdc04b07a2f14815d0ffcdcb4a3823ead/pyobjc_framework_inputmethodkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1f065cb44041821a1812861e13ee1eca4aee37b57c8de0ce7ffd7e55f7af8907", size = 9746, upload-time = "2025-11-14T09:51:36.034Z" }, +] + +[[package]] +name = "pyobjc-framework-installerplugins" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/60/ca4ab04eafa388a97a521db7d60a812e2f81a3c21c2372587872e6b074f9/pyobjc_framework_installerplugins-12.1.tar.gz", hash = "sha256:1329a193bd2e92a2320a981a9a421a9b99749bade3e5914358923e94fe995795", size = 25277, upload-time = "2025-11-14T10:16:04.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/1f/31dca45db3342882a628aa1b27707a283d4dc7ef558fddd2533175a0661a/pyobjc_framework_installerplugins-12.1-py2.py3-none-any.whl", hash = "sha256:d2201c81b05bdbe0abf0af25db58dc230802573463bea322f8b2863e37b511d5", size = 4813, upload-time = "2025-11-14T09:51:37.836Z" }, +] + +[[package]] +name = "pyobjc-framework-instantmessage" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/67/66754e0d26320ba24a33608ca94d3f38e60ee6b2d2e094cb6269b346fdd4/pyobjc_framework_instantmessage-12.1.tar.gz", hash = "sha256:f453118d5693dc3c94554791bd2aaafe32a8b03b0e3d8ec3934b44b7fdd1f7e7", size = 31217, upload-time = "2025-11-14T10:16:07.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/38/6ae95b5c87d887c075bd5f4f7cca3d21dafd0a77cfdde870e87ca17579eb/pyobjc_framework_instantmessage-12.1-py2.py3-none-any.whl", hash = "sha256:cd91d38e8f356afd726b6ea8c235699316ea90edfd3472965c251efbf4150bc9", size = 5436, upload-time = "2025-11-14T09:51:39.557Z" }, +] + +[[package]] +name = "pyobjc-framework-intents" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/a1/3bab6139e94b97eca098e1562f5d6840e3ff10ea1f7fd704a17111a97d5b/pyobjc_framework_intents-12.1.tar.gz", hash = "sha256:bd688c3ab34a18412f56e459e9dae29e1f4152d3c2048fcacdef5fc49dfb9765", size = 132262, upload-time = "2025-11-14T10:16:16.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/90/e9489492ae90b4c1ffd02c1221c0432b8768d475787e7887f79032c2487a/pyobjc_framework_intents-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ea9f3e79bf4baf6c7b0fd2d2797184ed51a372bf7f32974b4424f9bd067ef50", size = 32156, upload-time = "2025-11-14T09:51:49.438Z" }, + { url = "https://files.pythonhosted.org/packages/74/83/6b03ac6d5663be41d76ab69412a21f94eff69c67ffa13516a91e4b946890/pyobjc_framework_intents-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1da8d1501c8c85198dfbc4623ea18db96077f9947f6e1fe5ffa2ed06935e8a3b", size = 32168, upload-time = "2025-11-14T09:51:52.888Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f8/1fd0a75de415d335a1aa43e9c86e468960b3a4d969a87aa4a70084452277/pyobjc_framework_intents-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:50ab244f2a9ad4c94bbc1dd81421f8553f59121d4e0ad0c894a927a878319843", size = 32413, upload-time = "2025-11-14T09:51:56.057Z" }, + { url = "https://files.pythonhosted.org/packages/42/8a/d319b1a014dcf52cd46c2c956bed0e66f7c80253acaebd1ec5920b01bf41/pyobjc_framework_intents-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:5c50c336418a3ba8fdfa5b5d12e46dca290e4321fb9844245af4a32b11cf6563", size = 32191, upload-time = "2025-11-14T09:51:59.097Z" }, + { url = "https://files.pythonhosted.org/packages/38/cd/b5ce5d389a3ca767b3d0ce70daf35c52cb35775e4a285ed4bedaa89ab75e/pyobjc_framework_intents-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:03cbccec0380a431bc291725af0fcbaf61ea1bb1301a70cb267c8ecf2d04d608", size = 32481, upload-time = "2025-11-14T09:52:02.16Z" }, +] + +[[package]] +name = "pyobjc-framework-intentsui" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-intents" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/cf/f0e385b9cfbf153d68efe8d19e5ae672b59acbbfc1f9b58faaefc5ec8c9e/pyobjc_framework_intentsui-12.1.tar.gz", hash = "sha256:16bdf4b7b91c0d1ec9d5513a1182861f1b5b7af95d4f4218ff7cf03032d57f99", size = 19784, upload-time = "2025-11-14T10:16:18.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/17/06812542a9028f5b2dcce56f52f25633c08b638faacd43bad862aad1b41d/pyobjc_framework_intentsui-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cb894fcc4c9ea613a424dcf6fb48142d51174559b82cfdafac8cb47555c842cf", size = 8983, upload-time = "2025-11-14T09:52:07.667Z" }, + { url = "https://files.pythonhosted.org/packages/57/af/4dc8b6f714ba1bd9cf0218da98c49ece5dcee4e0593b59196ec5aa85e07c/pyobjc_framework_intentsui-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:369a88db1ff3647e4d8cf38d315f1e9b381fc7732d765b08994036f9d330f57d", size = 9004, upload-time = "2025-11-14T09:52:09.625Z" }, + { url = "https://files.pythonhosted.org/packages/18/ab/794ed92dcf955dc2d0a0dcfbc384e087864f2dacd330d59d1185f8403353/pyobjc_framework_intentsui-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8742e9237ef2df8dbb1566cdc77e4d747b2693202f438d49435e0c3c91eaa709", size = 9177, upload-time = "2025-11-14T09:52:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/68/07/61dc855f6eeaf75d274ad4b66006e05b0bef2138a6a559c60f0bc59d32ea/pyobjc_framework_intentsui-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d01222760005421324c3892b6b98c5b4295828a6b157a1fc410f63eb336b2d97", size = 9054, upload-time = "2025-11-14T09:52:12.896Z" }, + { url = "https://files.pythonhosted.org/packages/76/fa/d6dabff68951b66f2d7d8c8aa651f2a139a1ca0be556e1e64c6bdd7be18b/pyobjc_framework_intentsui-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:547aef7233b6c7495b3c679aa779f01368fc992883732ade065523235f07fa3b", size = 9248, upload-time = "2025-11-14T09:52:14.936Z" }, +] + +[[package]] +name = "pyobjc-framework-iobluetooth" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/aa/ca3944bbdfead4201b4ae6b51510942c5a7d8e5e2dc3139a071c74061fdf/pyobjc_framework_iobluetooth-12.1.tar.gz", hash = "sha256:8a434118812f4c01dfc64339d41fe8229516864a59d2803e9094ee4cbe2b7edd", size = 155241, upload-time = "2025-11-14T10:16:28.896Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/b6/933b56afb5e84c3c35c074c9e30d7b701c6038989d4867867bdaa7ab618b/pyobjc_framework_iobluetooth-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:111a6e54be9e9dcf77fa2bf84fdac09fae339aa33087d8647ea7ffbd34765d4c", size = 40439, upload-time = "2025-11-14T09:52:26.071Z" }, + { url = "https://files.pythonhosted.org/packages/15/6f/5e165daaf3b637d37fee50f42beda62ab3d5e6e99b1d84c4af4700d39d01/pyobjc_framework_iobluetooth-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2ee0d4fdddf871fb89c49033495ae49973cc8b0e8de50c2e60c92355ce3bea86", size = 40452, upload-time = "2025-11-14T09:52:29.68Z" }, + { url = "https://files.pythonhosted.org/packages/37/bd/7cc5f01fbf573112059766c94535ae3f9c044d6e0cf49c599e490224db58/pyobjc_framework_iobluetooth-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0cd2ea9384e93913703bf40641196a930af83c2f6f62f59f8606b7162fe1caa3", size = 40659, upload-time = "2025-11-14T09:52:33.299Z" }, + { url = "https://files.pythonhosted.org/packages/ef/58/4553d846513840622cd56ef715543f922d7d5ddfbe38316dbc7e43f23832/pyobjc_framework_iobluetooth-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a14506046ad9403ea95c75c1dd248167f41aef4aed62f50b567bf2482056ebf5", size = 40443, upload-time = "2025-11-14T09:52:37.21Z" }, + { url = "https://files.pythonhosted.org/packages/8a/da/4846a76bd9cb73fb1e562d1fb7044bd3df15a289ab986bcaf053a65dbb88/pyobjc_framework_iobluetooth-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:42ec9a40e7234a00f434489c8b18458bc5deb6ea6938daba50b9527100e21f0c", size = 40649, upload-time = "2025-11-14T09:52:40.793Z" }, +] + +[[package]] +name = "pyobjc-framework-iobluetoothui" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-iobluetooth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/39/31d9a4e8565a4b1ec0a9ad81480dc0879f3df28799eae3bc22d1dd53705d/pyobjc_framework_iobluetoothui-12.1.tar.gz", hash = "sha256:81f8158bdfb2966a574b6988eb346114d6a4c277300c8c0a978c272018184e6f", size = 16495, upload-time = "2025-11-14T10:16:31.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/c9/69aeda0cdb5d25d30dc4596a1c5b464fc81b5c0c4e28efc54b7e11bde51c/pyobjc_framework_iobluetoothui-12.1-py2.py3-none-any.whl", hash = "sha256:a6d8ab98efa3029130577a57ee96b183c35c39b0f1c53a7534f8838260fab993", size = 4045, upload-time = "2025-11-14T09:52:42.201Z" }, +] + +[[package]] +name = "pyobjc-framework-iosurface" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/61/0f12ad67a72d434e1c84b229ec760b5be71f53671ee9018593961c8bfeb7/pyobjc_framework_iosurface-12.1.tar.gz", hash = "sha256:4b9d0c66431aa296f3ca7c4f84c00dc5fc961194830ad7682fdbbc358fa0db55", size = 17690, upload-time = "2025-11-14T10:16:33.282Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ad/793d98a7ed9b775dc8cce54144cdab0df1808a1960ee017e46189291a8f3/pyobjc_framework_iosurface-12.1-py2.py3-none-any.whl", hash = "sha256:e784e248397cfebef4655d2c0025766d3eaa4a70474e363d084fc5ce2a4f2a3f", size = 4902, upload-time = "2025-11-14T09:52:43.899Z" }, +] + +[[package]] +name = "pyobjc-framework-ituneslibrary" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/46/d9bcec88675bf4ee887b9707bd245e2a793e7cb916cf310f286741d54b1f/pyobjc_framework_ituneslibrary-12.1.tar.gz", hash = "sha256:7f3aa76c4d05f6fa6015056b88986cacbda107c3f29520dd35ef0936c7367a6e", size = 23730, upload-time = "2025-11-14T10:16:36.127Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/92/b598694a1713ee46f45c4bfb1a0425082253cbd2b1caf9f8fd50f292b017/pyobjc_framework_ituneslibrary-12.1-py2.py3-none-any.whl", hash = "sha256:fb678d7c3ff14c81672e09c015e25880dac278aa819971f4d5f75d46465932ef", size = 5205, upload-time = "2025-11-14T09:52:45.733Z" }, +] + +[[package]] +name = "pyobjc-framework-kernelmanagement" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/7e/ecbac119866e8ac2cce700d7a48a4297946412ac7cbc243a7084a6582fb1/pyobjc_framework_kernelmanagement-12.1.tar.gz", hash = "sha256:488062893ac2074e0c8178667bf864a21f7909c11111de2f6a10d9bc579df59d", size = 11773, upload-time = "2025-11-14T10:16:38.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/32/04325a20f39d88d6d712437e536961a9e6a4ec19f204f241de6ed54d1d84/pyobjc_framework_kernelmanagement-12.1-py2.py3-none-any.whl", hash = "sha256:926381bfbfbc985c3e6dfcb7004af21bb16ff66ecbc08912b925989a705944ff", size = 3704, upload-time = "2025-11-14T09:52:47.268Z" }, +] + +[[package]] +name = "pyobjc-framework-latentsemanticmapping" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/3c/b621dac54ae8e77ac25ee75dd93e310e2d6e0faaf15b8da13513258d6657/pyobjc_framework_latentsemanticmapping-12.1.tar.gz", hash = "sha256:f0b1fa823313eefecbf1539b4ed4b32461534b7a35826c2cd9f6024411dc9284", size = 15526, upload-time = "2025-11-14T10:16:40.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/8e/74a7eb29b545f294485cd3cf70557b4a35616555fe63021edbb3e0ea4c20/pyobjc_framework_latentsemanticmapping-12.1-py2.py3-none-any.whl", hash = "sha256:7d760213b42bc8b1bc1472e1873c0f78ee80f987225978837b1fecdceddbdbf4", size = 5471, upload-time = "2025-11-14T09:52:48.939Z" }, +] + +[[package]] +name = "pyobjc-framework-launchservices" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-coreservices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/d0/24673625922b0ad21546be5cf49e5ec1afaa4553ae92f222adacdc915907/pyobjc_framework_launchservices-12.1.tar.gz", hash = "sha256:4d2d34c9bd6fb7f77566155b539a2c70283d1f0326e1695da234a93ef48352dc", size = 20470, upload-time = "2025-11-14T10:16:42.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/af/9a0aebaab4c15632dc8fcb3669c68fa541a3278d99541d9c5f966fbc0909/pyobjc_framework_launchservices-12.1-py2.py3-none-any.whl", hash = "sha256:e63e78fceeed4d4dc807f9dabd5cf90407e4f552fab6a0d75a8d0af63094ad3c", size = 3905, upload-time = "2025-11-14T09:52:50.71Z" }, +] + +[[package]] +name = "pyobjc-framework-libdispatch" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/e8/75b6b9b3c88b37723c237e5a7600384ea2d84874548671139db02e76652b/pyobjc_framework_libdispatch-12.1.tar.gz", hash = "sha256:4035535b4fae1b5e976f3e0e38b6e3442ffea1b8aa178d0ca89faa9b8ecdea41", size = 38277, upload-time = "2025-11-14T10:16:46.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/6f/96e15c7b2f7b51fc53252216cd0bed0c3541bc0f0aeb32756fefd31bed7d/pyobjc_framework_libdispatch-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e9570d7a9a3136f54b0b834683bf3f206acd5df0e421c30f8fd4f8b9b556789", size = 15650, upload-time = "2025-11-14T09:52:59.284Z" }, + { url = "https://files.pythonhosted.org/packages/38/3a/d85a74606c89b6b293782adfb18711026ff79159db20fc543740f2ac0bc7/pyobjc_framework_libdispatch-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:58ffce5e6bcd7456b4311009480b195b9f22107b7682fb0835d4908af5a68ad0", size = 15668, upload-time = "2025-11-14T09:53:01.354Z" }, + { url = "https://files.pythonhosted.org/packages/cc/40/49b1c1702114ee972678597393320d7b33f477e9d24f2a62f93d77f23dfb/pyobjc_framework_libdispatch-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e9f49517e253716e40a0009412151f527005eec0b9a2311ac63ecac1bdf02332", size = 15938, upload-time = "2025-11-14T09:53:03.461Z" }, + { url = "https://files.pythonhosted.org/packages/59/d8/7d60a70fc1a546c6cb482fe0595cb4bd1368d75c48d49e76d0bc6c0a2d0f/pyobjc_framework_libdispatch-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0ebfd9e4446ab6528126bff25cfb09e4213ddf992b3208978911cfd3152e45f5", size = 15693, upload-time = "2025-11-14T09:53:05.531Z" }, + { url = "https://files.pythonhosted.org/packages/99/32/15e08a0c4bb536303e1568e2ba5cae1ce39a2e026a03aea46173af4c7a2d/pyobjc_framework_libdispatch-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:23fc9915cba328216b6a736c7a48438a16213f16dfb467f69506300b95938cc7", size = 15976, upload-time = "2025-11-14T09:53:07.936Z" }, +] + +[[package]] +name = "pyobjc-framework-libxpc" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/e4/364db7dc26f235e3d7eaab2f92057f460b39800bffdec3128f113388ac9f/pyobjc_framework_libxpc-12.1.tar.gz", hash = "sha256:e46363a735f3ecc9a2f91637750623f90ee74f9938a4e7c833e01233174af44d", size = 35186, upload-time = "2025-11-14T10:16:49.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/7f/fdec72430f90921b154517a6f9bbeefa7bacfb16b91320742eb16a5955c5/pyobjc_framework_libxpc-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ba93e91e9ca79603dd265382e9f80e9bd32309cd09c8ac3e6489fc5b233676c8", size = 19730, upload-time = "2025-11-14T09:53:17.113Z" }, + { url = "https://files.pythonhosted.org/packages/0a/64/c4e2f9a4f92f4d2b84c0e213b4a9410968b5f181f15a764eeb43f92c4eb2/pyobjc_framework_libxpc-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:635520187a6456ad259e40dd04829caeef08561d0a1a0cfd09787ebd281d47b3", size = 19729, upload-time = "2025-11-14T09:53:19.038Z" }, + { url = "https://files.pythonhosted.org/packages/51/c2/654dd2a22b6f505ff706a66117c522029df9449a9a19ca4827af0d16b5b3/pyobjc_framework_libxpc-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1c36e3e109a95275f90b319161265a7f6a5e0e674938ce49babdf3a64d9fc892", size = 20309, upload-time = "2025-11-14T09:53:22.657Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9d/d66559d9183dae383962c79ca67eaabf7fe9f8bb9f65cf5a4369fbdcdd0e/pyobjc_framework_libxpc-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:bc5eaed7871fab8971631e99151ea0271f64d4059790c9f41a30ae4841f4fd89", size = 19451, upload-time = "2025-11-14T09:53:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f6/cb5d5e6f83d94cff706dff533423fdf676249ee392dc9ae4acdd0e02d451/pyobjc_framework_libxpc-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c862ed4f79c82e7a246fe49a8fae9e9684a7163512265f1c01790899dc730551", size = 20022, upload-time = "2025-11-14T09:53:26.605Z" }, +] + +[[package]] +name = "pyobjc-framework-linkpresentation" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/58/c0c5919d883485ccdb6dccd8ecfe50271d2f6e6ab7c9b624789235ccec5a/pyobjc_framework_linkpresentation-12.1.tar.gz", hash = "sha256:84df6779591bb93217aa8bd82c10e16643441678547d2d73ba895475a02ade94", size = 13330, upload-time = "2025-11-14T10:16:52.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/51/226eb45f196f3bf93374713571aae6c8a4760389e1d9435c4a4cc3f38ea4/pyobjc_framework_linkpresentation-12.1-py2.py3-none-any.whl", hash = "sha256:853a84c7b525b77b114a7a8d798aef83f528ed3a6803bda12184fe5af4e79a47", size = 3865, upload-time = "2025-11-14T09:53:28.386Z" }, +] + +[[package]] +name = "pyobjc-framework-localauthentication" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-security" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/0e/7e5d9a58bb3d5b79a75d925557ef68084171526191b1c0929a887a553d4f/pyobjc_framework_localauthentication-12.1.tar.gz", hash = "sha256:2284f587d8e1206166e4495b33f420c1de486c36c28c4921d09eec858a699d05", size = 29947, upload-time = "2025-11-14T10:16:54.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/93/91761ad4e5fa1c3ec25819865d1ccfbee033987147087bff4fcce67a4dc4/pyobjc_framework_localauthentication-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3af1acd287d830cc7f912f46cde0dab054952bde0adaf66c8e8524311a68d279", size = 10773, upload-time = "2025-11-14T09:53:34.074Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f5/a12c76525e4839c7fc902c6b0f0c441414a4dd9bc9a2d89ae697f6cd8850/pyobjc_framework_localauthentication-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e26e746717f4774cce0568debec711f1d8effc430559ad634ff6b06fefd0a0bf", size = 10792, upload-time = "2025-11-14T09:53:35.876Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ed/2714934b027afc6a99d0d817e42bf482d08c711422795fe777e3cd9ad8be/pyobjc_framework_localauthentication-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02357cddc979aa169782bf09f380aab1c3af475c9eb6ffb07c77084ed10f6a6a", size = 10931, upload-time = "2025-11-14T09:53:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/e6/58/6dfb304103b4cdaee44acd7f5093c07f3053df0cc9648c87876f1e5fc690/pyobjc_framework_localauthentication-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f8d525ed2ad5cd56e420436187b534454d1f7d1fae6e585df82397d6d92c6e54", size = 10841, upload-time = "2025-11-14T09:53:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/17/af/1c7ce26b46cc978852895017212cf3637d5334274213265234149e0937d4/pyobjc_framework_localauthentication-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:93c5470a9d60b53afa0faf31d95dc8d6fc3a7ff85c425ab157ea491b6dc3af39", size = 10975, upload-time = "2025-11-14T09:53:41.177Z" }, +] + +[[package]] +name = "pyobjc-framework-localauthenticationembeddedui" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-localauthentication" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/20/83ab4180e29b9a4a44d735c7f88909296c6adbe6250e8e00a156aff753e1/pyobjc_framework_localauthenticationembeddedui-12.1.tar.gz", hash = "sha256:a15ec44bf2769c872e86c6b550b6dd4f58d4eda40ad9ff00272a67d279d1d4e9", size = 13611, upload-time = "2025-11-14T10:16:57.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/7d/0d46639c7a26b6af928ab4c822cd28b733791e02ac28cc84c3014bcf7dc7/pyobjc_framework_localauthenticationembeddedui-12.1-py2.py3-none-any.whl", hash = "sha256:a7ce7b56346597b9f4768be61938cbc8fc5b1292137225b6c7f631b9cde97cd7", size = 3991, upload-time = "2025-11-14T09:53:42.958Z" }, +] + +[[package]] +name = "pyobjc-framework-mailkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/98/3d9028620c1cd32ff4fb031155aba3b5511e980cdd114dd51383be9cb51b/pyobjc_framework_mailkit-12.1.tar.gz", hash = "sha256:d5574b7259baec17096410efcaacf5d45c7bb5f893d4c25cbb7072369799b652", size = 20996, upload-time = "2025-11-14T10:16:59.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/8d/3c968b736a3a8bd9d8e870b39b1c772a013eea1b81b89fc4efad9021a6cb/pyobjc_framework_mailkit-12.1-py2.py3-none-any.whl", hash = "sha256:536ac0c4ea3560364cd159a6512c3c18a744a12e4e0883c07df0f8a2ff21e3fe", size = 4871, upload-time = "2025-11-14T09:53:44.697Z" }, +] + +[[package]] +name = "pyobjc-framework-mapkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-corelocation" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bb/2a668203c20e509a648c35e803d79d0c7f7816dacba74eb5ad8acb186790/pyobjc_framework_mapkit-12.1.tar.gz", hash = "sha256:dbc32dc48e821aaa9b4294402c240adbc1c6834e658a07677b7c19b7990533c5", size = 63520, upload-time = "2025-11-14T10:17:04.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/00/a3de41cdf3e6cd7a144e38999fe1ea9777ad19e19d863f2da862e7affe7b/pyobjc_framework_mapkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:84ad7766271c114bdc423e4e2ff5433e5fc6771a3338b5f8e4b54d0340775800", size = 22518, upload-time = "2025-11-14T09:53:52.727Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f1/db2aa9fa44669b9c060a3ae02d5661052a05868ccba1674543565818fdaf/pyobjc_framework_mapkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ea210ba88bef2468adb5c8303071d86118d630bf37a29d28cf236c13c3bb85ad", size = 22539, upload-time = "2025-11-14T09:53:55.543Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e4/7dd9f7333eea7f4666274f568cac03e4687b442c9b20622f244497700177/pyobjc_framework_mapkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:dfee615b73bb687101f08e7fd839eea2aa8b241563ad4cabbcb075d12f598266", size = 22712, upload-time = "2025-11-14T09:53:58.159Z" }, + { url = "https://files.pythonhosted.org/packages/06/ef/f802b9f0a620039b277374ba36702a0e359fe54e8526dcd90d2b061d2594/pyobjc_framework_mapkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c2f47e813e81cb13e48343108ea3185a856c13bab1cb17e76d0d87568e18459b", size = 22562, upload-time = "2025-11-14T09:54:00.735Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6b/aae01ed3322326e034113140d41a6d7529d2a298d9da3ce1f89184fbeb95/pyobjc_framework_mapkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:59a746ac2d4bb32fca301325430b37cde7959213ce1b6c3e30fa40d6085bf75a", size = 22775, upload-time = "2025-11-14T09:54:03.354Z" }, +] + +[[package]] +name = "pyobjc-framework-mediaaccessibility" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/10/dc1007e56944ed2e981e69e7b2fed2b2202c79b0d5b742b29b1081d1cbdd/pyobjc_framework_mediaaccessibility-12.1.tar.gz", hash = "sha256:cc4e3b1d45e84133d240318d53424eff55968f5c6873c2c53267598853445a3f", size = 16325, upload-time = "2025-11-14T10:17:07.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0c/7fb5462561f59d739192c6d02ba0fd36ad7841efac5a8398a85a030ef7fc/pyobjc_framework_mediaaccessibility-12.1-py2.py3-none-any.whl", hash = "sha256:2ff8845c97dd52b0e5cf53990291e6d77c8a73a7aac0e9235d62d9a4256916d1", size = 4800, upload-time = "2025-11-14T09:54:05.04Z" }, +] + +[[package]] +name = "pyobjc-framework-mediaextension" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-avfoundation" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coremedia" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/aa/1e8015711df1cdb5e4a0aa0ed4721409d39971ae6e1e71915e3ab72423a3/pyobjc_framework_mediaextension-12.1.tar.gz", hash = "sha256:44409d63cc7d74e5724a68e3f9252cb62fd0fd3ccf0ca94c6a33e5c990149953", size = 39425, upload-time = "2025-11-14T10:17:11.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/ed/99038bcf72ec68e452709af10a087c1377c2d595ba4e66d7a2b0775145d2/pyobjc_framework_mediaextension-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:442bc3a759efb5c154cb75d643a5e182297093533fcdd1c24be6f64f68b93371", size = 38973, upload-time = "2025-11-14T09:54:16.701Z" }, + { url = "https://files.pythonhosted.org/packages/01/df/7ecdbac430d2d2844fb2145e26f3e87a8a7692fa669d0629d90f32575991/pyobjc_framework_mediaextension-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0f3bdca0eb11923efc1e3b95beb1e6e01c675fd7809ed7ef0b475334e3562931", size = 38991, upload-time = "2025-11-14T09:54:20.316Z" }, + { url = "https://files.pythonhosted.org/packages/fc/98/88ac2edeb69bde3708ef3f7b6434f810ba89321d8375914ad642c9a575b0/pyobjc_framework_mediaextension-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0101b8495051bac9791a0488530386eefe9c722477a5239c5bd208967d0eaa67", size = 39198, upload-time = "2025-11-14T09:54:23.806Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f0/fcff5206bb1a7ce89b9923ceb3215af767fd3c91dafc9d176ba08d6a3f30/pyobjc_framework_mediaextension-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4f66719c97f508c619368377d768266c58cc783cf5fc51bd9d8e5e0cad0c824c", size = 38980, upload-time = "2025-11-14T09:54:27.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/30/bdea26fe2ca33260edcbd93f212e0141c6e145586d53c58fac4416e0135f/pyobjc_framework_mediaextension-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:eef6ab5104fdfb257e17a73c2e7c11b0db09a94ced24f2a4948e1d593ec6200e", size = 39191, upload-time = "2025-11-14T09:54:30.798Z" }, +] + +[[package]] +name = "pyobjc-framework-medialibrary" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/e9/848ebd02456f8fdb41b42298ec585bfed5899dbd30306ea5b0a7e4c4b341/pyobjc_framework_medialibrary-12.1.tar.gz", hash = "sha256:690dcca09b62511df18f58e8566cb33d9652aae09fe63a83f594bd018b5edfcd", size = 15995, upload-time = "2025-11-14T10:17:15.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/cd/eeaf8585a343fda5b8cf3b8f144c872d1057c845202098b9441a39b76cb0/pyobjc_framework_medialibrary-12.1-py2.py3-none-any.whl", hash = "sha256:1f03ad6802a5c6e19ee3208b065689d3ec79defe1052cb80e00f54e1eff5f2a0", size = 4361, upload-time = "2025-11-14T09:54:32.259Z" }, +] + +[[package]] +name = "pyobjc-framework-mediaplayer" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-avfoundation" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/f0/851f6f47e11acbd62d5f5dcb8274afc969135e30018591f75bf3cbf6417f/pyobjc_framework_mediaplayer-12.1.tar.gz", hash = "sha256:5ef3f669bdf837d87cdb5a486ec34831542360d14bcba099c7c2e0383380794c", size = 35402, upload-time = "2025-11-14T10:17:18.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/c0/038ee3efd286c0fbc89c1e0cb688f4670ed0e5803aa36e739e79ffc91331/pyobjc_framework_mediaplayer-12.1-py2.py3-none-any.whl", hash = "sha256:85d9baec131807bfdf0f4c24d4b943e83cce806ab31c95c7e19c78e3fb7eefc8", size = 7120, upload-time = "2025-11-14T09:54:33.901Z" }, +] + +[[package]] +name = "pyobjc-framework-mediatoolbox" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/71/be5879380a161f98212a336b432256f307d1dcbaaaeb8ec988aea2ada2cd/pyobjc_framework_mediatoolbox-12.1.tar.gz", hash = "sha256:385b48746a5f08756ee87afc14037e552954c427ed5745d7ece31a21a7bad5ab", size = 22305, upload-time = "2025-11-14T10:17:22.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/94/d5ee221f2afbc64b2a7074efe25387cd8700e8116518904b28091ea6ad74/pyobjc_framework_mediatoolbox-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d7bcfeeff3fbf7e9e556ecafd8eaed2411df15c52baf134efa7480494e6faf6d", size = 12818, upload-time = "2025-11-14T09:54:41.251Z" }, + { url = "https://files.pythonhosted.org/packages/ca/30/79aa0010b30f3c54c68673d00f06f45ef28f5093ff1e927d68b5376ea097/pyobjc_framework_mediatoolbox-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1529a754cdb5b32797d297c0bf6279c7c14a3f7088f2dfbded09edcbfda19838", size = 12830, upload-time = "2025-11-14T09:54:43.191Z" }, + { url = "https://files.pythonhosted.org/packages/da/26/ae890f8ecce3fdda3e3a518426665467d36945c7c2729da1b073b1c44ff6/pyobjc_framework_mediatoolbox-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:13afec7d9f094ca5642e32b98680d1ee59aaa11a3d694cb1a6e454f72003f51c", size = 13420, upload-time = "2025-11-14T09:54:45.133Z" }, + { url = "https://files.pythonhosted.org/packages/bb/42/f0354b949f1eda6a57722a7450c77ff6689e53f9b2a933c4911e4385c2c8/pyobjc_framework_mediatoolbox-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:59921d4155a88d4acd04e80497707ac0208af3ff41574acba68214376e9fca23", size = 12808, upload-time = "2025-11-14T09:54:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/74/1e/7d9ffccd2053cd540e45e24aec03b70ac3d93d8bd99c8005b468a260c8a2/pyobjc_framework_mediatoolbox-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d99bf31c46b382f466888d1d80f738309916cbb83be0b4f1ccab5200de8f06c9", size = 13411, upload-time = "2025-11-14T09:54:49.228Z" }, +] + +[[package]] +name = "pyobjc-framework-metal" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/06/a84f7eb8561d5631954b9458cfca04b690b80b5b85ce70642bc89335f52a/pyobjc_framework_metal-12.1.tar.gz", hash = "sha256:bb554877d5ee2bf3f340ad88e8fe1b85baab7b5ec4bd6ae0f4f7604147e3eae7", size = 181847, upload-time = "2025-11-14T10:17:34.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/48/9286d06e1b14c11b65d3fea1555edc0061d9ebe11898dff8a14089e3a4c9/pyobjc_framework_metal-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38ab566b5a2979a43e13593d3eb12000a45e574576fe76996a5e1eb75ad7ac78", size = 75841, upload-time = "2025-11-14T09:55:06.801Z" }, + { url = "https://files.pythonhosted.org/packages/1c/aa/caa900c1fdb9a3b7e48946c5206171a7adcf3b5189bcdb535cf899220909/pyobjc_framework_metal-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f04a1a687cc346d23f3baf1ec56e3f42206709b590058d9778b52d45ca1c8ab", size = 75871, upload-time = "2025-11-14T09:55:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a9/a42a173ea2d94071bc0f3112006a5d6ba7eaf0df9c48424f99b3e867e02d/pyobjc_framework_metal-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3f3aa0848f4da46773952408b4814a440b210dc3f67f5ec5cfc0156ca2c8c0b6", size = 76420, upload-time = "2025-11-14T09:55:18.985Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/890dbc66bdae2ec839e28a15f16696ed1ab34b3cf32d58ed4dcd76183f25/pyobjc_framework_metal-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2440db9b7057b6bafbabe8a2c5dde044865569176058ee34a7d138df0fc96c8c", size = 75876, upload-time = "2025-11-14T09:55:24.905Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/df12913fa33b52ff0e2c3cb7d578849a198b2a141d6e07e8930856a40851/pyobjc_framework_metal-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:476eeba3bebc2b3010e352b6bd28e3732432a3d5a8d5c3fb1cebd257dc7ea41e", size = 76483, upload-time = "2025-11-14T09:55:30.656Z" }, +] + +[[package]] +name = "pyobjc-framework-metalfx" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-metal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/09/ce5c74565677fde66de3b9d35389066b19e5d1bfef9d9a4ad80f0c858c0c/pyobjc_framework_metalfx-12.1.tar.gz", hash = "sha256:1551b686fb80083a97879ce0331bdb1d4c9b94557570b7ecc35ebf40ff65c90b", size = 29470, upload-time = "2025-11-14T10:17:37.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/0b/508e3af499694f4eec74cc3ab0530e38db76e43a27db9ecb98c50c68f5f9/pyobjc_framework_metalfx-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a4418ae5c2eb77ec00695fa720a547638dc252dfd77ecb6feb88f713f5a948fd", size = 15062, upload-time = "2025-11-14T09:55:37.352Z" }, + { url = "https://files.pythonhosted.org/packages/02/b6/baa6071a36962e11c8834d8d13833509ce7ecb63e5c79fe2718d153a8312/pyobjc_framework_metalfx-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d443b0ee06de1b21a3ec5adab315840e71d52a74f8585090200228ab2fa1e59d", size = 15073, upload-time = "2025-11-14T09:55:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/42/d1/b4ea7e6c0c66710db81f315c48dca0252ed81bbde4a41de21b8d54ff2241/pyobjc_framework_metalfx-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:dcd334b42c5c50ec88e049f1b0bf43544b52e3ac09fd57b712fec8f63507190e", size = 15286, upload-time = "2025-11-14T09:55:41.642Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a6/fe7108290f798f79f2efbcf511fdb605b834f3616496fae8bec0c719ba65/pyobjc_framework_metalfx-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b5c4d81ebe71be69db838041ec93c12fb0458fe68a06f61f87a4d892135953dc", size = 16349, upload-time = "2025-11-14T09:55:44.009Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/2c782b429baed0cc545154c9b4f866eb86aa2d74977452e2c9c2157daef8/pyobjc_framework_metalfx-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:795f081c558312f51079de2d739412d286229f421282cfab36e195fef557f2ca", size = 16588, upload-time = "2025-11-14T09:55:46.128Z" }, +] + +[[package]] +name = "pyobjc-framework-metalkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-metal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/15/5091147aae12d4011a788b93971c3376aaaf9bf32aa935a2c9a06a71e18b/pyobjc_framework_metalkit-12.1.tar.gz", hash = "sha256:14cc5c256f0e3471b412a5b3582cb2a0d36d3d57401a8aa09e433252d1c34824", size = 25473, upload-time = "2025-11-14T10:17:39.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/c0/c8b5b060895cd51493afe3f09909b7e34893b1161cf4d93bc8e3cd306129/pyobjc_framework_metalkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1c4869076571d94788fe539fabfdd568a5c8e340936c7726d2551196640bd152", size = 8755, upload-time = "2025-11-14T09:55:51.683Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f04e991f4db4512e64ea7611796141c316506e733d75c468512df0e8fda4/pyobjc_framework_metalkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4dec94431ee888682115fe88ae72fca8bffc5df0957e3c006777c1d8267f65c3", size = 8769, upload-time = "2025-11-14T09:55:53.318Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b8/6f2fc56b6f8aee222d584edbdef4cf300e90782813e315418eba6d395533/pyobjc_framework_metalkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:d16958c0d4e2a75e1ea973de8951c775da1e39e378a7a7762fbce1837bf3179c", size = 8922, upload-time = "2025-11-14T09:55:55.016Z" }, + { url = "https://files.pythonhosted.org/packages/d4/52/84c2829df343322025d3ad474153359c850c3189555c0819155044b8777d/pyobjc_framework_metalkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a1b8ac9582b65d2711836b56dd24ce450aa740b0c478da9ee0621cc4c64e64cb", size = 8824, upload-time = "2025-11-14T09:55:56.672Z" }, + { url = "https://files.pythonhosted.org/packages/09/e9/ca6433dbdee520b8e3be3383b2b350692af4366f03842f6d79510a87c33c/pyobjc_framework_metalkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3d41ab59184d1a79981c5fb15d042750047a1a73574efa26179d7e174ddeaca6", size = 8972, upload-time = "2025-11-14T09:55:58.662Z" }, +] + +[[package]] +name = "pyobjc-framework-metalperformanceshaders" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-metal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/68/58da38e54aa0d8c19f0d3084d8c84e92d54cc8c9254041f07119d86aa073/pyobjc_framework_metalperformanceshaders-12.1.tar.gz", hash = "sha256:b198e755b95a1de1525e63c3b14327ae93ef1d88359e6be1ce554a3493755b50", size = 137301, upload-time = "2025-11-14T10:17:49.554Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/84/d505496fca9341e0cb11258ace7640cd986fe3e831f8b4749035e9f82109/pyobjc_framework_metalperformanceshaders-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c00e786c352b3ff5d86cf0cf3a830dc9f6fc32a03ae1a7539d20d11324adb2e8", size = 33242, upload-time = "2025-11-14T09:56:09.354Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/8f3d81905ce6b0613fe364a6dd77bf4ed85a6350f867b40a5e99b69e8d07/pyobjc_framework_metalperformanceshaders-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:240321f2fad1555b5ede3aed938c9f37da40a57fc3e7e9c96a45658dc12c3771", size = 33269, upload-time = "2025-11-14T09:56:12.527Z" }, + { url = "https://files.pythonhosted.org/packages/58/44/4813f8606a91a88f67a0b0c02ed9e2449cbfd5b701f7ca61cf9ce3fe0769/pyobjc_framework_metalperformanceshaders-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0aa287ee357fe5bd5660b3d0688f947a768cda8565dbbca3b876307b9876639e", size = 33457, upload-time = "2025-11-14T09:56:15.72Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d7/1177d8815549c90d8ddb0764b62c17bdaca6d6e03b8b54f3e7137167d8f3/pyobjc_framework_metalperformanceshaders-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:5d5a0a5c859c5493d597842f3d011c59bf7c10d04a29852016298364fca9e16e", size = 33324, upload-time = "2025-11-14T09:56:18.802Z" }, + { url = "https://files.pythonhosted.org/packages/4b/35/35302a62ae81e3b31c84bc1a2fc6fd0ad80a43b7edee9ef9bca482d55edd/pyobjc_framework_metalperformanceshaders-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c23b3a0f869c730e50851468a082014f1b0b3d4433d5d15ac28d6a736084026c", size = 33534, upload-time = "2025-11-14T09:56:21.984Z" }, +] + +[[package]] +name = "pyobjc-framework-metalperformanceshadersgraph" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-metalperformanceshaders" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/56/7ad0cd085532f7bdea9a8d4e9a2dfde376d26dd21e5eabdf1a366040eff8/pyobjc_framework_metalperformanceshadersgraph-12.1.tar.gz", hash = "sha256:b8fd017b47698037d7b172d41bed7a4835f4c4f2a288235819d200005f89ee35", size = 42992, upload-time = "2025-11-14T10:17:53.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/c9/5e7fd0d4bc9bdf7b442f36e020677c721ba9b4c1dc1fa3180085f22a4ef9/pyobjc_framework_metalperformanceshadersgraph-12.1-py2.py3-none-any.whl", hash = "sha256:85a1c7a6114ada05c7924b3235a1a98c45359410d148097488f15aee5ebb6ab9", size = 6481, upload-time = "2025-11-14T09:56:23.66Z" }, +] + +[[package]] +name = "pyobjc-framework-metrickit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/13/5576ddfbc0b174810a49171e2dbe610bdafd3b701011c6ecd9b3a461de8a/pyobjc_framework_metrickit-12.1.tar.gz", hash = "sha256:77841daf6b36ba0c19df88545fd910c0516acf279e6b7b4fa0a712a046eaa9f1", size = 27627, upload-time = "2025-11-14T10:17:56.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/04/8da5126e47306438c99750f1dfed430d7cc388f6b7f420ae748f3060ab96/pyobjc_framework_metrickit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3ec96e9ec7dc37fbce57dae277f0d89c66ffe1c3fa2feaca1b7125f8b2b29d87", size = 8120, upload-time = "2025-11-14T09:56:28.73Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/8b379325acb39e0966f818106b3c3c8e3966bf87a7ab5c2d0e89753b0d1f/pyobjc_framework_metrickit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:884afb6ec863883318975fda38db9d741b8da5f64a2b8c34bf8edc5ff56019d4", size = 8131, upload-time = "2025-11-14T09:56:30.524Z" }, + { url = "https://files.pythonhosted.org/packages/86/67/dcd2b18a787d3fec89e372aadb83c01879dda24fe1ed2a333a5e1d388591/pyobjc_framework_metrickit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:37674b0e049035d8b32d0221d0afbfedd3f643e4a2ee74b9a0e4e6d1b94fcd69", size = 8273, upload-time = "2025-11-14T09:56:32.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8b/a97a1463fc4453e5b1c157816a8356d800c4d66d5624154dc6dbdd7f52c0/pyobjc_framework_metrickit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f6cde78ba1a401660fe0e3a945d1941efef255c1021a8772a838aceb31bd74e6", size = 8190, upload-time = "2025-11-14T09:56:33.911Z" }, + { url = "https://files.pythonhosted.org/packages/ec/8b/a61b0fb889a2833b23fe2d4439d910a3d24a7eab83abc15c82f1fa1541a7/pyobjc_framework_metrickit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8f407172e1ecc8ee63afadda477a0f1c633c09be761edcadab8a9d1eebddd27c", size = 8333, upload-time = "2025-11-14T09:56:35.511Z" }, +] + +[[package]] +name = "pyobjc-framework-mlcompute" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/69/15f8ce96c14383aa783c8e4bc1e6d936a489343bb197b8e71abb3ddc1cb8/pyobjc_framework_mlcompute-12.1.tar.gz", hash = "sha256:3281db120273dcc56e97becffd5cedf9c62042788289f7be6ea067a863164f1e", size = 40698, upload-time = "2025-11-14T10:17:59.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f7/4614b9ccd0151795e328b9ed881fbcbb13e577a8ec4ae3507edb1a462731/pyobjc_framework_mlcompute-12.1-py2.py3-none-any.whl", hash = "sha256:4f0fc19551d710a03dfc4c7129299897544ff8ea76db6c7539ecc2f9b2571bde", size = 6744, upload-time = "2025-11-14T09:56:36.973Z" }, +] + +[[package]] +name = "pyobjc-framework-modelio" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/11/32c358111b623b4a0af9e90470b198fffc068b45acac74e1ba711aee7199/pyobjc_framework_modelio-12.1.tar.gz", hash = "sha256:d041d7bca7c2a4526344d3e593347225b7a2e51a499b3aa548895ba516d1bdbb", size = 66482, upload-time = "2025-11-14T10:18:04.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/0e/b8331100f0d658ecb3e87e75c108e2ae8ac7c78b521fd5ad0205b60a2584/pyobjc_framework_modelio-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:68d971917c289fdddf69094c74915d2ccb746b42b150e0bdc16d8161e6164022", size = 20193, upload-time = "2025-11-14T09:56:44.296Z" }, + { url = "https://files.pythonhosted.org/packages/db/fa/f111717fd64015fc3906b7c36dcfca4dda1d31916251c9640a8c70ff611a/pyobjc_framework_modelio-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dad6e914b6efe8ea3d2cd10029c4eb838f1ad6a12344787e8db70c4149df8cfc", size = 20208, upload-time = "2025-11-14T09:56:46.627Z" }, + { url = "https://files.pythonhosted.org/packages/58/d3/6f3131a16694684f3dfa6b2845054941dfb69a63f18980eea02a25c06f6d/pyobjc_framework_modelio-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f00b739f9333d611e7124acf95491bdf025dd32ba7c48b7521f6845b92e2dcce", size = 20448, upload-time = "2025-11-14T09:56:49.184Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/52b19e6ba86de2d38aed69a091c5d0c436c007ddf73441cbcc0a217db1d4/pyobjc_framework_modelio-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:5250e7f58cc71ca8928b33a00ac0dc56ca0eead97507f4bfcf777582a4b05e39", size = 20183, upload-time = "2025-11-14T09:56:51.861Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2c/13a22d22ffb1c175db9c23bea5f26dc3002c72056b68a362c04697778914/pyobjc_framework_modelio-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:aa76942301b2115c8904bcb10c73b19d10d7731ea35e6155cbfd6934d7c91e4b", size = 20426, upload-time = "2025-11-14T09:56:54.191Z" }, +] + +[[package]] +name = "pyobjc-framework-multipeerconnectivity" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/35/0d0bb6881004cb238cfd7bf74f4b2e42601a1accdf27b2189ec61cf3a2dc/pyobjc_framework_multipeerconnectivity-12.1.tar.gz", hash = "sha256:7123f734b7174cacbe92a51a62b4645cc9033f6b462ff945b504b62e1b9e6c1c", size = 22816, upload-time = "2025-11-14T10:18:07.363Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8d/0646ff7db36942829f0e84be18ba44bc5cd96d6a81651f8e7dc0974821c1/pyobjc_framework_multipeerconnectivity-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1c3bd254a16debed321debf4858f9c9b7d41572ddf1058a4bacf6a5bcfedeeff", size = 12001, upload-time = "2025-11-14T09:57:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/93/65/589cf3abaec888878d9b86162e5e622d4d467fd88a5f55320f555484dd54/pyobjc_framework_multipeerconnectivity-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25169a2fded90d13431db03787ac238b4ed551c44f7656996f8dfb6b6986b997", size = 12019, upload-time = "2025-11-14T09:57:02.86Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/c184a36ba61d803d482029021410568b0a2155b5bf0dd2def4256ab58a1e/pyobjc_framework_multipeerconnectivity-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3a6c2d233ecda3127bd6b6ded289ef0d1fa6ddc3acbab7f8af996c96090f7bfc", size = 12194, upload-time = "2025-11-14T09:57:04.63Z" }, + { url = "https://files.pythonhosted.org/packages/d6/64/fd5932ab32bec0e340b60ca87f57c07a9d963b56ab5f857787efcec236e4/pyobjc_framework_multipeerconnectivity-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:014f92d7e176154531c3173cf7113b6be374c041646c4b86d93afb84d2ea334c", size = 11989, upload-time = "2025-11-14T09:57:06.451Z" }, + { url = "https://files.pythonhosted.org/packages/99/1d/a7d2d26a081d5b9328a99865424078d9f9981e35c8e38a71321252e529f5/pyobjc_framework_multipeerconnectivity-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6490651224d1403d96e52ca3aed041b79b5456e3261abd9cb225c1fbc1893a69", size = 12210, upload-time = "2025-11-14T09:57:08.244Z" }, +] + +[[package]] +name = "pyobjc-framework-naturallanguage" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/d1/c81c0cdbb198d498edc9bc5fbb17e79b796450c17bb7541adbf502f9ad65/pyobjc_framework_naturallanguage-12.1.tar.gz", hash = "sha256:cb27a1e1e5b2913d308c49fcd2fd04ab5ea87cb60cac4a576a91ebf6a50e52f6", size = 23524, upload-time = "2025-11-14T10:18:09.883Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/d8/715a11111f76c80769cb267a19ecf2a4ac76152a6410debb5a4790422256/pyobjc_framework_naturallanguage-12.1-py2.py3-none-any.whl", hash = "sha256:a02ef383ec88948ca28f03ab8995523726b3bc75c49f593b5c89c218bcbce7ce", size = 5320, upload-time = "2025-11-14T09:57:10.294Z" }, +] + +[[package]] +name = "pyobjc-framework-netfs" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/68/4bf0e5b8cc0780cf7acf0aec54def58c8bcf8d733db0bd38f5a264d1af06/pyobjc_framework_netfs-12.1.tar.gz", hash = "sha256:e8d0c25f41d7d9ced1aa2483238d0a80536df21f4b588640a72e1bdb87e75c1e", size = 14799, upload-time = "2025-11-14T10:18:11.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/6b/8c2f223879edd3e3f030d0a9c9ba812775519c6d0c257e3e7255785ca6e7/pyobjc_framework_netfs-12.1-py2.py3-none-any.whl", hash = "sha256:0021f8b141e693d3821524c170e9c645090eb320e80c2935ddb978a6e8b8da81", size = 4163, upload-time = "2025-11-14T09:57:11.845Z" }, +] + +[[package]] +name = "pyobjc-framework-network" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/13/a71270a1b0a9ec979e68b8ec84b0f960e908b17b51cb3cac246a74d52b6b/pyobjc_framework_network-12.1.tar.gz", hash = "sha256:dbf736ff84d1caa41224e86ff84d34b4e9eb6918ae4e373a44d3cb597648a16a", size = 56990, upload-time = "2025-11-14T10:18:16.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/ef/a53f04f43e93932817f2ea71689dcc8afe3b908d631c21d11ec30c7b2e87/pyobjc_framework_network-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5e53aad64eae2933fe12d49185d66aca62fb817abf8a46f86b01e436ce1b79e4", size = 19613, upload-time = "2025-11-14T09:57:19.571Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f5/612539c2c0c7ce1160bd348325747f3a94ea367901965b217af877a556a1/pyobjc_framework_network-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e341beb32c7f95ed3e38f00cfed0a9fe7f89b8d80679bf2bd97c1a8d2280180a", size = 19632, upload-time = "2025-11-14T09:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ff/6a1909206f6d840ebcf40c9ea5de9a9ee07e7bb1ffa4fe573da7f90fac12/pyobjc_framework_network-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8344e3b57afccc762983e4629ec5eff72a3d7292afa8169a3e2aada3348848a8", size = 19696, upload-time = "2025-11-14T09:57:23.948Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/a7fb29708f2797fa96bfa6ae740b8154ac719e150939393453073121b7c9/pyobjc_framework_network-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:25e20ec81e23699e1182808384b8e426cb3ae9adaf639684232fc205edb48183", size = 19361, upload-time = "2025-11-14T09:57:26.565Z" }, + { url = "https://files.pythonhosted.org/packages/40/54/9cb89d6fac3e2e8d34107fa6de36ab7890844428b3d4fb4a9692f3cc4926/pyobjc_framework_network-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:39be2f25b13d2d530e893f06ddd3f277b83233020a0ab58413554fe8e0496624", size = 19406, upload-time = "2025-11-14T09:57:28.765Z" }, +] + +[[package]] +name = "pyobjc-framework-networkextension" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/3e/ac51dbb2efa16903e6af01f3c1f5a854c558661a7a5375c3e8767ac668e8/pyobjc_framework_networkextension-12.1.tar.gz", hash = "sha256:36abc339a7f214ab6a05cb2384a9df912f247163710741e118662bd049acfa2e", size = 62796, upload-time = "2025-11-14T10:18:21.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/14/4934b10ade5ad0518001bfc25260d926816b9c7d08d85ef45e8a61fdef1b/pyobjc_framework_networkextension-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:adc9baacfc532944d67018e381c7645f66a9fa0064939a5a841476d81422cdcc", size = 14376, upload-time = "2025-11-14T09:57:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a8/5d847dd3ffea913597342982614eb17bad4c29c07fac3447b56c9c5136ab/pyobjc_framework_networkextension-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63453b38e5a795f9ff950397e5a564071c2b4fd3360d79169ab017755bbb932a", size = 14399, upload-time = "2025-11-14T09:57:38.178Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a8/8d56c6ca7826633f856924256761338094eeab1ae40783c29c14b9746bc9/pyobjc_framework_networkextension-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e21d8ec762ded95afaff41b68425219df55ca8c3f777b810238441a4f7c221e3", size = 14539, upload-time = "2025-11-14T09:57:40.222Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/460b9ef440663299153ac0c165a56916620016435d402e4cf4cfdc74b521/pyobjc_framework_networkextension-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21076ec44790023b579f21f6b88e13388d353de98658dbb50369df53e6a9c967", size = 14453, upload-time = "2025-11-14T09:57:42.556Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ee/c9ea9e426b169d3ae54ddcad46828a6236168cfadbab37abc892d07a75ce/pyobjc_framework_networkextension-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:06d78bab27d4a7c51c9787b1f4cfcfed4d85488fcd96d93bac400bb2690ddceb", size = 14589, upload-time = "2025-11-14T09:57:45.012Z" }, +] + +[[package]] +name = "pyobjc-framework-notificationcenter" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/12/ae0fe82fb1e02365c9fe9531c9de46322f7af09e3659882212c6bf24d75e/pyobjc_framework_notificationcenter-12.1.tar.gz", hash = "sha256:2d09f5ab9dc39770bae4fa0c7cfe961e6c440c8fc465191d403633dccc941094", size = 21282, upload-time = "2025-11-14T10:18:24.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/05/3168637dd425257df5693c2ceafecf92d2e6833c0aaa6594d894a528d797/pyobjc_framework_notificationcenter-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82a735bd63f315f0a56abd206373917b7d09a0ae35fd99f1639a0fac4c525c0a", size = 9895, upload-time = "2025-11-14T09:57:51.151Z" }, + { url = "https://files.pythonhosted.org/packages/44/9a/f2b627dd4631a0756ee3e99b57de1e78447081d11f10313ed198e7521a31/pyobjc_framework_notificationcenter-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:06470683f568803f55f1646accfbf5eaa3fda56d15f27fca31bdbff4eaa8796c", size = 9917, upload-time = "2025-11-14T09:57:53.001Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/5fff664571dc48eea9246d31530fc564c654af827bfca1ddab47b72dc344/pyobjc_framework_notificationcenter-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:bdf87e5f027bec727b24bb1764a9933af9728862f6a0e9a7f4a1835061f283dd", size = 10110, upload-time = "2025-11-14T09:57:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/da/0a/621ed53aa7521d534275b8069c0f0d5e6517d772808a49add8476ad5c86d/pyobjc_framework_notificationcenter-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9495b1b0820a3e82bfcd0331b92bc29e4e4ca3a4e58d6ec0e1eda6c301ec4460", size = 9980, upload-time = "2025-11-14T09:57:56.666Z" }, + { url = "https://files.pythonhosted.org/packages/78/1a/b427a2316fb783a7dc58b12ce4d58de3263927614a9ff04934aeb10d8b8a/pyobjc_framework_notificationcenter-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1aca78efbf3ceab878758ec11dacef0c85629f844eee9e21645319dd98fd3673", size = 10186, upload-time = "2025-11-14T09:57:58.317Z" }, +] + +[[package]] +name = "pyobjc-framework-opendirectory" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/11/bc2f71d3077b3bd078dccad5c0c5c57ec807fefe3d90c97b97dd0ed3d04b/pyobjc_framework_opendirectory-12.1.tar.gz", hash = "sha256:2c63ce5dd179828ef2d8f9e3961da3bfa971a57db07a6c34eedc296548a928bb", size = 61049, upload-time = "2025-11-14T10:18:29.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/e7/3c2dece9c5b28af28a44d72a27b35ea5ffac31fed7cbd8d696ea75dc4a81/pyobjc_framework_opendirectory-12.1-py2.py3-none-any.whl", hash = "sha256:b5b5a5cf3cc2fb25147b16b79f046b90e3982bf3ded1b210a993d8cfdba737c4", size = 11845, upload-time = "2025-11-14T09:58:00.175Z" }, +] + +[[package]] +name = "pyobjc-framework-osakit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/b9/bf52c555c75a83aa45782122432fa06066bb76469047f13d06fb31e585c4/pyobjc_framework_osakit-12.1.tar.gz", hash = "sha256:36ea6acf03483dc1e4344a0cce7250a9656f44277d12bc265fa86d4cbde01f23", size = 17102, upload-time = "2025-11-14T10:18:31.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/10/30a15d7b23e6fcfa63d41ca4c7356c39ff81300249de89c3ff28216a9790/pyobjc_framework_osakit-12.1-py2.py3-none-any.whl", hash = "sha256:c49165336856fd75113d2e264a98c6deb235f1bd033eae48f661d4d832d85e6b", size = 4162, upload-time = "2025-11-14T09:58:01.953Z" }, +] + +[[package]] +name = "pyobjc-framework-oslog" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coremedia" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/42/805c9b4ac6ad25deb4215989d8fc41533d01e07ffd23f31b65620bade546/pyobjc_framework_oslog-12.1.tar.gz", hash = "sha256:d0ec6f4e3d1689d5e4341bc1130c6f24cb4ad619939f6c14d11a7e80c0ac4553", size = 21193, upload-time = "2025-11-14T10:18:33.645Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/60/0b742347d484068e9d6867cd95dedd1810c790b6aca45f6ef1d0f089f1f5/pyobjc_framework_oslog-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:072a41d36fcf780a070f13ac2569f8bafbb5ae4792fab4136b1a4d602dd9f5b4", size = 7813, upload-time = "2025-11-14T09:58:07.768Z" }, + { url = "https://files.pythonhosted.org/packages/89/ad/719d65e7202623da7a3f22225e7f2b736f38cd6d3e0d87253b7f74f5b9c0/pyobjc_framework_oslog-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d26ce39be2394695cf4c4c699e47f9b85479cf1ccb0472614bb88027803a8986", size = 7834, upload-time = "2025-11-14T09:58:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/86/f0/a042b06f47d11bdad58d5c0cec9fe3dc4dc12ed9e476031cd4c0f08c6f18/pyobjc_framework_oslog-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6925e6764c6f293b69fbd4f5fd32a9810fca07d63e782c41cb4ebf05dc42977", size = 8016, upload-time = "2025-11-14T09:58:11.431Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c1/7a7742fc81708c53a0f736ce883069b3c1797440d691a7ed7b8e29e8dbbd/pyobjc_framework_oslog-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:16d98c49698da839b79904a2c63fee658fd4a8c4fa9223e5694270533127e8d4", size = 7875, upload-time = "2025-11-14T09:58:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/09/d2/c5703c03d6b57a3c729e211556c88e44ca4bfbe45bcbf5d6f4843095fdeb/pyobjc_framework_oslog-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:302956914b0d28dc9d8e27c2428d46c89cde8e2c64a426cda241d4b0c64315fd", size = 8075, upload-time = "2025-11-14T09:58:14.723Z" }, +] + +[[package]] +name = "pyobjc-framework-passkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/d4/2afb59fb0f99eb2f03888850887e536f1ef64b303fd756283679471a5189/pyobjc_framework_passkit-12.1.tar.gz", hash = "sha256:d8c27c352e86a3549bf696504e6b25af5f2134b173d9dd60d66c6d3da53bb078", size = 53835, upload-time = "2025-11-14T10:18:37.906Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/dc/9cb27e8b7b00649af5e802815ffa8928bd8a619f2984a1bea7dabd28f741/pyobjc_framework_passkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7e95a484ec529dbf1d44f5f7f1406502a77bda733511e117856e3dca9fa29c5c", size = 14102, upload-time = "2025-11-14T09:58:20.903Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e2/6135402be2151042b234ea241e89f4b8984f6494fd11d9f56b4a56a9d7d4/pyobjc_framework_passkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:64287e6dc54ab4c0aa8ba80a7a51762e36591602c77c6a803aee690e7464b6b2", size = 14110, upload-time = "2025-11-14T09:58:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/23/f3/ff6f81206eca1e1fb49c5a516d5eb15f143b38c5adee5b0c24076be02be9/pyobjc_framework_passkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a360e98b29eee8642f3e7d973c636284c24fb2ec2c3ee56022eeae6270943be", size = 14277, upload-time = "2025-11-14T09:58:25.338Z" }, + { url = "https://files.pythonhosted.org/packages/dc/71/bde73bb39a836fb07c10fbdc60f38a3bd436c0aada1de0f4140737813930/pyobjc_framework_passkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e28dcf1074cddd82c2bd3ee5c3800952ac59850578b1135b38871ff584ea9d41", size = 14118, upload-time = "2025-11-14T09:58:27.353Z" }, + { url = "https://files.pythonhosted.org/packages/c1/13/f2a4fe4fb6ce91689f16c577089fe19748b3be322a28099543a89ee6c0fb/pyobjc_framework_passkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a8782f31254016a9b152a9d1dc7ea18187729221f6ca175927be99a65b97640e", size = 14280, upload-time = "2025-11-14T09:58:29.374Z" }, +] + +[[package]] +name = "pyobjc-framework-pencilkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/43/859068016bcbe7d80597d5c579de0b84b0da62c5c55cdf9cc940e9f9c0f8/pyobjc_framework_pencilkit-12.1.tar.gz", hash = "sha256:d404982d1f7a474369f3e7fea3fbd6290326143fa4138d64b6753005a6263dc4", size = 17664, upload-time = "2025-11-14T10:18:40.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/26/daf47dcfced8f7326218dced5c68ed2f3b522ec113329218ce1305809535/pyobjc_framework_pencilkit-12.1-py2.py3-none-any.whl", hash = "sha256:33b88e5ed15724a12fd8bf27a68614b654ff739d227e81161298bc0d03acca4f", size = 4206, upload-time = "2025-11-14T09:58:30.814Z" }, +] + +[[package]] +name = "pyobjc-framework-phase" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-avfoundation" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/51/3b25eaf7ca85f38ceef892fdf066b7faa0fec716f35ea928c6ffec6ae311/pyobjc_framework_phase-12.1.tar.gz", hash = "sha256:3a69005c572f6fd777276a835115eb8359a33673d4a87e754209f99583534475", size = 32730, upload-time = "2025-11-14T10:18:43.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/9f/1ae45db731e8d6dd3e0b408c3accd0cf3236849e671f95c7c8cf95687240/pyobjc_framework_phase-12.1-py2.py3-none-any.whl", hash = "sha256:99a1c1efc6644f5312cce3693117d4e4482538f65ad08fe59e41e2579b67ab17", size = 6902, upload-time = "2025-11-14T09:58:32.436Z" }, +] + +[[package]] +name = "pyobjc-framework-photos" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/53/f8a3dc7f711034d2283e289cd966fb7486028ea132a24260290ff32d3525/pyobjc_framework_photos-12.1.tar.gz", hash = "sha256:adb68aaa29e186832d3c36a0b60b0592a834e24c5263e9d78c956b2b77dce563", size = 47034, upload-time = "2025-11-14T10:18:47.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/38/e6f25aec46a1a9d0a310795606cc43f9823d41c3e152114b814b597835a8/pyobjc_framework_photos-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eda8a584a851506a1ebbb2ee8de2cb1ed9e3431e6a642ef6a9543e32117d17b9", size = 12358, upload-time = "2025-11-14T09:58:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/71/5a/3c4e2af8d17e62ecf26e066fbb9209aacccfaf691f5faa42e3fd64b2b9f2/pyobjc_framework_photos-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bd7906d8662af29f91c71892ae0b0cab4682a3a7ef5be1a2277d881d7b8d37d3", size = 12367, upload-time = "2025-11-14T09:58:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/566de3200d4aa05ca75b0150e5d031d2384a388f9126a4fef62a8f53818f/pyobjc_framework_photos-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c822d81c778dd2a789f15d0f329cee633391c5ad766482ffbaf40d3dc57584a3", size = 12552, upload-time = "2025-11-14T09:58:44.134Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5c/47b9e1f6ac61a80b6544091dffe42dc883217d6e670ddc188968988ba7f6/pyobjc_framework_photos-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:95d5036bdaf1c50559adfa60fd715b57c68577d2574241ed1890e359849f923f", size = 12422, upload-time = "2025-11-14T09:58:46.072Z" }, + { url = "https://files.pythonhosted.org/packages/b4/33/48cc5ca364e62d08296de459e86daa538291b895b5d1abb670053263e0c4/pyobjc_framework_photos-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:77f181d3cb3fde9c04301c9a96693d02a139d478891e49ed76573dedf0437f49", size = 12607, upload-time = "2025-11-14T09:58:48.084Z" }, +] + +[[package]] +name = "pyobjc-framework-photosui" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/a5/14c538828ed1a420e047388aedc4a2d7d9292030d81bf6b1ced2ec27b6e9/pyobjc_framework_photosui-12.1.tar.gz", hash = "sha256:9141234bb9d17687f1e8b66303158eccdd45132341fbe5e892174910035f029a", size = 29886, upload-time = "2025-11-14T10:18:50.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/a2/b5afca8039b1a659a2a979bb1bdbdddfdf9b1d2724a2cc4633dca2573d5f/pyobjc_framework_photosui-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:713e3bb25feb5ea891e67260c2c0769cab44a7f11b252023bfcf9f8c29dd1206", size = 11714, upload-time = "2025-11-14T09:58:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/d6/cd/204298e136ff22d3502f0b66cda1d36df89346fa2b20f4a3a681c2c96fee/pyobjc_framework_photosui-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5fa3ca2bc4c8609dee46e3c8fb5f3fbfb615f39fa3d710a213febec38e227758", size = 11725, upload-time = "2025-11-14T09:58:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5e/492007c629844666e8334e535471c5492e93715965fdffe4f75227f47fac/pyobjc_framework_photosui-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:713ec72b13d8399229d285ccd1e94e5ea2627cf88858977a2a91cc94d1affcd6", size = 11921, upload-time = "2025-11-14T09:58:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/33/4e/d45cae151b0b46ab4110b6ea7d689af9480a07ced3dbf5f0860b201a542a/pyobjc_framework_photosui-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a8e0320908f497d1e548336569f435afd27ed964e65b2aefa3a2d2ea4c041da2", size = 11722, upload-time = "2025-11-14T09:59:00.326Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a3/c46998d5e96d38c04af9465808dba035fe3338d49092d8b887cc3f1c9f3d/pyobjc_framework_photosui-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1b3e9226601533843d6764a7006c2f218123a9c22ac935345c6fb88691b9f78b", size = 11908, upload-time = "2025-11-14T09:59:02.103Z" }, +] + +[[package]] +name = "pyobjc-framework-preferencepanes" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/bc/e87df041d4f7f6b7721bf7996fa02aa0255939fb0fac0ecb294229765f92/pyobjc_framework_preferencepanes-12.1.tar.gz", hash = "sha256:b2a02f9049f136bdeca7642b3307637b190850e5853b74b5c372bc7d88ef9744", size = 24543, upload-time = "2025-11-14T10:18:53.259Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/7b/8ceec1ab0446224d685e243e2770c5a5c92285bcab0b9324dbe7a893ae5a/pyobjc_framework_preferencepanes-12.1-py2.py3-none-any.whl", hash = "sha256:1b3af9db9e0cfed8db28c260b2cf9a22c15fda5f0ff4c26157b17f99a0e29bbf", size = 4797, upload-time = "2025-11-14T09:59:03.998Z" }, +] + +[[package]] +name = "pyobjc-framework-pushkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/45/de756b62709add6d0615f86e48291ee2bee40223e7dde7bbe68a952593f0/pyobjc_framework_pushkit-12.1.tar.gz", hash = "sha256:829a2fc8f4780e75fc2a41217290ee0ff92d4ade43c42def4d7e5af436d8ae82", size = 19465, upload-time = "2025-11-14T10:18:57.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/01/74cf1dd0764c590de05dc1e87d168031e424f834721940b7bb02c67fe821/pyobjc_framework_pushkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7bdf472a55ac65154e03f54ae0bcad64c4cf45e9b1acba62f15107f2bc994d69", size = 8177, upload-time = "2025-11-14T09:59:11.155Z" }, + { url = "https://files.pythonhosted.org/packages/1b/79/00368a140fe4a14e92393da25ef5a3037a09bb0024d984d7813e7e3fa11c/pyobjc_framework_pushkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f3751276cb595a9f886ed6094e06004fd11932443e345760eade09119f8e0181", size = 8193, upload-time = "2025-11-14T09:59:13.23Z" }, + { url = "https://files.pythonhosted.org/packages/57/29/dccede214ef1835662066c74138978629d92b6a9f723e28670cfb04f3ce7/pyobjc_framework_pushkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:64955af6441635449c2af6c6f468c9ba5e413e1494b87617bc1e9fbd8be7e5bf", size = 8339, upload-time = "2025-11-14T09:59:14.754Z" }, + { url = "https://files.pythonhosted.org/packages/16/09/9ba944e1146308460bf7474cdc2a0844682862f9850576494035a7653f4a/pyobjc_framework_pushkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:de82e1f6e01444582ad2ca6a76aeee1524c23695f0e4f56596f9db3e9d635623", size = 8254, upload-time = "2025-11-14T09:59:16.672Z" }, + { url = "https://files.pythonhosted.org/packages/79/be/9220099adb71ec5ae374d2b5b6c3b34e8c505e42fcd090c73e53035a414f/pyobjc_framework_pushkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:69c7a03a706bc7fb24ca69a9f79d030927be1e5166c0d2a5a9afc1c5d82a07ec", size = 8388, upload-time = "2025-11-14T09:59:18.707Z" }, +] + +[[package]] +name = "pyobjc-framework-quartz" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798, upload-time = "2025-11-14T10:00:01.236Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206, upload-time = "2025-11-14T10:00:15.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317, upload-time = "2025-11-14T10:00:30.703Z" }, + { url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" }, +] + +[[package]] +name = "pyobjc-framework-quicklookthumbnailing" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/1a/b90539500e9a27c2049c388d85a824fc0704009b11e33b05009f52a6dc67/pyobjc_framework_quicklookthumbnailing-12.1.tar.gz", hash = "sha256:4f7e09e873e9bda236dce6e2f238cab571baeb75eca2e0bc0961d5fcd85f3c8f", size = 14790, upload-time = "2025-11-14T10:21:26.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/22/7bd07b5b44bf8540514a9f24bc46da68812c1fd6c63bb2d3496e5ea44bf0/pyobjc_framework_quicklookthumbnailing-12.1-py2.py3-none-any.whl", hash = "sha256:5efe50b0318188b3a4147681788b47fce64709f6fe0e1b5d020e408ef40ab08e", size = 4234, upload-time = "2025-11-14T10:01:02.209Z" }, +] + +[[package]] +name = "pyobjc-framework-replaykit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/f8/b92af879734d91c1726227e7a03b9e68ab8d9d2bb1716d1a5c29254087f2/pyobjc_framework_replaykit-12.1.tar.gz", hash = "sha256:95801fd35c329d7302b2541f2754e6574bf36547ab869fbbf41e408dfa07268a", size = 23312, upload-time = "2025-11-14T10:21:29.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/fc/c68d2111b2655148d88574959d3d8b21d3a003573013301d4d2a7254c1af/pyobjc_framework_replaykit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b0528c2a6188440fdc2017f0924c0a0f15d0a2f6aa295f1d1c2d6b3894c22f1d", size = 10120, upload-time = "2025-11-14T10:01:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/22/f1/95d3cf08a5b747e15dfb45f4ad23aeae566e75e6c54f3c58caf59b99f4d9/pyobjc_framework_replaykit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:18af5ab59574102978790ce9ccc89fe24be9fa57579f24ed8cfc2b44ea28d839", size = 10141, upload-time = "2025-11-14T10:01:10.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/fac397700f62fdb73161e04affd608678883e9476553fd99e9d65db51f79/pyobjc_framework_replaykit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:31c826a71b76cd7d12c3f30956c202116b0c985a19eb420e91fc1f51bedd2f72", size = 10319, upload-time = "2025-11-14T10:01:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e7/e3efd189fbaf349962a98db3d63b3ba30fd5f27e249cc933993478421ebc/pyobjc_framework_replaykit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d6d8046825149f7f2627987a1b48ac7e4c9747a15e263054de0dfde1926a0f42", size = 10194, upload-time = "2025-11-14T10:01:13.754Z" }, + { url = "https://files.pythonhosted.org/packages/2b/52/7564ac0133033853432f3a3abf30fb98f820461c147c904cc8ed6c779d85/pyobjc_framework_replaykit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9f77dc914d5aabcd9273c39777a3372175aa839a3bd7f673a0ead4b7f2cf4211", size = 10383, upload-time = "2025-11-14T10:01:15.673Z" }, +] + +[[package]] +name = "pyobjc-framework-safariservices" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/4b/8f896bafbdbfa180a5ba1e21a6f5dc63150c09cba69d85f68708e02866ae/pyobjc_framework_safariservices-12.1.tar.gz", hash = "sha256:6a56f71c1e692bca1f48fe7c40e4c5a41e148b4e3c6cfb185fd80a4d4a951897", size = 25165, upload-time = "2025-11-14T10:21:32.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/3a/8c525562fd782c88bc44e8c07fc2c073919f98dead08fffd50f280ef1afa/pyobjc_framework_safariservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b475abc82504fc1c0801096a639562d6a6d37370193e8e4a406de9199a7cea13", size = 7281, upload-time = "2025-11-14T10:01:21.238Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e7/fc984cf2471597e71378b4f82be4a1923855a4c4a56486cc8d97fdaf1694/pyobjc_framework_safariservices-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:592cf5080a9e7f104d6a8d338ebf2523a961f38068f238f11783e86dc105f9c7", size = 7304, upload-time = "2025-11-14T10:01:22.786Z" }, + { url = "https://files.pythonhosted.org/packages/6e/99/3d3062808a64422f39586519d38a52e73304ed60f45500b2c75b97fdd667/pyobjc_framework_safariservices-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:097a2166f79c60633e963913722a087a13b1c5849f3173655b24a8be47039ac4", size = 7308, upload-time = "2025-11-14T10:01:24.299Z" }, + { url = "https://files.pythonhosted.org/packages/99/c3/766dd0e14d61ed05d416bccc4435a977169d5256828ab31ba5939b2f953d/pyobjc_framework_safariservices-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:090afa066820de497d2479a1c5bd4c8ed381eb36a615e4644e12e347ec9d9a3e", size = 7333, upload-time = "2025-11-14T10:01:25.874Z" }, + { url = "https://files.pythonhosted.org/packages/80/8c/93bd8887d83c7f7f6d920495a185f2e4f7d2c41bad7b93652a664913b94d/pyobjc_framework_safariservices-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3fc553396c51a7fd60c0a2e2b1cdb3fecab135881115adf2f1bbaeb64f801863", size = 7340, upload-time = "2025-11-14T10:01:27.726Z" }, +] + +[[package]] +name = "pyobjc-framework-safetykit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/bf/ad6bf60ceb61614c9c9f5758190971e9b90c45b1c7a244e45db64138b6c2/pyobjc_framework_safetykit-12.1.tar.gz", hash = "sha256:0cd4850659fb9b5632fd8ad21f2de6863e8303ff0d51c5cc9c0034aac5db08d8", size = 20086, upload-time = "2025-11-14T10:21:34.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/0c/08a20fb7516405186c0fe7299530edd4aa22c24f73290198312447f26c8c/pyobjc_framework_safetykit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e4977f7069a23252053d1a42b1a053aefc19b85c960a5214b05daf3c037a6f16", size = 8550, upload-time = "2025-11-14T10:01:32.885Z" }, + { url = "https://files.pythonhosted.org/packages/02/c5/0e8961e48a2e5942f3f4fad46be5a7b47e17792d89f4c2405b065c1241b5/pyobjc_framework_safetykit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:20170b4869c4ee5485f750ad02bbfcb25c53bbfe86892e5328096dc3c6478b83", size = 8564, upload-time = "2025-11-14T10:01:34.934Z" }, + { url = "https://files.pythonhosted.org/packages/48/3f/fdadc2b992cb3e08269fc75dec3128f8153dd833715b9fbfb975c193c4d2/pyobjc_framework_safetykit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a935c55ae8e731a44c3cb74324da7517634bfc0eca678b6d4b2f9fe04ff53d8", size = 8720, upload-time = "2025-11-14T10:01:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ec/759117239a3edbd8994069f1f595e4fbc72fa60fa7ebb4aeb4fd47265e7c/pyobjc_framework_safetykit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1b0e8761fd53e6a83a48dbd93961434b05fe17658478b9001c65627da46ba02b", size = 8616, upload-time = "2025-11-14T10:01:38.616Z" }, + { url = "https://files.pythonhosted.org/packages/43/fd/72e9d6703a0281ffc086b3655c63ca2502ddaff52b3b82e9eb1c9a206493/pyobjc_framework_safetykit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b3ea88d1de4be84f630e25856abb417f3b19c242038ac061cca85a9a9e3dc61b", size = 8778, upload-time = "2025-11-14T10:01:40.968Z" }, +] + +[[package]] +name = "pyobjc-framework-scenekit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/8c/1f4005cf0cb68f84dd98b93bbc0974ee7851bb33d976791c85e042dc2278/pyobjc_framework_scenekit-12.1.tar.gz", hash = "sha256:1bd5b866f31fd829f26feac52e807ed942254fd248115c7c742cfad41d949426", size = 101212, upload-time = "2025-11-14T10:21:41.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/f1/4986bd96e0ba0f60bff482a6b135b9d6db65d56578d535751f18f88190f0/pyobjc_framework_scenekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:40aea10098893f0b06191f1e79d7b25e12e36a9265549d324238bdb25c7e6df0", size = 33597, upload-time = "2025-11-14T10:01:51.297Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/c728a025fd09cd259870d43b68ce8e7cffb639112033693ffa02d3d1eac0/pyobjc_framework_scenekit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a032377a7374320131768b6c8bf84589e45819d9e0fe187bd3f8d985207016b9", size = 33623, upload-time = "2025-11-14T10:01:54.878Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/9cea4cc4ac7f43fa6fb60d0690d25b2da1d8e1cf42266316014d1bb43a11/pyobjc_framework_scenekit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:633909adff9b505b49c34307f507f4bd926b88a1482d8143655d5703481cbbf5", size = 33934, upload-time = "2025-11-14T10:01:57.994Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/eb436dda11b6f950bff7f7d9af108970058f2fa9822a946a6982d74a64f8/pyobjc_framework_scenekit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d4c8512c9186f12602ac19558072cdeec3a607d628c269317d5965341a14372c", size = 33728, upload-time = "2025-11-14T10:02:01.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/20/2adb296dd6ac1619bf4e2e8a878be7e13b8ed362d9d649c88734998a5cf7/pyobjc_framework_scenekit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b99a99edf37c8fe4194a9c0ab2092f57e564e07adb1ad54ef82b7213184be668", size = 34009, upload-time = "2025-11-14T10:02:05.107Z" }, +] + +[[package]] +name = "pyobjc-framework-screencapturekit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coremedia" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/7f/73458db1361d2cb408f43821a1e3819318a0f81885f833d78d93bdc698e0/pyobjc_framework_screencapturekit-12.1.tar.gz", hash = "sha256:50992c6128b35ab45d9e336f0993ddd112f58b8c8c8f0892a9cb42d61bd1f4c9", size = 32573, upload-time = "2025-11-14T10:21:44.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a8/533acdbf26e0a908ff640d3a445481f3c948682ca887be6711b5fcf82682/pyobjc_framework_screencapturekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:27df138ce2dfa9d4aae5106d4877e9ed694b5a174643c058f1c48678ffc7001a", size = 11504, upload-time = "2025-11-14T10:02:11.36Z" }, + { url = "https://files.pythonhosted.org/packages/45/f9/ff713b8c4659f9ef1c4dbb8ca4b59c4b22d9df48471230979d620709e3b4/pyobjc_framework_screencapturekit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:168125388fb35c6909bec93b259508156e89b9e30fec5748d4a04fd0157f0e0d", size = 11523, upload-time = "2025-11-14T10:02:13.494Z" }, + { url = "https://files.pythonhosted.org/packages/f0/26/8bf1bacdb2892cf26d043c7f6e8788a613bbb2ccb313a5ea0634612cfc24/pyobjc_framework_screencapturekit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4fc2fe72c1da5ac1b8898a7b2082ed69803e6d9c11f414bb5a5ec94422a5f74f", size = 11701, upload-time = "2025-11-14T10:02:15.634Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/881e2ff0e11e7d705716f01f1bfd10232f7d21bda38d630c3fbe409b13a9/pyobjc_framework_screencapturekit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:be210ea5df36c1392425c026c59c5e0797b0d6e07ee9551d032e40bed95d2833", size = 11581, upload-time = "2025-11-14T10:02:17.467Z" }, + { url = "https://files.pythonhosted.org/packages/24/d0/69f295412d5dfacb6e6890ee128b9c80c8f4f584c20842c576ee154bfc0b/pyobjc_framework_screencapturekit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:534f3a433edf6417c3dd58ac52a69360e5a19c924d1cb389495c4d6cc13a875d", size = 11783, upload-time = "2025-11-14T10:02:19.257Z" }, +] + +[[package]] +name = "pyobjc-framework-screensaver" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/99/7cfbce880cea61253a44eed594dce66c2b2fbf29e37eaedcd40cffa949e9/pyobjc_framework_screensaver-12.1.tar.gz", hash = "sha256:c4ca111317c5a3883b7eace0a9e7dd72bc6ffaa2ca954bdec918c3ab7c65c96f", size = 22229, upload-time = "2025-11-14T10:21:47.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a4/2481711f2e9557b90bac74fa8bf821162cf7b65835732ae560fd52e9037e/pyobjc_framework_screensaver-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a3c90c2299eac6d01add81427ae2f90d7724f15d676261e838d7a7750f812322", size = 8422, upload-time = "2025-11-14T10:02:24.49Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8a/2e0cb958e872896b67ae6d5877070867f4a845ea1010984ff887ad418396/pyobjc_framework_screensaver-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a865b6dbb39fb92cdb67b13f68d594ab84d08a984cc3e9a39fab3386f431649", size = 8442, upload-time = "2025-11-14T10:02:26.135Z" }, + { url = "https://files.pythonhosted.org/packages/35/45/3eb9984119be3dcd90f4628ecc3964c1a394b702a71034af6d932f98de3a/pyobjc_framework_screensaver-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c249dffcb95d55fc6be626bf17f70b477e320c33d94e234597bc0074e302cfcd", size = 8450, upload-time = "2025-11-14T10:02:27.782Z" }, + { url = "https://files.pythonhosted.org/packages/c6/97/2fab7dfb449ccc49fb617ade97bfa35689572c71fff5885ea25705479a30/pyobjc_framework_screensaver-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4744a01043a9c6b464f6a2230948812bf88bdd68f084b6f05b475b93093c3ea9", size = 8477, upload-time = "2025-11-14T10:02:29.424Z" }, + { url = "https://files.pythonhosted.org/packages/59/e1/605137cc679dbeddc08470397d05dfd7c20e4c626924d33030c3aa45c39a/pyobjc_framework_screensaver-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c02ec9dccf49463056a438b7f8a6374dc2416d4a0672003382d50603aed9ab5d", size = 8501, upload-time = "2025-11-14T10:02:31.09Z" }, +] + +[[package]] +name = "pyobjc-framework-screentime" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/11/ba18f905321895715dac3cae2071c2789745ae13605b283b8114b41e0459/pyobjc_framework_screentime-12.1.tar.gz", hash = "sha256:583de46b365543bbbcf27cd70eedd375d397441d64a2cf43c65286fd9c91af55", size = 13413, upload-time = "2025-11-14T10:21:49.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/06/904174de6170e11b53673cc5844e5f13394eeeed486e0bcdf5288c1b0853/pyobjc_framework_screentime-12.1-py2.py3-none-any.whl", hash = "sha256:d34a068ec8ba2704987fcd05c37c9a9392de61d92933e6e71c8e4eaa4dfce029", size = 3963, upload-time = "2025-11-14T10:02:32.577Z" }, +] + +[[package]] +name = "pyobjc-framework-scriptingbridge" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/cb/adc0a09e8c4755c2281bd12803a87f36e0832a8fc853a2d663433dbb72ce/pyobjc_framework_scriptingbridge-12.1.tar.gz", hash = "sha256:0e90f866a7e6a8aeaf723d04c826657dd528c8c1b91e7a605f8bb947c74ad082", size = 20339, upload-time = "2025-11-14T10:21:51.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/46/e0b07d2b3ff9effb8b1179a6cc681a953d3dfbf0eb8b1d6a0e54cef2e922/pyobjc_framework_scriptingbridge-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8083cd68c559c55a3787b2e74fc983c8665e5078571475aaeabf4f34add36b62", size = 8356, upload-time = "2025-11-14T10:02:38.559Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/b11568f21924a994aa59272e2752e742f8380ab2cf88d111326ba7baede0/pyobjc_framework_scriptingbridge-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bddbd3a13bfaeaa38ab66e44f10446d5bc7d1110dbc02e59b80bcd9c3a60548a", size = 8371, upload-time = "2025-11-14T10:02:40.603Z" }, + { url = "https://files.pythonhosted.org/packages/77/eb/9bc3e6e9611d757fc80b4423cc28128750a72eae8241be8ae43e1d76c4cd/pyobjc_framework_scriptingbridge-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:148191010b4e10c3938cdb2dcecad43fa0884cefb5a78499a21bdaf5a78318b3", size = 8526, upload-time = "2025-11-14T10:02:42.298Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bc/5f1d372bb1efa9cf1e3218e1831136f5548b9f5b12a4a6676bf8b37cca63/pyobjc_framework_scriptingbridge-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:48f4bc33b2cab6634f58f37549096bda9ec7d3ec664b4b40e7d3248d9f481f69", size = 8406, upload-time = "2025-11-14T10:02:43.979Z" }, + { url = "https://files.pythonhosted.org/packages/42/c2/c223ac13c69e99787301ad8e4be32fc192e067e4e2798e0e5cceabf1abbe/pyobjc_framework_scriptingbridge-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:81bf8b19cd7fd1db055530007bc724901fd61160823324ec2df0daa8e25b94f7", size = 8564, upload-time = "2025-11-14T10:02:45.629Z" }, +] + +[[package]] +name = "pyobjc-framework-searchkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-coreservices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/60/a38523198430e14fdef21ebe62a93c43aedd08f1f3a07ea3d96d9997db5d/pyobjc_framework_searchkit-12.1.tar.gz", hash = "sha256:ddd94131dabbbc2d7c3f17db3da87c1a712c431310eef16f07187771e7e85226", size = 30942, upload-time = "2025-11-14T10:21:55.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/46/4f9cd3011f47b43b21b2924ab3770303c3f0a4d16f05550d38c5fcb42e78/pyobjc_framework_searchkit-12.1-py2.py3-none-any.whl", hash = "sha256:844ce62b7296b19da8db7dedd539d07f7b3fb3bb8b029c261f7bcf0e01a97758", size = 3733, upload-time = "2025-11-14T10:02:47.026Z" }, +] + +[[package]] +name = "pyobjc-framework-security" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/aa/796e09a3e3d5cee32ebeebb7dcf421b48ea86e28c387924608a05e3f668b/pyobjc_framework_security-12.1.tar.gz", hash = "sha256:7fecb982bd2f7c4354513faf90ba4c53c190b7e88167984c2d0da99741de6da9", size = 168044, upload-time = "2025-11-14T10:22:06.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/66/5160c0f938fc0515fe8d9af146aac1b093f7ef285ce797fedae161b6c0e8/pyobjc_framework_security-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab42e55f5b782332be5442750fcd9637ee33247d57c7b1d5801bc0e24ee13278", size = 41280, upload-time = "2025-11-14T10:02:58.097Z" }, + { url = "https://files.pythonhosted.org/packages/32/48/b294ed75247c5cfa00d51925a10237337d24f54961d49a179b20a4307642/pyobjc_framework_security-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:afc36661cc6eb98cd794bed1d6668791e96557d6f72d9ac70aa49022d26af1d4", size = 41284, upload-time = "2025-11-14T10:03:01.722Z" }, + { url = "https://files.pythonhosted.org/packages/ef/57/0d3ef78779cf5c3bba878b2f824137e50978ad4a21dabe65d8b5ae0fc0d1/pyobjc_framework_security-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9510c98ab56921d1d416437372605cc1c1f6c1ad8d3061ee56b17bf423dd5427", size = 42162, upload-time = "2025-11-14T10:03:05.337Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/63c15f9449c191e7448a05ff8af4a82c39a51bb627bc96dc9697586c0f79/pyobjc_framework_security-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6319a34508fd87ab6ca3cda6f54e707196197a65b792b292705af967e225438a", size = 41348, upload-time = "2025-11-14T10:03:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d8/5aaa2a8124ed04a9d6ca7053dc0fa64e42be51497ed8263a24b744a95598/pyobjc_framework_security-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:03d166371cefdef24908825148eb848f99ee2c0b865870a09dcbb94334dd3e0a", size = 42908, upload-time = "2025-11-14T10:03:13.01Z" }, +] + +[[package]] +name = "pyobjc-framework-securityfoundation" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-security" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/d5/c2b77e83c1585ba43e5f00c917273ba4bf7ed548c1b691f6766eb0418d52/pyobjc_framework_securityfoundation-12.1.tar.gz", hash = "sha256:1f39f4b3db6e3bd3a420aaf4923228b88e48c90692cf3612b0f6f1573302a75d", size = 12669, upload-time = "2025-11-14T10:22:09.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/1e/349fb71a413b37b1b41e712c7ca180df82144478f8a9a59497d66d0f2ea2/pyobjc_framework_securityfoundation-12.1-py2.py3-none-any.whl", hash = "sha256:579cf23e63434226f78ffe0afb8426e971009588e4ad812c478d47dfd558201c", size = 3792, upload-time = "2025-11-14T10:03:14.459Z" }, +] + +[[package]] +name = "pyobjc-framework-securityinterface" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-security" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/64/bf5b5d82655112a2314422ee649f1e1e73d4381afa87e1651ce7e8444694/pyobjc_framework_securityinterface-12.1.tar.gz", hash = "sha256:deef11ad03be8d9ff77db6e7ac40f6b641ee2d72eaafcf91040537942472e88b", size = 25552, upload-time = "2025-11-14T10:22:12.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/3e/17889a6de03dc813606bb97887dc2c4c2d4e7c8f266bc439548bae756e90/pyobjc_framework_securityinterface-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5cb5e79a73ea17663ebd29e350401162d93e42343da7d96c77efb38ae64ff01f", size = 10783, upload-time = "2025-11-14T10:03:20.202Z" }, + { url = "https://files.pythonhosted.org/packages/78/c0/b286689fca6dd23f1ad5185eb429a12fba60d157d7d53f6188c19475b331/pyobjc_framework_securityinterface-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:af5db06d53c92f05446600d241afab5aec6fec7ab10941b4eeb27a452c543b64", size = 10799, upload-time = "2025-11-14T10:03:22.296Z" }, + { url = "https://files.pythonhosted.org/packages/72/52/d378f25bb15f0d34e610f6cba50cedb0b99fdbae9bae9c0f0e715340f338/pyobjc_framework_securityinterface-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:08516c01954233fecb9bd203778b1bf559d427ccea26444ae1fa93691e751ddd", size = 11139, upload-time = "2025-11-14T10:03:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/8e/df/c6b30b5eb671755d6d59baa34c406d38524eef309886b6a7d9b7a05eb00a/pyobjc_framework_securityinterface-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:153632d23b0235faa56d26d5641e585542dac6b13b0d7b152cca27655405dec4", size = 10836, upload-time = "2025-11-14T10:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/aa/11/0e439fe86d93afd43587640e2904e73ff6d9c9401537b1e142cb623d95f6/pyobjc_framework_securityinterface-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b9eb42c5d4c62af83d69adeff3608af9cd4cfe5b7c9885a6a399be74fcc3d0f0", size = 11182, upload-time = "2025-11-14T10:03:27.948Z" }, +] + +[[package]] +name = "pyobjc-framework-securityui" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-security" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/3f/d870305f5dec58cd02966ca06ac29b69fb045d8b46dfb64e2da31f295345/pyobjc_framework_securityui-12.1.tar.gz", hash = "sha256:f1435fed85edc57533c334a4efc8032170424b759da184cb7a7a950ceea0e0b6", size = 12184, upload-time = "2025-11-14T10:22:14.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/7f/eff9ffdd34511cc95a60e5bd62f1cfbcbcec1a5012ef1168161506628c87/pyobjc_framework_securityui-12.1-py2.py3-none-any.whl", hash = "sha256:3e988b83c9a2bb0393207eaa030fc023a8708a975ac5b8ea0508cdafc2b60705", size = 3594, upload-time = "2025-11-14T10:03:29.628Z" }, +] + +[[package]] +name = "pyobjc-framework-sensitivecontentanalysis" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/ce/17bf31753e14cb4d64fffaaba2377453c4977c2c5d3cf2ff0a3db30026c7/pyobjc_framework_sensitivecontentanalysis-12.1.tar.gz", hash = "sha256:2c615ac10e93eb547b32b214cd45092056bee0e79696426fd09978dc3e670f25", size = 13745, upload-time = "2025-11-14T10:22:16.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/23/c99568a0d4e38bd8337d52e4ae25a0b0bd540577f2e06f3430c951d73209/pyobjc_framework_sensitivecontentanalysis-12.1-py2.py3-none-any.whl", hash = "sha256:faf19d32d4599ac2b18fb1ccdc3e33b2b242bdf34c02e69978bd62d3643ad068", size = 4230, upload-time = "2025-11-14T10:03:31.26Z" }, +] + +[[package]] +name = "pyobjc-framework-servicemanagement" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/d0/b26c83ae96ab55013df5fedf89337d4d62311b56ce3f520fc7597d223d82/pyobjc_framework_servicemanagement-12.1.tar.gz", hash = "sha256:08120981749a698033a1d7a6ab99dbbe412c5c0d40f2b4154014b52113511c1d", size = 14585, upload-time = "2025-11-14T10:22:18.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/5d/1009c32189f9cb26da0124b4a60640ed26dd8ad453810594f0cbfab0ff70/pyobjc_framework_servicemanagement-12.1-py2.py3-none-any.whl", hash = "sha256:9a2941f16eeb71e55e1cd94f50197f91520778c7f48ad896761f5e78725cc08f", size = 5357, upload-time = "2025-11-14T10:03:32.928Z" }, +] + +[[package]] +name = "pyobjc-framework-sharedwithyou" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-sharedwithyoucore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/8b/8ab209a143c11575a857e2111acc5427fb4986b84708b21324cbcbf5591b/pyobjc_framework_sharedwithyou-12.1.tar.gz", hash = "sha256:167d84794a48f408ee51f885210c616fda1ec4bff3dd8617a4b5547f61b05caf", size = 24791, upload-time = "2025-11-14T10:22:21.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/ee/e5113ce985a480d13a0fa3d41a242c8068dc09b3c13210557cf5cc6a544a/pyobjc_framework_sharedwithyou-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a99a6ebc6b6de7bc8663b1f07332fab9560b984a57ce344dc5703f25258f258d", size = 8763, upload-time = "2025-11-14T10:03:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/2e/51/e833c41cb6578f51623da361f6ded50b5b91331f9339b125ea50b4e62f8b/pyobjc_framework_sharedwithyou-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491b35cdb3a0bc11e730c96d4109944c77ab153573a28220ff12d41d34dd9c0f", size = 8781, upload-time = "2025-11-14T10:03:40.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/c4/b843dc3b7bd1385634df7f0bb8b557d8d09df3a384c7b2df0bc85af5bd4e/pyobjc_framework_sharedwithyou-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:50f0b32e2bf6f7ceb3af4422b015f674dc20a8cb1afa72d78f7e4186eb3710b9", size = 8917, upload-time = "2025-11-14T10:03:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b0/eca22cf9ba67c8ba04a98f8a26af0a5ca16b40e05a8100b8209a153046b1/pyobjc_framework_sharedwithyou-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:5a38bc6e3e0c9a36fe86e331eb16b680bab0024c897d252af1e611f0cd1087ef", size = 8824, upload-time = "2025-11-14T10:03:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e9/4cc7420c7356b1a25b4c9a4544454e99c3da8d50ee4b4d9b55a82eb5a836/pyobjc_framework_sharedwithyou-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1b65c51a8f6f5baf382e419cda74896d196625f1468710660a1a87a8b02b34dc", size = 8970, upload-time = "2025-11-14T10:03:45.19Z" }, +] + +[[package]] +name = "pyobjc-framework-sharedwithyoucore" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/ef/84059c5774fd5435551ab7ab40b51271cfb9997b0d21f491c6b429fe57a8/pyobjc_framework_sharedwithyoucore-12.1.tar.gz", hash = "sha256:0813149eeb755d718b146ec9365eb4ca3262b6af9ff9ba7db2f7b6f4fd104518", size = 22350, upload-time = "2025-11-14T10:22:23.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/0e/0c2b0591ebc72d437dccca7a1e7164c5f11dde2189d4f4c707a132bab740/pyobjc_framework_sharedwithyoucore-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed928266ae9d577ff73de72a03bebc66a751918eb59ca660a9eca157392f17be", size = 8530, upload-time = "2025-11-14T10:03:50.839Z" }, + { url = "https://files.pythonhosted.org/packages/5e/23/2446cb158efe0f55d983ae7b4729b3b24c52a1370b5d22bc134f046cdb34/pyobjc_framework_sharedwithyoucore-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:13eebca21722556449e47b0eda3339165b5afbb455ae00b34aabe03988affd7a", size = 8547, upload-time = "2025-11-14T10:03:52.459Z" }, + { url = "https://files.pythonhosted.org/packages/8e/42/6c5de4e508a0c0f4715e3466c0035e23b5875d2a43525a6ed81e4770ad3c/pyobjc_framework_sharedwithyoucore-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:d9aa525cdff75005a8f0ca2f7afdd1535b9e34ccafb6a92a932f3ded4b6d64d4", size = 8677, upload-time = "2025-11-14T10:03:54.15Z" }, + { url = "https://files.pythonhosted.org/packages/94/a1/24ffb35098a239a8804e469fcd7430eaee5e47bf0756c59cd77a66c3edff/pyobjc_framework_sharedwithyoucore-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2ceb4c3ad7bc1c93b4cbbbab6404d3e32714c12c36fab2932c170946af83c548", size = 8591, upload-time = "2025-11-14T10:03:56.543Z" }, + { url = "https://files.pythonhosted.org/packages/9f/5e/2460f60a931f11933ea6d5d1f7c73b6f4ade7980360cfcf327cb785b7bf8/pyobjc_framework_sharedwithyoucore-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0a55c843bd4cfdefa4a4566ccb64782466341715ecab3956c3566dbfbad0d1e5", size = 8739, upload-time = "2025-11-14T10:03:58.23Z" }, +] + +[[package]] +name = "pyobjc-framework-shazamkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/2c/8d82c5066cc376de68ad8c1454b7c722c7a62215e5c2f9dac5b33a6c3d42/pyobjc_framework_shazamkit-12.1.tar.gz", hash = "sha256:71db2addd016874639a224ed32b2000b858802b0370c595a283cce27f76883fe", size = 22518, upload-time = "2025-11-14T10:22:25.996Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5e/7d60d8e7b036b20d0e94cd7c4563e7414653344482e85fbc7facffabc95f/pyobjc_framework_shazamkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e184dd0f61a604b1cfcf44418eb95b943e7b8f536058a29e4b81acadd27a9420", size = 8577, upload-time = "2025-11-14T10:04:04.182Z" }, + { url = "https://files.pythonhosted.org/packages/a9/fa/476cf0eb6f70e434056276b1a52bb47419e4b91d80e0c8e1190ce84f888f/pyobjc_framework_shazamkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:957c5e31b2b275c822ea43d7c4435fa1455c6dc5469ad4b86b29455571794027", size = 8587, upload-time = "2025-11-14T10:04:06.351Z" }, + { url = "https://files.pythonhosted.org/packages/9a/69/105fccda6c5ca32d35edc5e055d4cffc9aefe6a40fdd00bb21ec5d21e0ce/pyobjc_framework_shazamkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:eb2875ddf18d3cd2dc2b1327f58e142b9bd86fafd32078387ed867ec5a6c5571", size = 8734, upload-time = "2025-11-14T10:04:08.33Z" }, + { url = "https://files.pythonhosted.org/packages/8d/79/09d4b2c121d3d3a662e19d67328904fd62a3303b7a169698d654a3493140/pyobjc_framework_shazamkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:951b989997a7c19d0c0d91a477d3d221ddb890085f3538ae3c520177c2322caa", size = 8647, upload-time = "2025-11-14T10:04:09.972Z" }, + { url = "https://files.pythonhosted.org/packages/74/37/859660e654ebcf6b0b4a7f3016a0473629642cf387419be2052f363a6001/pyobjc_framework_shazamkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:70f203ffe3e4c130b3a9c699d9a2081884bd7b3bd1ce08c7402b6d60fc755d75", size = 8790, upload-time = "2025-11-14T10:04:11.957Z" }, +] + +[[package]] +name = "pyobjc-framework-social" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/21/afc6f37dfdd2cafcba0227e15240b5b0f1f4ad57621aeefda2985ac9560e/pyobjc_framework_social-12.1.tar.gz", hash = "sha256:1963db6939e92ae40dd9d68852e8f88111cbfd37a83a9fdbc9a0c08993ca7e60", size = 13184, upload-time = "2025-11-14T10:22:28.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/fb/090867e332d49a1e492e4b8972ac6034d1c7d17cf39f546077f35be58c46/pyobjc_framework_social-12.1-py2.py3-none-any.whl", hash = "sha256:2f3b36ba5769503b1bc945f85fd7b255d42d7f6e417d78567507816502ff2b44", size = 4462, upload-time = "2025-11-14T10:04:14.578Z" }, +] + +[[package]] +name = "pyobjc-framework-soundanalysis" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/d6/5039b61edc310083425f87ce2363304d3a87617e941c1d07968c63b5638d/pyobjc_framework_soundanalysis-12.1.tar.gz", hash = "sha256:e2deead8b9a1c4513dbdcf703b21650dcb234b60a32d08afcec4895582b040b1", size = 14804, upload-time = "2025-11-14T10:22:29.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/d3/8df5183d52d20d459225d3f5d24f55e01b8cd9fe587ed972e3f20dd18709/pyobjc_framework_soundanalysis-12.1-py2.py3-none-any.whl", hash = "sha256:8b2029ab48c1a9772f247f0aea995e8c3ff4706909002a9c1551722769343a52", size = 4188, upload-time = "2025-11-14T10:04:16.12Z" }, +] + +[[package]] +name = "pyobjc-framework-speech" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/3d/194cf19fe7a56c2be5dfc28f42b3b597a62ebb1e1f52a7dd9c55b917ac6c/pyobjc_framework_speech-12.1.tar.gz", hash = "sha256:2a2a546ba6c52d5dd35ddcfee3fd9226a428043d1719597e8701851a6566afdd", size = 25218, upload-time = "2025-11-14T10:22:32.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1b/224cb98c9c32a6d5e68072f89d26444095be54c6f461efe4fefe9d1330a5/pyobjc_framework_speech-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cae4b88ef9563157a6c9e66b37778fc4022ee44dd1a2a53081c2adbb69698945", size = 9254, upload-time = "2025-11-14T10:04:21.361Z" }, + { url = "https://files.pythonhosted.org/packages/21/98/9ae05ebe183f35ac4bb769070f90533405d886fb9216e868e30a0e58d1ad/pyobjc_framework_speech-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:49df0ac39ae6fb44a83b2f4d7f500e0fa074ff58fbc53106d8f626d325079c23", size = 9274, upload-time = "2025-11-14T10:04:23.399Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9d/41581c58ea8f8962189bcf6a15944f9a0bf36b46c5fce611a9632b3344a2/pyobjc_framework_speech-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ed5455f6d9e473c08ebf904ae280ad5fd0d00a073448bf4f0a01fee5887c5537", size = 9430, upload-time = "2025-11-14T10:04:25.026Z" }, + { url = "https://files.pythonhosted.org/packages/00/df/2af011d05b4ab008b1e9e4b8c71b730926ef8e9599aeb8220a898603580b/pyobjc_framework_speech-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a958b3ace1425cf9319f5d8ace920c2f3dac95a5a6d1bd8742d5b64d24671e30", size = 9336, upload-time = "2025-11-14T10:04:26.764Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2e/51599acce043228164355f073b218253d57c06a2927c5dbebc300c5a4cf8/pyobjc_framework_speech-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:893052631198c5447453f81e4ed4af8077038666a7893fbe2d6a2f72b9c44b7e", size = 9496, upload-time = "2025-11-14T10:04:28.403Z" }, +] + +[[package]] +name = "pyobjc-framework-spritekit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/78/d683ebe0afb49f46d2d21d38c870646e7cb3c2e83251f264e79d357b1b74/pyobjc_framework_spritekit-12.1.tar.gz", hash = "sha256:a851f4ef5aa65cc9e08008644a528e83cb31021a1c0f17ebfce4de343764d403", size = 64470, upload-time = "2025-11-14T10:22:37.569Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/38/97c3b6c3437e3e9267fb4e1cd86e0da4eff07e0abe7cd6923644d2dfc878/pyobjc_framework_spritekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1649e57c25145795d04bb6a1ec44c20ef7cf0af7c60a9f6f5bc7998dd269db1e", size = 17802, upload-time = "2025-11-14T10:04:35.346Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c6/0e62700fbc90ab57170931fb5056d964202d49efd4d07a610fdaa28ffcfa/pyobjc_framework_spritekit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd6847cb7a287c42492ffd7c30bc08165f4fbb51b2602290e001c0d27e0aa0f0", size = 17818, upload-time = "2025-11-14T10:04:37.804Z" }, + { url = "https://files.pythonhosted.org/packages/a6/22/26b19fc487913d9324cbba824841c9ac921aa9bdd6e340ed46b9968547bc/pyobjc_framework_spritekit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:dd6e309aa284fa9b434aa7bf8ab9ab23fe52e7a372e2db3869586a74471f3419", size = 18088, upload-time = "2025-11-14T10:04:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/13/df/453d5885c79a1341e947c7654aa2c4c0cd6bed5cef4d1c16b26c58051d91/pyobjc_framework_spritekit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:5c9cb8f23436fc7bd0a8149f1271b307131a4c5669dfbb8302beef56cdca057f", size = 17787, upload-time = "2025-11-14T10:04:42.166Z" }, + { url = "https://files.pythonhosted.org/packages/6d/96/4cf353ee49e92f7df02b069eb8eeb6cc36ac09d40a016cf48d1b462dd4c4/pyobjc_framework_spritekit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9ebe7740c124ea7f8fb765e86df39f331f137be575ddb6d0d81bfb2258ee72d7", size = 18069, upload-time = "2025-11-14T10:04:44.348Z" }, +] + +[[package]] +name = "pyobjc-framework-storekit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/87/8a66a145feb026819775d44975c71c1c64df4e5e9ea20338f01456a61208/pyobjc_framework_storekit-12.1.tar.gz", hash = "sha256:818452e67e937a10b5c8451758274faa44ad5d4329df0fa85735115fb0608da9", size = 34574, upload-time = "2025-11-14T10:22:40.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/9f/938985e506de0cc3a543e44e1f9990e9e2fb8980b8f3bcfc8f7921d09061/pyobjc_framework_storekit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9fe2d65a2b644bb6b4fdd3002292cba153560917de3dd6cf969431fa32d21dd0", size = 12819, upload-time = "2025-11-14T10:04:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/d354fd6f50952148614597dd4ebd52ed1d6a3e38cbd5d88e930bd549983d/pyobjc_framework_storekit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:556c3dc187646ab8bda714a7e5630201b931956b81b0162ba420c64f55e5faaf", size = 12835, upload-time = "2025-11-14T10:04:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/4f/24/f8a8d2f1c1107a0a0f85bd830b9e0ff7016d4530924b17787cb8c7bf4f4c/pyobjc_framework_storekit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:15d4643bc4de4aa62f72efcb7a4930bd7e15280867be225bd2c582b3367d75ae", size = 13028, upload-time = "2025-11-14T10:04:55.605Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9b/3d510cc03d5aeef298356578aa8077e4ddebea0a0cd2f50a13bf4f98f9e8/pyobjc_framework_storekit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:5e9354f2373b243066358bf32988d07d8a2da6718563ee6946a40c981a37c7c1", size = 12828, upload-time = "2025-11-14T10:04:57.557Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0c/760f3d4e4deedc11c4144fa3fdf2a697ea7e2f7eef492f6662687b872085/pyobjc_framework_storekit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d11ffe3f8e638ebe7c156c5bf2919115c7562f44f44be8067521b7c5f6e50553", size = 13013, upload-time = "2025-11-14T10:04:59.517Z" }, +] + +[[package]] +name = "pyobjc-framework-symbols" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/ce/a48819eb8524fa2dc11fb3dd40bb9c4dcad0596fe538f5004923396c2c6c/pyobjc_framework_symbols-12.1.tar.gz", hash = "sha256:7d8e999b8a59c97d38d1d343b6253b1b7d04bf50b665700957d89c8ac43b9110", size = 12782, upload-time = "2025-11-14T10:22:42.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/ea/6e9af9c750d68109ac54fbffb5463e33a7b54ffe8b9901a5b6b603b7884b/pyobjc_framework_symbols-12.1-py2.py3-none-any.whl", hash = "sha256:c72eecbc25f6bfcd39c733067276270057c5aca684be20fdc56def645f2b6446", size = 3331, upload-time = "2025-11-14T10:05:01.333Z" }, +] + +[[package]] +name = "pyobjc-framework-syncservices" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coredata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/91/6d03a988831ddb0fb001b13573560e9a5bcccde575b99350f98fe56a2dd4/pyobjc_framework_syncservices-12.1.tar.gz", hash = "sha256:6a213e93d9ce15128810987e4c5de8c73cfab1564ac8d273e6b437a49965e976", size = 31032, upload-time = "2025-11-14T10:22:45.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/ac/a83cdd120e279ee905e9085afda90992159ed30c6a728b2c56fa2d36b6ea/pyobjc_framework_syncservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cd629bea95692aad2d26196657cde2fbadedae252c7846964228661a600b900", size = 13411, upload-time = "2025-11-14T10:05:07.741Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e3/9a6bd76529feffe08a3f6b2962c9a96d75febc02453881ec81389ff9ac13/pyobjc_framework_syncservices-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:606afac9255b5bf828f1dcf7b0d7bdc7726021b686ad4f5743978eb4086902d9", size = 13425, upload-time = "2025-11-14T10:05:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5d/338850a31968b94417ba95a7b94db9fcd40b16011eaf82f757de7c1eba6c/pyobjc_framework_syncservices-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9d1ebe60e92efd08455be209a265879cf297feda831aadf36431f38229b1dd52", size = 13599, upload-time = "2025-11-14T10:05:11.732Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/f27f1a706a72c7a87a2aa37e49ae5f5e7445e02323218638e6ff5897c5c9/pyobjc_framework_syncservices-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2af99db7c23f0368300e8bd428ecfb75b14449d3467e883ff544dbc5ae9e1351", size = 13404, upload-time = "2025-11-14T10:05:13.677Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/0b135d4af853fabc9a794e78647100503457f9e42e8c0289f745c558c105/pyobjc_framework_syncservices-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c27754af8cb86bd445e1182a184617229fa70cf3a716e740a93b0622f44ceb27", size = 13585, upload-time = "2025-11-14T10:05:16.03Z" }, +] + +[[package]] +name = "pyobjc-framework-systemconfiguration" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/7d/50848df8e1c6b5e13967dee9fb91d3391fe1f2399d2d0797d2fc5edb32ba/pyobjc_framework_systemconfiguration-12.1.tar.gz", hash = "sha256:90fe04aa059876a21626931c71eaff742a27c79798a46347fd053d7008ec496e", size = 59158, upload-time = "2025-11-14T10:22:53.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/d3/bb935c3d4bae9e6ce4a52638e30eea7039c480dd96bc4f0777c9fabda21b/pyobjc_framework_systemconfiguration-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e5bb9103d39483964431db7125195c59001b7bff2961869cfe157b4c861e52d", size = 21578, upload-time = "2025-11-14T10:05:25.572Z" }, + { url = "https://files.pythonhosted.org/packages/64/26/22f031c99fd7012dffa41455951004a758aaf9a25216b3a4ee83496bc44f/pyobjc_framework_systemconfiguration-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:359b35c00f52f57834169c1057522279201ac5a64ac5b4d90dbafa40ad6c54b4", size = 21575, upload-time = "2025-11-14T10:05:28.396Z" }, + { url = "https://files.pythonhosted.org/packages/f2/58/648803bdf3d2ebd3221ef43deb008c77aefe0bec231af2aa67e5b29a78e2/pyobjc_framework_systemconfiguration-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f4ff57defb4dcd933db392eb8ea9e5a46005cb7a6f2b46c27ab2dd5e13a459ab", size = 21990, upload-time = "2025-11-14T10:05:30.875Z" }, + { url = "https://files.pythonhosted.org/packages/05/95/9fbb2ab26f03142b84ff577dcd2dcd3ca8b0c13c2f6193ceecd20544b7a5/pyobjc_framework_systemconfiguration-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e9c597c13b9815dce7e1fccdfae7c66b9df98e8c688b7afdf4af39de26d917b3", size = 21612, upload-time = "2025-11-14T10:05:33.387Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/c1d5ea1089c41f0d1563ab42d6ff6ed320e195646008c8fdaa3e31d354cd/pyobjc_framework_systemconfiguration-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:10ad47ec2bee4f567e78369359b8c75a23097c6d89b11aa37840c22cc79229f1", size = 21997, upload-time = "2025-11-14T10:05:36.211Z" }, +] + +[[package]] +name = "pyobjc-framework-systemextensions" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/01/8a706cd3f7dfcb9a5017831f2e6f9e5538298e90052db3bb8163230cbc4f/pyobjc_framework_systemextensions-12.1.tar.gz", hash = "sha256:243e043e2daee4b5c46cd90af5fff46b34596aac25011bab8ba8a37099685eeb", size = 20701, upload-time = "2025-11-14T10:22:58.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/cc/a42883d6ad0ae257a7fa62660b4dd13be15f8fa657922f9a5b6697f26e28/pyobjc_framework_systemextensions-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:01fac4f8d88c0956d9fc714d24811cd070e67200ba811904317d91e849e38233", size = 9166, upload-time = "2025-11-14T10:05:41.479Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ef/fd34784added1dff088bd18cc2694049b0893b01e835587eab1735fd68f3/pyobjc_framework_systemextensions-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:038032801d46cc7b1ea69400f43d5c17b25d7a16efa7a7d9727b25789387a8cf", size = 9185, upload-time = "2025-11-14T10:05:43.136Z" }, + { url = "https://files.pythonhosted.org/packages/72/76/fd6f06e54299998677548bacd21105450bc6435df215a6620422a31b0099/pyobjc_framework_systemextensions-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:2aea4e823d915abca463b1c091ff969cef09108c88b71b68569485dec6f3651d", size = 9345, upload-time = "2025-11-14T10:05:44.814Z" }, + { url = "https://files.pythonhosted.org/packages/af/c8/4e9669b6b43af7f50df43cb76af84805ee3a9b32881d69b4e7685edd3017/pyobjc_framework_systemextensions-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:51f0a4488fa245695c7e8c1c83909c86bf27b34519807437c753602ff6d7e9af", size = 9253, upload-time = "2025-11-14T10:05:46.508Z" }, + { url = "https://files.pythonhosted.org/packages/18/6e/91e55fa71bd402acbf06ecfc342e4f56dbc0f7d622be1e5dd22d13508d0e/pyobjc_framework_systemextensions-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b393e3bf85ccb9321f134405eac6fd16a8e7f048286301b67f0cf8d99588bf29", size = 9412, upload-time = "2025-11-14T10:05:48.256Z" }, +] + +[[package]] +name = "pyobjc-framework-threadnetwork" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/7e/f1816c3461e4121186f2f7750c58af083d1826bbd73f72728da3edcf4915/pyobjc_framework_threadnetwork-12.1.tar.gz", hash = "sha256:e071eedb41bfc1b205111deb54783ec5a035ccd6929e6e0076336107fdd046ee", size = 12788, upload-time = "2025-11-14T10:23:00.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/b8/94b37dd353302c051a76f1a698cf55b5ad50ca061db7f0f332aa9e195766/pyobjc_framework_threadnetwork-12.1-py2.py3-none-any.whl", hash = "sha256:07d937748fc54199f5ec04d5a408e8691a870481c11b641785c2adc279dd8e4b", size = 3771, upload-time = "2025-11-14T10:05:49.899Z" }, +] + +[[package]] +name = "pyobjc-framework-uniformtypeidentifiers" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/b8/dd9d2a94509a6c16d965a7b0155e78edf520056313a80f0cd352413f0d0b/pyobjc_framework_uniformtypeidentifiers-12.1.tar.gz", hash = "sha256:64510a6df78336579e9c39b873cfcd03371c4b4be2cec8af75a8a3d07dff607d", size = 17030, upload-time = "2025-11-14T10:23:02.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/5f/1f10f5275b06d213c9897850f1fca9c881c741c1f9190cea6db982b71824/pyobjc_framework_uniformtypeidentifiers-12.1-py2.py3-none-any.whl", hash = "sha256:ec5411e39152304d2a7e0e426c3058fa37a00860af64e164794e0bcffee813f2", size = 4901, upload-time = "2025-11-14T10:05:51.532Z" }, +] + +[[package]] +name = "pyobjc-framework-usernotifications" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/cd/e0253072f221fa89a42fe53f1a2650cc9bf415eb94ae455235bd010ee12e/pyobjc_framework_usernotifications-12.1.tar.gz", hash = "sha256:019ccdf2d400f9a428769df7dba4ea97c02453372bc5f8b75ce7ae54dfe130f9", size = 29749, upload-time = "2025-11-14T10:23:05.364Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/c95053a475246464cba686e16269b0973821601910d1947d088b855a8dac/pyobjc_framework_usernotifications-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:412afb2bf5fe0049f9c4e732e81a8a35d5ebf97c30a5a6abd276259d020c82ac", size = 9644, upload-time = "2025-11-14T10:05:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/b1/cc/4c6efe6a65b1742ea238734f81509ceba5346b45f605baa809ca63f30692/pyobjc_framework_usernotifications-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:40a5457f4157ca007f80f0644413f44f0dc141f7864b28e1728623baf56a8539", size = 9659, upload-time = "2025-11-14T10:05:58.763Z" }, + { url = "https://files.pythonhosted.org/packages/06/4e/02ff6975567974f360cf0e1e358236026e35f7ba7795511bc4dcbaa13f62/pyobjc_framework_usernotifications-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:58c09bd1bd7a8cd29613d0d0e6096eda6c8465dc5a7a733675e1b8d0406f7adc", size = 9811, upload-time = "2025-11-14T10:06:00.775Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1a/caa96066b36c2c20ba6f033857fc24ff8e6b5811cf1bc112818928d27216/pyobjc_framework_usernotifications-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:cc69e2aed9b55296a447f2fb69cc52a1a026c50e46253dbf482f5807bce3ae7c", size = 9720, upload-time = "2025-11-14T10:06:02.409Z" }, + { url = "https://files.pythonhosted.org/packages/95/f7/8def35e9e7b2a7a7d4e61923b0f29fcdca70df5ac6b91cddb418a1d5ffed/pyobjc_framework_usernotifications-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0746d2a67ca05ae907b7551ccd3a534e9d6e76115882ab962365f9ad259c4032", size = 9876, upload-time = "2025-11-14T10:06:04.07Z" }, +] + +[[package]] +name = "pyobjc-framework-usernotificationsui" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-usernotifications" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/03/73e29fd5e5973cb3800c9d56107c1062547ef7524cbcc757c3cbbd5465c6/pyobjc_framework_usernotificationsui-12.1.tar.gz", hash = "sha256:51381c97c7344099377870e49ed0871fea85ba50efe50ab05ccffc06b43ec02e", size = 13125, upload-time = "2025-11-14T10:23:07.259Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/c8/52ac8a879079c1fbf25de8335ff506f7db87ff61e64838b20426f817f5d5/pyobjc_framework_usernotificationsui-12.1-py2.py3-none-any.whl", hash = "sha256:11af59dc5abfcb72c08769ab4d7ca32a628527a8ba341786431a0d2dacf31605", size = 3933, upload-time = "2025-11-14T10:06:05.478Z" }, +] + +[[package]] +name = "pyobjc-framework-videosubscriberaccount" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/f8/27927a9c125c622656ee5aada4596ccb8e5679da0260742360f193df6dcf/pyobjc_framework_videosubscriberaccount-12.1.tar.gz", hash = "sha256:750459fa88220ab83416f769f2d5d210a1f77b8938fa4d119aad0002fc32846b", size = 18793, upload-time = "2025-11-14T10:23:09.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/ca/e2f982916267508c1594f1e50d27bf223a24f55a5e175ab7d7822a00997c/pyobjc_framework_videosubscriberaccount-12.1-py2.py3-none-any.whl", hash = "sha256:381a5e8a3016676e52b88e38b706559fa09391d33474d8a8a52f20a883104a7b", size = 4825, upload-time = "2025-11-14T10:06:07.027Z" }, +] + +[[package]] +name = "pyobjc-framework-videotoolbox" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coremedia" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/5f/6995ee40dc0d1a3460ee183f696e5254c0ad14a25b5bc5fd9bd7266c077b/pyobjc_framework_videotoolbox-12.1.tar.gz", hash = "sha256:7adc8670f3b94b086aed6e86c3199b388892edab4f02933c2e2d9b1657561bef", size = 57825, upload-time = "2025-11-14T10:23:13.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/a5/91c6c95416f41c412c2079950527cb746c0712ec319c51a6c728c8d6b231/pyobjc_framework_videotoolbox-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eb6ce6837344ee319122066c16ada4beb913e7bfd62188a8d14b1ecbb5a89234", size = 18908, upload-time = "2025-11-14T10:06:14.087Z" }, + { url = "https://files.pythonhosted.org/packages/f0/59/7fc3d67df437f3e263b477dd181eef3ac3430cb7eb1acc951f5f1e84cc4d/pyobjc_framework_videotoolbox-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca28b39e22016eb5f81f540102a575ee6e6114074d09e17e22eb3b5647976d93", size = 18929, upload-time = "2025-11-14T10:06:16.418Z" }, + { url = "https://files.pythonhosted.org/packages/f4/41/08b526d2f228271994f8216651d2e5c8e76415224daa012e67c53c90fc7a/pyobjc_framework_videotoolbox-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:dba7e078df01432331ee75a90c2c147264bfdb9e31998b4e4fc28913b93b832e", size = 19139, upload-time = "2025-11-14T10:06:18.602Z" }, + { url = "https://files.pythonhosted.org/packages/00/a9/581edc658e3ae242a55d463092a237cf9f744ba5a91d91c769af7d3f2ac6/pyobjc_framework_videotoolbox-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e67a3890916346b7c15c9270d247e191c3899e4698fee79d460a476145715401", size = 18927, upload-time = "2025-11-14T10:06:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/91/17/97f3e4704246b0496c90bf4c604005f426f62c75e616e68d2e3f8833affb/pyobjc_framework_videotoolbox-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:67227431c340e308c4ecdce743b5d1d27757994663c983f179f2e934acdacb99", size = 19121, upload-time = "2025-11-14T10:06:23.072Z" }, +] + +[[package]] +name = "pyobjc-framework-virtualization" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/6a/9d110b5521d9b898fad10928818c9f55d66a4af9ac097426c65a9878b095/pyobjc_framework_virtualization-12.1.tar.gz", hash = "sha256:e96afd8e801e92c6863da0921e40a3b68f724804f888bce43791330658abdb0f", size = 40682, upload-time = "2025-11-14T10:23:17.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/f2/0da47e91f3f8eeda9a8b4bb0d3a0c54a18925009e99b66a8226b9e06ce1e/pyobjc_framework_virtualization-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7d5724b38e64b39ab5ec3b45993afa29fc88b307d99ee2c7a1c0fd770e9b4b21", size = 13131, upload-time = "2025-11-14T10:06:29.337Z" }, + { url = "https://files.pythonhosted.org/packages/76/ca/228fffccbeafecbe7599fc2cdaa64bf2a8e42fd8fe619c5b670c92b263c3/pyobjc_framework_virtualization-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:232956de8a0c3086a58c96621e0a2148497d1750ebb1bb6bea9f7f34ec3c83c6", size = 13147, upload-time = "2025-11-14T10:06:31.294Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2f/4e56147bc9963bb7f96886fda376004a66c5abe579dc029180952fd872fa/pyobjc_framework_virtualization-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a9552e49b967fb520e5be1cfce510e0b68c2ba314a28ac90aad36fe33218d430", size = 13351, upload-time = "2025-11-14T10:06:33.189Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/ed32bb177edca9feedd518aa2f98c75e86365497f086af21d807785d264c/pyobjc_framework_virtualization-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e40bff972adfefbe8a02e508571b32c58e90e4d974d65470eab75c53fe47006d", size = 13137, upload-time = "2025-11-14T10:06:35.426Z" }, + { url = "https://files.pythonhosted.org/packages/3b/01/fc9a7714bd3d9d43085c7c027c395b9c0205a330956f200bfa3c41b09a82/pyobjc_framework_virtualization-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8d53e81f1928c4e90cbebebd39b965aa679f7fadda1fd075e18991872c4cb56b", size = 13343, upload-time = "2025-11-14T10:06:37.219Z" }, +] + +[[package]] +name = "pyobjc-framework-vision" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coreml" }, + { name = "pyobjc-framework-quartz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/5a/08bb3e278f870443d226c141af14205ff41c0274da1e053b72b11dfc9fb2/pyobjc_framework_vision-12.1.tar.gz", hash = "sha256:a30959100e85dcede3a786c544e621ad6eb65ff6abf85721f805822b8c5fe9b0", size = 59538, upload-time = "2025-11-14T10:23:21.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/5a/23502935b3fc877d7573e743fc3e6c28748f33a45c43851d503bde52cde7/pyobjc_framework_vision-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6b3211d84f3a12aad0cde752cfd43a80d0218960ac9e6b46b141c730e7d655bd", size = 16625, upload-time = "2025-11-14T10:06:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e4/e87361a31b82b22f8c0a59652d6e17625870dd002e8da75cb2343a84f2f9/pyobjc_framework_vision-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7273e2508db4c2e88523b4b7ff38ac54808756e7ba01d78e6c08ea68f32577d2", size = 16640, upload-time = "2025-11-14T10:06:46.653Z" }, + { url = "https://files.pythonhosted.org/packages/b1/dd/def55d8a80b0817f486f2712fc6243482c3264d373dc5ff75037b3aeb7ea/pyobjc_framework_vision-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:04296f0848cc8cdead66c76df6063720885cbdf24fdfd1900749a6e2297313db", size = 16782, upload-time = "2025-11-14T10:06:48.816Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a4/ee1ef14d6e1df6617e64dbaaa0ecf8ecb9e0af1425613fa633f6a94049c1/pyobjc_framework_vision-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:631add775ed1dafb221a6116137cdcd78432addc16200ca434571c2a039c0e03", size = 16614, upload-time = "2025-11-14T10:06:50.852Z" }, + { url = "https://files.pythonhosted.org/packages/af/53/187743d9244becd4499a77f8ee699ae286e2f6ade7c0c7ad2975ae60f187/pyobjc_framework_vision-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fe41a1a70cc91068aee7b5293fa09dc66d1c666a8da79fdf948900988b439df6", size = 16771, upload-time = "2025-11-14T10:06:53.04Z" }, +] + +[[package]] +name = "pyobjc-framework-webkit" +version = "12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/10/110a50e8e6670765d25190ca7f7bfeecc47ec4a8c018cb928f4f82c56e04/pyobjc_framework_webkit-12.1.tar.gz", hash = "sha256:97a54dd05ab5266bd4f614e41add517ae62cdd5a30328eabb06792474b37d82a", size = 284531, upload-time = "2025-11-14T10:23:40.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/67/64920c8d201a7fc27962f467c636c4e763b43845baba2e091a50a97a5d52/pyobjc_framework_webkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:af2c7197447638b92aafbe4847c063b6dd5e1ed83b44d3ce7e71e4c9b042ab5a", size = 50084, upload-time = "2025-11-14T10:07:05.868Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/80d36280164c69220ce99372f7736a028617c207e42cb587716009eecb88/pyobjc_framework_webkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1da0c428c9d9891c93e0de51c9f272bfeb96d34356cdf3136cb4ad56ce32ec2d", size = 50096, upload-time = "2025-11-14T10:07:10.027Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7a/03c29c46866e266b0c705811c55c22625c349b0a80f5cf4776454b13dc4c/pyobjc_framework_webkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1a29e334d5a7dd4a4f0b5647481b6ccf8a107b92e67b2b3c6b368c899f571965", size = 50572, upload-time = "2025-11-14T10:07:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ac/924878f239c167ffe3bfc643aee4d6dd5b357e25f6b28db227e40e9e6df3/pyobjc_framework_webkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:99d0d28542a266a95ee2585f51765c0331794bca461aaf4d1f5091489d475179", size = 50210, upload-time = "2025-11-14T10:07:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/637cda4983dc0936b73a385f3906256953ac434537b812814cb0b6d231a2/pyobjc_framework_webkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1aaa3bf12c7b68e1a36c0b294d2728e06f2cc220775e6dc4541d5046290e4dc8", size = 50680, upload-time = "2025-11-14T10:07:23.331Z" }, +] + +[[package]] +name = "pyotp" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/b2/1d5994ba2acde054a443bd5e2d384175449c7d2b6d1a0614dbca3a63abfc/pyotp-2.9.0.tar.gz", hash = "sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63", size = 17763, upload-time = "2023-07-27T23:41:03.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/c0/c33c8792c3e50193ef55adb95c1c3c2786fe281123291c2dbf0eaab95a6f/pyotp-2.9.0-py3-none-any.whl", hash = "sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612", size = 13376, upload-time = "2023-07-27T23:41:01.685Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pypdf" +version = "6.6.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/bb/a44bab1ac3c54dbcf653d7b8bcdee93dddb2d3bf025a3912cacb8149a2f2/pypdf-6.6.2.tar.gz", hash = "sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016", size = 5281850, upload-time = "2026-01-26T11:57:55.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/be/549aaf1dfa4ab4aed29b09703d2fb02c4366fc1f05e880948c296c5764b9/pypdf-6.6.2-py3-none-any.whl", hash = "sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba", size = 329132, upload-time = "2026-01-26T11:57:54.099Z" }, +] + +[[package]] +name = "pypdf" +version = "6.7.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/52/37cc0aa9e9d1bf7729a737a0d83f8b3f851c8eb137373d9f71eafb0a3405/pypdf-6.7.5.tar.gz", hash = "sha256:40bb2e2e872078655f12b9b89e2f900888bb505e88a82150b64f9f34fa25651d", size = 5304278, upload-time = "2026-03-02T09:05:21.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/89/336673efd0a88956562658aba4f0bbef7cb92a6fbcbcaf94926dbc82b408/pypdf-6.7.5-py3-none-any.whl", hash = "sha256:07ba7f1d6e6d9aa2a17f5452e320a84718d4ce863367f7ede2fd72280349ab13", size = 331421, upload-time = "2026-03-02T09:05:19.722Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, +] + +[[package]] +name = "python-docx" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "questionary" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, +] + +[[package]] +name = "rank-bm25" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/0a/f9579384aa017d8b4c15613f86954b92a95a93d641cc849182467cf0bb3b/rank_bm25-0.2.2.tar.gz", hash = "sha256:096ccef76f8188563419aaf384a02f0ea459503fdf77901378d4fd9d87e5e51d", size = 8347, upload-time = "2022-02-16T12:10:52.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/21/f691fb2613100a62b3fa91e9988c991e9ca5b89ea31c0d3152a3210344f9/rank_bm25-0.2.2-py3-none-any.whl", hash = "sha256:7bd4a95571adadfc271746fa146a4bcfd89c0cf731e49c3d1ad863290adbe8ae", size = 8584, upload-time = "2022-02-16T12:10:50.626Z" }, +] + +[[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, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, + { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, + { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, + { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, + { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, + { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, + { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, + { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, + { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, + { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, + { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, + { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, +] + +[[package]] +name = "reportlab" +version = "4.4.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "charset-normalizer", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pillow", version = "12.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/39/42cf24aee570a80e1903221ae3a92a2e34c324794a392eb036cbb6dc3839/reportlab-4.4.9.tar.gz", hash = "sha256:7cf487764294ee791a4781f5a157bebce262a666ae4bbb87786760a9676c9378", size = 3911246, upload-time = "2026-01-15T10:07:56.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/77/546e50edfaba6a0e58e8ec5fdc4446510227cec9e8f40172b60941d5a633/reportlab-4.4.9-py3-none-any.whl", hash = "sha256:68e2d103ae8041a37714e8896ec9b79a1c1e911d68c3bd2ea17546568cf17bfd", size = 1954401, upload-time = "2026-01-15T09:27:59.133Z" }, +] + +[[package]] +name = "reportlab" +version = "4.4.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "charset-normalizer", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pillow", version = "12.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/57/28bfbf0a775b618b6e4d854ef8dd3f5c8988e5d614d8898703502a35f61c/reportlab-4.4.10.tar.gz", hash = "sha256:5cbbb34ac3546039d0086deb2938cdec06b12da3cdb836e813258eb33cd28487", size = 3714962, upload-time = "2026-02-12T10:45:21.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/2e/e1798b8b248e1517e74c6cdf10dd6edd485044e7edf46b5f11ffcc5a0add/reportlab-4.4.10-py3-none-any.whl", hash = "sha256:5abc815746ae2bc44e7ff25db96814f921349ca814c992c7eac3c26029bf7c24", size = 1955400, upload-time = "2026-02-12T10:45:18.828Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rich" +version = "14.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "markdown-it-py", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "pygments", marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +dependencies = [ + { name = "markdown-it-py", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pygments", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "screeninfo" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cython", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/bb/e69e5e628d43f118e0af4fc063c20058faa8635c95a1296764acc8167e27/screeninfo-0.8.1.tar.gz", hash = "sha256:9983076bcc7e34402a1a9e4d7dabf3729411fd2abb3f3b4be7eba73519cd2ed1", size = 10666, upload-time = "2022-09-09T11:35:23.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/bf/c5205d480307bef660e56544b9e3d7ff687da776abb30c9cb3f330887570/screeninfo-0.8.1-py3-none-any.whl", hash = "sha256:e97d6b173856edcfa3bd282f81deb528188aff14b11ec3e195584e7641be733c", size = 12907, upload-time = "2022-09-09T11:35:21.351Z" }, +] + +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "5.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/30/21664028fc0776eb1ca024879480bbbab36f02923a8ff9e4cae5a150fa35/sentence_transformers-5.2.3.tar.gz", hash = "sha256:3cd3044e1f3fe859b6a1b66336aac502eaae5d3dd7d5c8fc237f37fbf58137c7", size = 381623, upload-time = "2026-02-17T14:05:20.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/9f/dba4b3e18ebbe1eaa29d9f1764fbc7da0cd91937b83f2b7928d15c5d2d36/sentence_transformers-5.2.3-py3-none-any.whl", hash = "sha256:6437c62d4112b615ddebda362dfc16a4308d604c5b68125ed586e3e95d5b2e30", size = 494225, upload-time = "2026-02-17T14:05:18.596Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + +[[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, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "skops" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "prettytable" }, + { name = "scikit-learn" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/0c/5ec987633e077dd0076178ea6ade2d6e57780b34afea0b497fb507d7a1ed/skops-0.13.0.tar.gz", hash = "sha256:66949fd3c95cbb5c80270fbe40293c0fe1e46cb4a921860e42584dd9c20ebeb1", size = 581312, upload-time = "2025-08-06T09:48:14.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/e8/6a2b2030f0689f894432b9c2f0357f2f3286b2a00474827e04b8fe9eea13/skops-0.13.0-py3-none-any.whl", hash = "sha256:55e2cccb18c86f5916e4cfe5acf55ed7b0eecddf08a151906414c092fa5926dc", size = 131200, upload-time = "2025-08-06T09:48:13.356Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +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/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "tau2" +version = "0.2.1.dev0" +source = { git = "https://github.com/sierra-research/tau2-bench.git?branch=dev%2Ftau3#17e07b1da2bbc0cadfddeea36412686e0604127b" } +dependencies = [ + { name = "addict" }, + { name = "deepdiff" }, + { name = "docstring-parser" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "litellm" }, + { name = "loguru" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "psutil" }, + { name = "python-dotenv", version = "1.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "python-dotenv", version = "1.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich", version = "14.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "rich", version = "14.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "tabulate" }, + { name = "tenacity" }, + { name = "toml" }, + { name = "typer" }, + { name = "uvicorn" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" }, + { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, + { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, + { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, + { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, + { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "transformers" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer-slim" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/7e/8a0c57d562015e5b16c97c1f0b8e0e92ead2c7c20513225dc12c2043ba9f/transformers-5.2.0.tar.gz", hash = "sha256:0088b8b46ccc9eff1a1dca72b5d618a5ee3b1befc3e418c9512b35dea9f9a650", size = 8618176, upload-time = "2026-02-16T18:54:02.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/93/79754b0ca486e556c2b95d4f5afc66aaf4b260694f3d6e1b51da2d036691/transformers-5.2.0-py3-none-any.whl", hash = "sha256:9ecaf243dc45bee11a7d93f8caf03746accc0cb069181bbf4ad8566c53e854b4", size = 10403304, upload-time = "2026-02-16T18:53:59.699Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich", version = "14.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or sys_platform != 'win32'" }, + { name = "rich", version = "14.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a7/e6aecc4b4eb59598829a3b5076a93aff291b4fdaa2ded25efc4e1f4d219c/typer_slim-0.24.0.tar.gz", hash = "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34", size = 4776, upload-time = "2026-02-16T22:08:51.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, + { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, + { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, + { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, + { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, +] + +[[package]] +name = "uuid7" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/19/7472bd526591e2192926247109dbf78692e709d3e56775792fec877a7720/uuid7-0.1.0.tar.gz", hash = "sha256:8c57aa32ee7456d3cc68c95c4530bc571646defac01895cfc73545449894a63c", size = 14052, upload-time = "2021-12-29T01:38:21.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/77/8852f89a91453956582a85024d80ad96f30a41fed4c2b3dce0c9f12ecc7e/uuid7-0.1.0-py2.py3-none-any.whl", hash = "sha256:5e259bb63c8cb4aded5927ff41b444a80d0c7124e8a0ced7cf44efa1f5cccf61", size = 7477, upload-time = "2021-12-29T01:38:20.418Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, +] + +[[package]] +name = "waitress" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/cb/04ddb054f45faa306a230769e868c28b8065ea196891f09004ebace5b184/waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f", size = 179901, upload-time = "2024-11-16T20:02:35.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/57/a27182528c90ef38d82b636a11f606b0cbb0e17588ed205435f8affe3368/waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e", size = 56232, upload-time = "2024-11-16T20:02:33.858Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]