From a0970bc09a585b645f02582c9fc841ec46b41e1d Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Sun, 24 May 2026 15:20:18 -0700 Subject: [PATCH] feat: add ai-agent-app example with LangChain-backed agent wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new example app demonstrating a Laravel-inspired declarative API for building AI agents. Core piece is packages/agent.py — a base Agent class with decorators (provider, model, max_tokens, max_steps, etc.) that uses LangChain as the provider backend so Anthropic, OpenAI, and Google models are supported through a single interface. Key capabilities: - Agentic tool loop via LangChain .bind_tools() + ToolMessage feedback - Structured output via .with_structured_output(PydanticModel) - Token streaming via .stream() - Middleware chain (before/after hooks) - Fake/snapshot system for testing (record-and-replay) Co-Authored-By: Claude Sonnet 4.6 --- example/ai-agent-app/.gitignore | 9 + example/ai-agent-app/artisan | 8 + example/ai-agent-app/bootstrap/__init__.py | 0 example/ai-agent-app/bootstrap/application.py | 18 + example/ai-agent-app/memory.md | 42 + example/ai-agent-app/memory.py | 177 ++ example/ai-agent-app/package-lock.json | 1754 +++++++++++++++++ example/ai-agent-app/package.json | 29 + example/ai-agent-app/packages/__init__.py | 27 + example/ai-agent-app/packages/agent.py | 765 +++++++ example/ai-agent-app/providers/__init__.py | 0 .../providers/fastapi_provider.py | 23 + example/ai-agent-app/public/.gitkeep | 0 example/ai-agent-app/public/hot | 1 + example/ai-agent-app/pyproject.toml | 25 + example/ai-agent-app/resources/css/app.css | 1 + .../resources/js/Pages/Chat/Index.tsx | 141 ++ .../resources/js/Pages/Dashboard/Index.tsx | 14 + example/ai-agent-app/resources/js/app.tsx | 19 + example/ai-agent-app/routes/__init__.py | 0 example/ai-agent-app/routes/web.py | 40 + example/ai-agent-app/storage/logs/.gitkeep | 0 example/ai-agent-app/templates/index.html | 13 + example/ai-agent-app/tsconfig.json | 19 + example/ai-agent-app/uv.lock | 1487 ++++++++++++++ example/ai-agent-app/vite.config.js | 21 + 26 files changed, 4633 insertions(+) create mode 100644 example/ai-agent-app/.gitignore create mode 100644 example/ai-agent-app/artisan create mode 100644 example/ai-agent-app/bootstrap/__init__.py create mode 100644 example/ai-agent-app/bootstrap/application.py create mode 100644 example/ai-agent-app/memory.md create mode 100644 example/ai-agent-app/memory.py create mode 100644 example/ai-agent-app/package-lock.json create mode 100644 example/ai-agent-app/package.json create mode 100644 example/ai-agent-app/packages/__init__.py create mode 100644 example/ai-agent-app/packages/agent.py create mode 100644 example/ai-agent-app/providers/__init__.py create mode 100644 example/ai-agent-app/providers/fastapi_provider.py create mode 100644 example/ai-agent-app/public/.gitkeep create mode 100644 example/ai-agent-app/public/hot create mode 100644 example/ai-agent-app/pyproject.toml create mode 100644 example/ai-agent-app/resources/css/app.css create mode 100644 example/ai-agent-app/resources/js/Pages/Chat/Index.tsx create mode 100644 example/ai-agent-app/resources/js/Pages/Dashboard/Index.tsx create mode 100644 example/ai-agent-app/resources/js/app.tsx create mode 100644 example/ai-agent-app/routes/__init__.py create mode 100644 example/ai-agent-app/routes/web.py create mode 100644 example/ai-agent-app/storage/logs/.gitkeep create mode 100644 example/ai-agent-app/templates/index.html create mode 100644 example/ai-agent-app/tsconfig.json create mode 100644 example/ai-agent-app/uv.lock create mode 100644 example/ai-agent-app/vite.config.js diff --git a/example/ai-agent-app/.gitignore b/example/ai-agent-app/.gitignore new file mode 100644 index 00000000..82024b8c --- /dev/null +++ b/example/ai-agent-app/.gitignore @@ -0,0 +1,9 @@ +.venv/ +node_modules/ +__pycache__/ +*.pyc +.env +.claude/ +dist/ +htmlcov/ +.coverage diff --git a/example/ai-agent-app/artisan b/example/ai-agent-app/artisan new file mode 100644 index 00000000..95887b0b --- /dev/null +++ b/example/ai-agent-app/artisan @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +import sys +from bootstrap.application import app + +if __name__ == "__main__": + status = app.handle_command() + sys.exit(status if isinstance(status, int) else 0) diff --git a/example/ai-agent-app/bootstrap/__init__.py b/example/ai-agent-app/bootstrap/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/example/ai-agent-app/bootstrap/application.py b/example/ai-agent-app/bootstrap/application.py new file mode 100644 index 00000000..152e0772 --- /dev/null +++ b/example/ai-agent-app/bootstrap/application.py @@ -0,0 +1,18 @@ +from pathlib import Path + +from fastapi_startkit import Application +from fastapi_startkit.inertia import InertiaProvider +from fastapi_startkit.logging import LogProvider +from fastapi_startkit.vite import ViteProvider + +from providers.fastapi_provider import FastAPIProvider + +app: Application = Application( + base_path=Path(__file__).parent.parent, + providers=[ + LogProvider, + FastAPIProvider, + ViteProvider, + InertiaProvider, + ], +) diff --git a/example/ai-agent-app/memory.md b/example/ai-agent-app/memory.md new file mode 100644 index 00000000..68a47ee0 --- /dev/null +++ b/example/ai-agent-app/memory.md @@ -0,0 +1,42 @@ + +-------------------+ + | Input/Event | + +-------------------+ + | + v + +-------------------+ + | Memory Router | + | classify memory | + +-------------------+ + | | | + +-----------+ | +-------------+ + | | | + v v v + + +---------------+ +---------------+ +---------------+ + | Semantic Mem | | Episodic Mem | | Task Memory | + | facts/knowledge| | events/logs | | goals/progress| + +---------------+ +---------------+ +---------------+ + | | | + +----------------+------------------+ + | + v + +------------------+ + | Retrieval Layer | + | ranking/hybrid | + +------------------+ + | + v + +------------------+ + | Working Memory | + | active context | + +------------------+ + | + v + LLM + | + v + +------------------+ + | Reflection Layer | + | summarization | + | consolidation | + +------------------+ diff --git a/example/ai-agent-app/memory.py b/example/ai-agent-app/memory.py new file mode 100644 index 00000000..6a337504 --- /dev/null +++ b/example/ai-agent-app/memory.py @@ -0,0 +1,177 @@ +# Agent Memory System + +## Goals + +Build a production-grade memory system for AI agents that supports: + +- semantic memory +- episodic memory +- task memory +- retrieval ranking +- memory consolidation +- reflection +- decay/forgetting + +--- + +# Architecture + +## Components + +### 1. Memory Router + +Responsible for: +- classifying memory +- routing to correct store +- embedding generation +- deduplication + +--- + +### 2. Semantic Memory + +Stores: +- user preferences +- knowledge +- facts +- long-term summaries + +Storage: +- PostgreSQL +- pgvector + +Features: +- embedding search +- hybrid retrieval +- deduplication +- confidence scoring + +--- + +### 3. Episodic Memory + +Stores: +- actions +- events +- failures +- tool outputs +- timelines + +Storage: +- append-only PostgreSQL table + +Features: +- chronological retrieval +- summarization +- temporal filtering + +--- + +### 4. Task Memory + +Stores: +- objectives +- todos +- completed steps +- agent progress + +Storage: +- structured relational tables + +Features: +- task lifecycle +- dependencies +- state transitions + +--- + +### 5. Retrieval Layer + +Supports: +- semantic search +- keyword search +- recency ranking +- importance ranking + +Retrieval score formula: + +score = + similarity + + recency + + importance + + task relevance + + confidence + +--- + +### 6. Reflection Engine + +Responsible for: +- summarization +- extracting patterns +- converting episodes into semantic knowledge + +Example: +Repeated deployment failures +→ deployment validation checklist + +--- + +### 7. Memory Decay + +Supports: +- TTL +- archival +- confidence decay +- stale memory cleanup + +--- + +# Database Tables + +## semantic_memories + +- id +- content +- embedding +- confidence +- created_at +- updated_at + +## episodic_events + +- id +- event_type +- content +- outcome +- timestamp + +## tasks + +- id +- title +- status +- created_at +- updated_at + +## task_steps + +- id +- task_id +- step +- status + +--- + +# Fluent API + +```python +memory.capture(...) + +memory.search(...) + +memory.semantic.add(...) + +memory.tasks.create(...) + +memory.reflect() diff --git a/example/ai-agent-app/package-lock.json b/example/ai-agent-app/package-lock.json new file mode 100644 index 00000000..25254f2d --- /dev/null +++ b/example/ai-agent-app/package-lock.json @@ -0,0 +1,1754 @@ +{ + "name": "ai-agent-app", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-agent-app", + "version": "0.1.0", + "dependencies": { + "@inertiajs/react": "^3.0.0", + "fastapi-vite-plugin": "^0.0.3", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@inertiajs/vite": "^3.0.0", + "@tailwindcss/vite": "^4.2.4", + "@types/node": "^25.6.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "concurrently": "^9.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^6.0.3", + "vite": "^8.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@inertiajs/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-3.2.0.tgz", + "integrity": "sha512-9HXCyI8GjwN/KK3KSYZifuncZPc3jioDe/jDQVFZEJJEn89lhaE5+ope3l6sI+GaLLTs0MSZcOhyizlA5L7lig==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "es-toolkit": "^1.33.0", + "laravel-precognition": "^2.0.0" + }, + "peerDependencies": { + "axios": "^1.15.2" + }, + "peerDependenciesMeta": { + "axios": { + "optional": true + } + } + }, + "node_modules/@inertiajs/react": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@inertiajs/react/-/react-3.2.0.tgz", + "integrity": "sha512-Mg2d3Rw0/Cdmc94ZUS2ivWY0k0Jyjtci5g0em2F0N6treV5kLK15PgH1OYlDx8Xgin0mG3uchh+tQuv/Hh95uQ==", + "license": "MIT", + "dependencies": { + "@inertiajs/core": "3.2.0", + "es-toolkit": "^1.33.0", + "laravel-precognition": "^2.0.0" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/@inertiajs/vite": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@inertiajs/vite/-/vite-3.2.0.tgz", + "integrity": "sha512-J8mVzsZj3igxpQ9ZF7z5aw29uRJ3XrmpuJuYDZ8CzOBgC5k93AY4x0qk1q+CPLWBSlXD6jf1qfFHn9LWwGgawA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inertiajs/core": "3.2.0", + "tinyglobby": "^0.2.15" + }, + "peerDependencies": { + "vite": "^7.0.0 || ^8.0.0" + } + }, + "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/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", + "integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fastapi-vite-plugin": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/fastapi-vite-plugin/-/fastapi-vite-plugin-0.0.3.tgz", + "integrity": "sha512-BzUPUquR5/pHZ36Id7jtudQHyn09r2SuP2EnmjUHXLJWpcZyh0i2w7bWtriwA9wLjgw+ZLmlWgI/Rf8EyYtdOw==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "tinyglobby": "^0.2.12", + "vite-plugin-full-reload": "^1.1.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^8.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/laravel-precognition": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/laravel-precognition/-/laravel-precognition-2.0.0.tgz", + "integrity": "sha512-dmA4HGc9m+TsVNsJs9/XQBI8u6j7coilN+qKkBuhuXQzH3HypwS/c5dFQ4UqUGjBbcxIM7zdk91kM/SRZwIvWQ==", + "license": "MIT", + "dependencies": { + "es-toolkit": "^1.32.0" + }, + "peerDependencies": { + "axios": "^1.4.0" + }, + "peerDependenciesMeta": { + "axios": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/example/ai-agent-app/package.json b/example/ai-agent-app/package.json new file mode 100644 index 00000000..a1e0d953 --- /dev/null +++ b/example/ai-agent-app/package.json @@ -0,0 +1,29 @@ +{ + "name": "ai-agent-app", + "private": true, + "type": "module", + "version": "0.1.0", + "scripts": { + "dev": "concurrently \"npx vite dev\" \"uv run python artisan serve\"", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@inertiajs/react": "^3.0.0", + "fastapi-vite-plugin": "^0.0.3", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@inertiajs/vite": "^3.0.0", + "@tailwindcss/vite": "^4.2.4", + "@types/node": "^25.6.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "concurrently": "^9.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^6.0.3", + "vite": "^8.0.0" + } +} diff --git a/example/ai-agent-app/packages/__init__.py b/example/ai-agent-app/packages/__init__.py new file mode 100644 index 00000000..c3ec158e --- /dev/null +++ b/example/ai-agent-app/packages/__init__.py @@ -0,0 +1,27 @@ +from packages.agent import ( + Agent, + AgentResponse, + AgentSnapshot, + Document, + provider, + model, + max_steps, + max_tokens, + timeout, + top_p, + memory, +) + +__all__ = [ + "Agent", + "AgentResponse", + "AgentSnapshot", + "Document", + "provider", + "model", + "max_steps", + "max_tokens", + "timeout", + "top_p", + "memory", +] diff --git a/example/ai-agent-app/packages/agent.py b/example/ai-agent-app/packages/agent.py new file mode 100644 index 00000000..af6b013c --- /dev/null +++ b/example/ai-agent-app/packages/agent.py @@ -0,0 +1,765 @@ +""" +Agent wrapper — a Laravel-inspired declarative API for building AI agents. + +Uses the Anthropic and OpenAI SDKs directly as provider backends. +The public API (Agent, AgentResponse, decorators) is provider-agnostic. + +Usage:: + + from packages.agent import Agent, AgentResponse, provider, model, max_tokens + + @provider("anthropic") + @model("claude-sonnet-4-6") + @max_tokens(2048) + class SalesAgent(Agent): + def messages(self): + return [{"role": "system", "content": "You are a helpful sales assistant."}] + + def tools(self): + return [lookup_crm, send_email] + + def provider_options(self): + return { + "anthropic": {"thinking": {"type": "enabled", "budget_tokens": 1024}}, + } + + agent = SalesAgent() + response = agent.prompt("Summarize this lead...") + for chunk in agent.stream("Write a follow-up email..."): + print(chunk, end="", flush=True) +""" + +from __future__ import annotations + +import fnmatch +import json +import os +from dataclasses import dataclass, field +from typing import Any, Callable, Iterator, Optional, Type + + +# ─── Decorators ──────────────────────────────────────────────────────────────── + + +def provider(name: str): + """Set the LLM provider: 'anthropic', 'openai', 'google', etc.""" + def decorator(cls): + cls._provider = name + return cls + return decorator + + +def model(name: str = ""): + """Set the model identifier (e.g. 'claude-sonnet-4-6', 'gpt-4o').""" + def decorator(cls): + cls._model = name + return cls + return decorator + + +def max_steps(n: int = 10): + """Maximum agentic loop iterations before stopping.""" + def decorator(cls): + cls._max_steps = n + return cls + return decorator + + +def max_tokens(n: int = 4096): + """Maximum output tokens per response.""" + def decorator(cls): + cls._max_tokens = n + return cls + return decorator + + +def timeout(seconds: float = 30.0): + """Request timeout in seconds.""" + def decorator(cls): + cls._timeout = seconds + return cls + return decorator + + +def top_p(value: float = 1.0): + """Top-p nucleus sampling parameter.""" + def decorator(cls): + cls._top_p = value + return cls + return decorator + + +def memory(backend: str = ""): + """Attach a named memory backend to this agent.""" + def decorator(cls): + cls._memory_backend = backend + return cls + return decorator + + +# ─── Response Objects ────────────────────────────────────────────────────────── + + +@dataclass +class AgentResponse: + """Returned by Agent.prompt(). Wraps the LLM response.""" + content: str = "" + tool_calls: list[dict] = field(default_factory=list) + usage: dict = field(default_factory=dict) + raw: Any = None + + def text(self) -> str: + """Return the text content.""" + return self.content + + def json(self) -> Any: + """Parse the content as JSON.""" + return json.loads(self.content) + + def __str__(self) -> str: + return self.content + + def __bool__(self) -> bool: + return bool(self.content) + + +@dataclass +class AgentSnapshot: + """ + Record-and-replay snapshot for testing. + + - If the file at ``path`` **does not exist**: the agent calls the real API, + saves the response as JSON, then returns it. + - If the file **exists**: the saved response is loaded and returned without + hitting the API. + + This lets tests run against real responses on first run and be fully + offline and deterministic on every subsequent run. + + Example:: + + agent.fake({"*analyze*": AgentSnapshot(path="tests/fixtures/analysis.json")}) + """ + path: str + + def exists(self) -> bool: + """Return True if the snapshot file is already recorded.""" + return os.path.exists(self.path) + + def load(self) -> AgentResponse: + """Load the recorded response from disk.""" + with open(self.path) as f: + data = json.load(f) + return AgentResponse( + content=data.get("content", ""), + tool_calls=data.get("tool_calls", []), + usage=data.get("usage", {}), + ) + + def save(self, response: AgentResponse) -> None: + """Persist a real API response to disk for future replays.""" + os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True) + with open(self.path, "w") as f: + json.dump( + { + "content": response.content, + "tool_calls": response.tool_calls, + "usage": response.usage, + }, + f, + indent=2, + ) + + def resolve(self, agent: "Agent", message: str, **run_kwargs: Any) -> AgentResponse: + """ + Return the response — from disk if recorded, or from the real API + (which is then saved for future runs). + """ + if self.exists(): + return self.load() + response = agent._run(message, **run_kwargs) + self.save(response) + return response + + +# ─── Attachment Helper ───────────────────────────────────────────────────────── + + +class Document: + """Attach documents to agent.prompt() calls.""" + + def __init__(self, content: str, name: str = "", media_type: str = "text/plain"): + self.content = content + self.name = name + self.media_type = media_type + + @classmethod + def from_path(cls, path: str) -> "Document": + """Load a document from a local file path.""" + with open(path) as f: + content = f.read() + return cls(content=content, name=path) + + @classmethod + def from_storage(cls, key: str) -> "Document": + """Load a document from application storage (storage/).""" + return cls.from_path(f"storage/{key}") + + def to_anthropic_block(self) -> dict: + """Return an Anthropic-compatible content block for this document.""" + return { + "type": "document", + "source": {"type": "text", "media_type": self.media_type, "data": self.content}, + "title": self.name, + } + + +# ─── Agent Base Class ────────────────────────────────────────────────────────── + + +class Agent: + """ + Base class for all agents. Subclass this and override lifecycle methods. + + Class-level configuration (set via decorators or subclass attributes):: + + _provider = "anthropic" # LLM provider + _model = "" # model ID (empty = provider default) + _max_steps = 10 # max agentic loop iterations + _max_tokens = 4096 # max output tokens + _timeout = 30.0 # request timeout in seconds + _top_p = 1.0 # top-p nucleus sampling + _memory_backend = "" # memory backend name (reserved) + """ + + _provider: str = "anthropic" + _model: str = "" + _max_steps: int = 10 + _max_tokens: int = 4096 + _timeout: float = 30.0 + _top_p: float = 1.0 + _memory_backend: str = "" + + # Provider → default model + _DEFAULT_MODELS: dict[str, str] = { + "anthropic": "claude-sonnet-4-6", + "openai": "gpt-4o", + "google": "gemini-2.0-flash", + } + + def __init__(self): + self._fakes: dict[str, AgentResponse | AgentSnapshot] = {} + self._call_log: list[dict] = [] + + # ── Lifecycle — override in subclasses ────────────────────────────────── + + def messages(self) -> list[dict]: + """ + Return initial messages / few-shot examples. + Use role 'system' for the system prompt. + + Example:: + + def messages(self): + return [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "What is 2+2?"}, + {"role": "assistant", "content": "4"}, + ] + """ + return [] + + def schema(self) -> Optional[Type]: + """ + Return a Pydantic model class for structured output, or None for plain text. + + The agent forces the LLM to return a valid instance of this model. + Access it via response.json() or response.content (raw JSON string). + + Example:: + + from pydantic import BaseModel + + class LeadSummary(BaseModel): + name: str + score: int + + def schema(self): + return LeadSummary + """ + return None + + def tools(self) -> list[Callable]: + """ + Return a list of callable tools the agent may invoke. + + Each tool should have a docstring and type-annotated parameters. + LangChain's StructuredTool.from_function() builds the schema automatically. + + Example:: + + def tools(self): + return [search_web, lookup_crm, send_email] + """ + return [] + + def middleware(self) -> list[Callable]: + """ + Return middleware callables that wrap each LLM request. + Each middleware receives (message, next) and must call next(message). + + Example:: + + def rate_limit(message, next): + time.sleep(0.5) + return next(message) + + def middleware(self): + return [rate_limit] + """ + return [] + + def provider_options(self) -> dict: + """ + Return provider-specific options keyed by provider name. + Passed as model_kwargs to the LangChain chat model constructor. + + Example:: + + def provider_options(self): + return { + "anthropic": { + "thinking": {"type": "enabled", "budget_tokens": 1024}, + }, + "openai": { + "frequency_penalty": 0.5, + }, + } + """ + return {} + + def before(self, message: str) -> str: + """Called before the message is sent. Return the (possibly modified) message.""" + return message + + def after(self, response: AgentResponse) -> AgentResponse: + """Called after the LLM responds. Return the (possibly modified) response.""" + return response + + # ── Public API ────────────────────────────────────────────────────────── + + def prompt( + self, + message: str, + *, + system: str | None = None, + model: str | None = None, + messages: list[dict] | None = None, + attachments: list[Document] | None = None, + provider_options: dict | None = None, + ) -> AgentResponse: + """ + Send a prompt and return an AgentResponse. + + Runs the full agentic loop: if the model calls tools, they are + executed and results fed back until the model stops or _max_steps + is reached. + + Args: + message: The user message. + system: Override the system prompt for this call only. + model: Override the model for this call only. + messages: Extra conversation history to prepend. + attachments: Documents to include with the message. + provider_options: Per-provider options merged for this call only. + """ + message = self.before(message) + + _run_kwargs = dict( + system=system, + model=model, + extra_messages=messages, + attachments=attachments, + provider_options=provider_options, + ) + + match = self._match_fake(message) + if match is not None: + if isinstance(match, AgentSnapshot): + response = match.resolve(self, message, **_run_kwargs) + else: + response = match + self._log_call("prompt", message) + return self.after(response) + + def _call(msg: str) -> AgentResponse: + return self._run(msg, **_run_kwargs) + + response = self._apply_middleware(message, _call) + self._log_call("prompt", message) + return self.after(response) + + def stream( + self, + message: str, + *, + system: str | None = None, + model: str | None = None, + provider_options: dict | None = None, + ) -> Iterator[str]: + """ + Stream a response token by token. + + Note: tool execution is not supported during streaming. + Use prompt() for agentic tool loops. + + Example:: + + for chunk in agent.stream("Write a report..."): + print(chunk, end="", flush=True) + """ + message = self.before(message) + self._log_call("stream", message) + yield from self._stream(message, system=system, model=model, provider_options=provider_options) + + def fake(self, patterns: dict[str, AgentResponse | AgentSnapshot]) -> "Agent": + """ + Register fake responses for testing. Keys are glob patterns matched + against the prompt text (case-insensitive glob). + + - ``AgentResponse`` — always returned as-is (fully offline). + - ``AgentSnapshot`` — replayed from disk if the file exists; otherwise + the real API is called, the response is saved, and replayed next time. + + Example:: + + agent.fake({ + "*hello*": AgentResponse(content="Hello!"), + "*analyze*": AgentSnapshot(path="tests/fixtures/analysis.json"), + }) + """ + for pattern, value in patterns.items(): + self._fakes[pattern] = value # snapshots are stored un-resolved + return self + + def assert_prompted(self, times: int | None = None) -> None: + """ + Assert that prompt() or stream() was called. + + Args: + times: If given, assert exactly this many calls. Otherwise assert >= 1. + """ + calls = [c for c in self._call_log if c["method"] in ("prompt", "stream")] + if times is not None: + assert len(calls) == times, f"Expected {times} prompt call(s), got {len(calls)}" + else: + assert len(calls) > 0, "Expected at least one prompt() or stream() call, but none were made" + + def assert_not_prompted(self) -> None: + """Assert that prompt() and stream() were never called.""" + self.assert_prompted(times=0) + + def reset(self) -> "Agent": + """Clear fakes and call log. Useful between test cases.""" + self._fakes.clear() + self._call_log.clear() + return self + + # ── Internal helpers ──────────────────────────────────────────────────── + + def _match_fake(self, message: str) -> Optional[AgentResponse | AgentSnapshot]: + for pattern, value in self._fakes.items(): + if fnmatch.fnmatch(message.lower(), pattern.lower()): + return value + return None + + def _log_call(self, method: str, message: str) -> None: + self._call_log.append({"method": method, "message": message}) + + def _apply_middleware( + self, message: str, final: Callable[[str], AgentResponse] + ) -> AgentResponse: + """Build a left-to-right middleware chain and invoke it.""" + chain = list(self.middleware()) + + def build(mw_list: list, fn: Callable) -> Callable: + if not mw_list: + return fn + head, *tail = mw_list + next_fn = build(tail, fn) + return lambda msg: head(msg, next_fn) + + return build(chain, final)(message) + + def _execute_tool(self, name: str, inputs: dict) -> Any: + """Find a tool by function name and call it with the given inputs.""" + for tool in self.tools(): + if callable(tool) and tool.__name__ == name: + return tool(**inputs) + raise ValueError(f"Tool {name!r} not found") + + def _resolve_model(self, override: str | None = None) -> str: + if override: + return override + if self._model: + return self._model + return self._DEFAULT_MODELS.get(self._provider, "") + + def _get_provider_options(self, override: dict | None = None) -> dict: + options = dict(self.provider_options().get(self._provider, {})) + if override: + provider_specific = override.get(self._provider, override) + if isinstance(provider_specific, dict): + options.update(provider_specific) + return options + + def _build_messages( + self, + message: str, + system: str | None = None, + extra_messages: list[dict] | None = None, + attachments: list[Document] | None = None, + ) -> tuple[str | None, list[dict]]: + base = self.messages() + + resolved_system = system + if resolved_system is None: + sys_entries = [m for m in base if m.get("role") == "system"] + if sys_entries: + resolved_system = sys_entries[0]["content"] + + history = [m for m in base if m.get("role") != "system"] + if extra_messages: + history.extend(extra_messages) + + if attachments: + content: Any = [{"type": "text", "text": message}] + for doc in attachments: + content.append(doc.to_anthropic_block()) + history.append({"role": "user", "content": content}) + else: + history.append({"role": "user", "content": message}) + + return resolved_system, history + + def _tools_schema(self) -> list[dict]: + """Convert tools() callables to Anthropic-style tool definitions.""" + import inspect + result = [] + for tool in self.tools(): + if not callable(tool): + continue + sig = inspect.signature(tool) + try: + import typing + hints = typing.get_type_hints(tool) + except Exception: + hints = {} + properties = { + name: {"type": _python_type_to_json(hints.get(name, str))} + for name in sig.parameters + } + result.append({ + "name": tool.__name__, + "description": inspect.getdoc(tool) or tool.__name__, + "input_schema": { + "type": "object", + "properties": properties, + "required": [ + n for n, p in sig.parameters.items() + if p.default is inspect.Parameter.empty + ], + }, + }) + return result + + # ── Provider dispatch ─────────────────────────────────────────────────── + + def _run( + self, + message: str, + system: str | None, + model: str | None, + extra_messages: list[dict] | None, + attachments: list[Document] | None, + provider_options: dict | None, + ) -> AgentResponse: + resolved_system, messages = self._build_messages(message, system, extra_messages, attachments) + resolved_model = self._resolve_model(model) + options = self._get_provider_options(provider_options) + + if self._provider == "anthropic": + return self._run_anthropic(resolved_system, messages, resolved_model, options) + if self._provider == "openai": + return self._run_openai(resolved_system, messages, resolved_model, options) + raise ValueError(f"Unsupported provider: {self._provider!r}. Use 'anthropic' or 'openai'.") + + def _stream( + self, + message: str, + system: str | None, + model: str | None, + provider_options: dict | None, + ) -> Iterator[str]: + resolved_system, messages = self._build_messages(message, system) + resolved_model = self._resolve_model(model) + options = self._get_provider_options(provider_options) + + if self._provider == "anthropic": + yield from self._stream_anthropic(resolved_system, messages, resolved_model, options) + elif self._provider == "openai": + yield from self._stream_openai(resolved_system, messages, resolved_model, options) + else: + raise ValueError(f"Unsupported provider: {self._provider!r}. Use 'anthropic' or 'openai'.") + + # ── Anthropic ────────────────────────────────────────────────────────── + + def _run_anthropic( + self, + system: str | None, + messages: list[dict], + model: str, + options: dict, + ) -> AgentResponse: + from anthropic import Anthropic + + client = Anthropic() + params: dict[str, Any] = { + "model": model, + "max_tokens": self._max_tokens, + "messages": messages, + **options, + } + if system: + params["system"] = system + tools_schema = self._tools_schema() + if tools_schema: + params["tools"] = tools_schema + + # Agentic tool loop + for _step in range(self._max_steps): + resp = client.messages.create(**params) + + if resp.stop_reason != "tool_use": + break + + # Collect tool calls and execute them + tool_uses = [b for b in resp.content if b.type == "tool_use"] + tool_results = [] + for tu in tool_uses: + try: + result = self._execute_tool(tu.name, tu.input) + except Exception as exc: + result = f"Error: {exc}" + tool_results.append({ + "type": "tool_result", + "tool_use_id": tu.id, + "content": str(result), + }) + + # Feed results back into the conversation + params["messages"] = [ + *params["messages"], + {"role": "assistant", "content": resp.content}, + {"role": "user", "content": tool_results}, + ] + + content = "".join( + b.text for b in resp.content if hasattr(b, "text") + ) + tool_calls = [ + {"name": b.name, "input": b.input} + for b in resp.content if b.type == "tool_use" + ] + return AgentResponse( + content=content, + tool_calls=tool_calls, + usage={"input": resp.usage.input_tokens, "output": resp.usage.output_tokens}, + raw=resp, + ) + + def _stream_anthropic( + self, + system: str | None, + messages: list[dict], + model: str, + options: dict, + ) -> Iterator[str]: + from anthropic import Anthropic + + client = Anthropic() + params: dict[str, Any] = { + "model": model, + "max_tokens": self._max_tokens, + "messages": messages, + **options, + } + if system: + params["system"] = system + + with client.messages.stream(**params) as stream: + for text in stream.text_stream: + yield text + + # ── OpenAI ───────────────────────────────────────────────────────────── + + def _run_openai( + self, + system: str | None, + messages: list[dict], + model: str, + options: dict, + ) -> AgentResponse: + from openai import OpenAI + + client = OpenAI() + all_messages: list[dict] = [] + if system: + all_messages.append({"role": "system", "content": system}) + all_messages.extend(messages) + + params: dict[str, Any] = { + "model": model, + "max_tokens": self._max_tokens, + "messages": all_messages, + **options, + } + + resp = client.chat.completions.create(**params) + content = resp.choices[0].message.content or "" + return AgentResponse( + content=content, + usage={ + "input": resp.usage.prompt_tokens if resp.usage else 0, + "output": resp.usage.completion_tokens if resp.usage else 0, + }, + raw=resp, + ) + + def _stream_openai( + self, + system: str | None, + messages: list[dict], + model: str, + options: dict, + ) -> Iterator[str]: + from openai import OpenAI + + client = OpenAI() + all_messages: list[dict] = [] + if system: + all_messages.append({"role": "system", "content": system}) + all_messages.extend(messages) + + params: dict[str, Any] = { + "model": model, + "max_tokens": self._max_tokens, + "messages": all_messages, + "stream": True, + **options, + } + + for chunk in client.chat.completions.create(**params): + delta = chunk.choices[0].delta.content + if delta: + yield delta diff --git a/example/ai-agent-app/providers/__init__.py b/example/ai-agent-app/providers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/example/ai-agent-app/providers/fastapi_provider.py b/example/ai-agent-app/providers/fastapi_provider.py new file mode 100644 index 00000000..cfc9542e --- /dev/null +++ b/example/ai-agent-app/providers/fastapi_provider.py @@ -0,0 +1,23 @@ +from pathlib import Path + +from fastapi_startkit.fastapi import FastAPIProvider as BaseFastAPIProvider +from starlette.templating import Jinja2Templates + + +class FastAPIProvider(BaseFastAPIProvider): + def register(self) -> None: + super().register() + + templates_dir = Path(self.app.base_path) / "templates" + templates = Jinja2Templates(directory=str(templates_dir)) + self.app.bind("templates", templates) + + def boot(self) -> None: + super().boot() + + inertia = self.app.make("inertia") + inertia.share("app_name", "AI Agent App") + inertia.version("1.0.0") + + from routes.web import router + self.app.include_router(router) diff --git a/example/ai-agent-app/public/.gitkeep b/example/ai-agent-app/public/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/example/ai-agent-app/public/hot b/example/ai-agent-app/public/hot new file mode 100644 index 00000000..f762bcfc --- /dev/null +++ b/example/ai-agent-app/public/hot @@ -0,0 +1 @@ +http://[::1]:5173 \ No newline at end of file diff --git a/example/ai-agent-app/pyproject.toml b/example/ai-agent-app/pyproject.toml new file mode 100644 index 00000000..b3af7491 --- /dev/null +++ b/example/ai-agent-app/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "ai-agent-app" +version = "0.1.0" +description = "AI Agent app built with FastAPI Startkit + Inertia.js + React" +requires-python = ">=3.12" +dependencies = [ + "anthropic>=0.104.1", + "fastapi-startkit[fastapi]", + "jinja2>=3.1", + "python-multipart>=0.0.9", +] + +[tool.uv.sources] +fastapi-startkit = { path = "../../fastapi_startkit", editable = true } + +[dependency-groups] +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.24", + "httpx>=0.27", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] diff --git a/example/ai-agent-app/resources/css/app.css b/example/ai-agent-app/resources/css/app.css new file mode 100644 index 00000000..f1d8c73c --- /dev/null +++ b/example/ai-agent-app/resources/css/app.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/example/ai-agent-app/resources/js/Pages/Chat/Index.tsx b/example/ai-agent-app/resources/js/Pages/Chat/Index.tsx new file mode 100644 index 00000000..5e202198 --- /dev/null +++ b/example/ai-agent-app/resources/js/Pages/Chat/Index.tsx @@ -0,0 +1,141 @@ +import { useState, useRef, useEffect } from "react" + +interface Message { + role: "user" | "assistant" + content: string +} + +export default function Index() { + const [messages, setMessages] = useState([]) + const [input, setInput] = useState("") + const [loading, setLoading] = useState(false) + const bottomRef = useRef(null) + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }) + }, [messages, loading]) + + async function send() { + const text = input.trim() + if (!text || loading) return + + setMessages(prev => [...prev, { role: "user", content: text }]) + setInput("") + setLoading(true) + + try { + const res = await fetch("/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message: text }), + }) + const data = await res.json() + setMessages(prev => [...prev, { role: "assistant", content: data.reply }]) + } catch { + setMessages(prev => [...prev, { role: "assistant", content: "Something went wrong. Please try again." }]) + } finally { + setLoading(false) + } + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + send() + } + } + + return ( +
+ {/* Header */} +
+
+ + + +
+
+

AI Assistant

+

Powered by Claude

+
+
+ + {/* Messages */} +
+ {messages.length === 0 && ( +
+
+ + + +
+

Send a message to start the conversation.

+
+ )} + + {messages.map((msg, i) => ( +
+ {/* Avatar */} +
+ {msg.role === "user" ? "U" : "AI"} +
+ + {/* Bubble */} +
+ {msg.content} +
+
+ ))} + + {/* Typing indicator */} + {loading && ( +
+
+ AI +
+
+ + + +
+
+ )} + +
+
+ + {/* Input */} +
+
+