From f6fb7f61bf506d9b9270999216af87ff947b443c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:00:21 +0000 Subject: [PATCH 1/9] Initial plan From c8a6632b3b816d0416d30ba180ca5b608885d30a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:18:07 +0000 Subject: [PATCH 2/9] Add 8 comprehensive guide files for dhamps-vdb features Co-authored-by: awagner-mainz <2088443+awagner-mainz@users.noreply.github.com> --- .gitmodules | 3 + docs/.hugo_build.lock | 0 docs/archetypes/default.md | 5 + docs/config.toml | 9 + docs/content/_index.md | 54 + docs/content/api/_index.md | 39 + docs/content/api/endpoints/_index.md | 41 + docs/content/concepts/_index.md | 24 + docs/content/concepts/architecture.md | 341 +++ docs/content/concepts/embeddings.md | 466 +++ docs/content/concepts/llm-services.md | 428 +++ docs/content/concepts/metadata.md | 552 ++++ docs/content/concepts/projects.md | 439 +++ docs/content/concepts/similarity-search.md | 445 +++ docs/content/concepts/users-and-auth.md | 368 +++ docs/content/deployment/_index.md | 35 + docs/content/development/_index.md | 47 + docs/content/getting-started/_index.md | 31 + docs/content/getting-started/configuration.md | 156 + docs/content/getting-started/docker.md | 537 ++++ docs/content/getting-started/first-project.md | 415 +++ docs/content/getting-started/installation.md | 129 + docs/content/getting-started/quick-start.md | 292 ++ docs/content/guides/_index.md | 23 + docs/content/guides/batch-operations.md | 718 +++++ docs/content/guides/instance-management.md | 634 ++++ docs/content/guides/metadata-filtering.md | 468 +++ docs/content/guides/metadata-validation.md | 665 +++++ docs/content/guides/ownership-transfer.md | 406 +++ docs/content/guides/project-sharing.md | 333 +++ docs/content/guides/public-projects.md | 420 +++ docs/content/guides/rag-workflow.md | 393 +++ docs/content/reference/_index.md | 20 + docs/public/404.html | 4 + docs/public/asciinema/asciinema-auto.js | 7 + docs/public/asciinema/asciinema-player.css | 2389 +++++++++++++++ docs/public/asciinema/asciinema-player.min.js | 1 + ...87344917b325208841feca0968fe450f570575.css | 1 + docs/public/categories/index.html | 3 + docs/public/categories/index.xml | 1 + ...ecbe5ed12ab4d8e11ba873c2f11161202b945.json | 1 + ...4c0460e5894c72e7286c511967078b24f321f5e.js | 1 + docs/public/favicon.png | Bin 0 -> 298 bytes docs/public/favicon.svg | 1 + docs/public/fuse.min.js | 9 + docs/public/icons/menu.svg | 1 + docs/public/index.html | 3 + docs/public/index.xml | 1 + docs/public/katex/auto-render.min.js | 1 + docs/public/katex/fonts/KaTeX_AMS-Regular.ttf | Bin 0 -> 63632 bytes .../public/katex/fonts/KaTeX_AMS-Regular.woff | Bin 0 -> 33516 bytes .../katex/fonts/KaTeX_AMS-Regular.woff2 | Bin 0 -> 28076 bytes .../katex/fonts/KaTeX_Caligraphic-Bold.ttf | Bin 0 -> 12368 bytes .../katex/fonts/KaTeX_Caligraphic-Bold.woff | Bin 0 -> 7716 bytes .../katex/fonts/KaTeX_Caligraphic-Bold.woff2 | Bin 0 -> 6912 bytes .../katex/fonts/KaTeX_Caligraphic-Regular.ttf | Bin 0 -> 12344 bytes .../fonts/KaTeX_Caligraphic-Regular.woff | Bin 0 -> 7656 bytes .../fonts/KaTeX_Caligraphic-Regular.woff2 | Bin 0 -> 6908 bytes .../public/katex/fonts/KaTeX_Fraktur-Bold.ttf | Bin 0 -> 19584 bytes .../katex/fonts/KaTeX_Fraktur-Bold.woff | Bin 0 -> 13296 bytes .../katex/fonts/KaTeX_Fraktur-Bold.woff2 | Bin 0 -> 11348 bytes .../katex/fonts/KaTeX_Fraktur-Regular.ttf | Bin 0 -> 19572 bytes .../katex/fonts/KaTeX_Fraktur-Regular.woff | Bin 0 -> 13208 bytes .../katex/fonts/KaTeX_Fraktur-Regular.woff2 | Bin 0 -> 11316 bytes docs/public/katex/fonts/KaTeX_Main-Bold.ttf | Bin 0 -> 51336 bytes docs/public/katex/fonts/KaTeX_Main-Bold.woff | Bin 0 -> 29912 bytes docs/public/katex/fonts/KaTeX_Main-Bold.woff2 | Bin 0 -> 25324 bytes .../katex/fonts/KaTeX_Main-BoldItalic.ttf | Bin 0 -> 32968 bytes .../katex/fonts/KaTeX_Main-BoldItalic.woff | Bin 0 -> 19412 bytes .../katex/fonts/KaTeX_Main-BoldItalic.woff2 | Bin 0 -> 16780 bytes docs/public/katex/fonts/KaTeX_Main-Italic.ttf | Bin 0 -> 33580 bytes .../public/katex/fonts/KaTeX_Main-Italic.woff | Bin 0 -> 19676 bytes .../katex/fonts/KaTeX_Main-Italic.woff2 | Bin 0 -> 16988 bytes .../public/katex/fonts/KaTeX_Main-Regular.ttf | Bin 0 -> 53580 bytes .../katex/fonts/KaTeX_Main-Regular.woff | Bin 0 -> 30772 bytes .../katex/fonts/KaTeX_Main-Regular.woff2 | Bin 0 -> 26272 bytes .../katex/fonts/KaTeX_Math-BoldItalic.ttf | Bin 0 -> 31196 bytes .../katex/fonts/KaTeX_Math-BoldItalic.woff | Bin 0 -> 18668 bytes .../katex/fonts/KaTeX_Math-BoldItalic.woff2 | Bin 0 -> 16400 bytes docs/public/katex/fonts/KaTeX_Math-Italic.ttf | Bin 0 -> 31308 bytes .../public/katex/fonts/KaTeX_Math-Italic.woff | Bin 0 -> 18748 bytes .../katex/fonts/KaTeX_Math-Italic.woff2 | Bin 0 -> 16440 bytes .../katex/fonts/KaTeX_SansSerif-Bold.ttf | Bin 0 -> 24504 bytes .../katex/fonts/KaTeX_SansSerif-Bold.woff | Bin 0 -> 14408 bytes .../katex/fonts/KaTeX_SansSerif-Bold.woff2 | Bin 0 -> 12216 bytes .../katex/fonts/KaTeX_SansSerif-Italic.ttf | Bin 0 -> 22364 bytes .../katex/fonts/KaTeX_SansSerif-Italic.woff | Bin 0 -> 14112 bytes .../katex/fonts/KaTeX_SansSerif-Italic.woff2 | Bin 0 -> 12028 bytes .../katex/fonts/KaTeX_SansSerif-Regular.ttf | Bin 0 -> 19436 bytes .../katex/fonts/KaTeX_SansSerif-Regular.woff | Bin 0 -> 12316 bytes .../katex/fonts/KaTeX_SansSerif-Regular.woff2 | Bin 0 -> 10344 bytes .../katex/fonts/KaTeX_Script-Regular.ttf | Bin 0 -> 16648 bytes .../katex/fonts/KaTeX_Script-Regular.woff | Bin 0 -> 10588 bytes .../katex/fonts/KaTeX_Script-Regular.woff2 | Bin 0 -> 9644 bytes .../katex/fonts/KaTeX_Size1-Regular.ttf | Bin 0 -> 12228 bytes .../katex/fonts/KaTeX_Size1-Regular.woff | Bin 0 -> 6496 bytes .../katex/fonts/KaTeX_Size1-Regular.woff2 | Bin 0 -> 5468 bytes .../katex/fonts/KaTeX_Size2-Regular.ttf | Bin 0 -> 11508 bytes .../katex/fonts/KaTeX_Size2-Regular.woff | Bin 0 -> 6188 bytes .../katex/fonts/KaTeX_Size2-Regular.woff2 | Bin 0 -> 5208 bytes .../katex/fonts/KaTeX_Size3-Regular.ttf | Bin 0 -> 7588 bytes .../katex/fonts/KaTeX_Size3-Regular.woff | Bin 0 -> 4420 bytes .../katex/fonts/KaTeX_Size3-Regular.woff2 | Bin 0 -> 3624 bytes .../katex/fonts/KaTeX_Size4-Regular.ttf | Bin 0 -> 10364 bytes .../katex/fonts/KaTeX_Size4-Regular.woff | Bin 0 -> 5980 bytes .../katex/fonts/KaTeX_Size4-Regular.woff2 | Bin 0 -> 4928 bytes .../katex/fonts/KaTeX_Typewriter-Regular.ttf | Bin 0 -> 27556 bytes .../katex/fonts/KaTeX_Typewriter-Regular.woff | Bin 0 -> 16028 bytes .../fonts/KaTeX_Typewriter-Regular.woff2 | Bin 0 -> 13568 bytes docs/public/katex/katex.min.css | 1 + docs/public/katex/katex.min.js | 1 + docs/public/manifest.json | 15 + docs/public/mermaid.min.js | 2659 +++++++++++++++++ docs/public/sitemap.xml | 1 + docs/public/tags/index.html | 3 + docs/public/tags/index.xml | 1 + ...s_b807c86e8030af4cdc30edccea379f5f.content | 1 + ...scss_b807c86e8030af4cdc30edccea379f5f.json | 1 + docs/themes/book | 1 + 119 files changed, 14044 insertions(+) create mode 100644 .gitmodules create mode 100644 docs/.hugo_build.lock create mode 100644 docs/archetypes/default.md create mode 100644 docs/config.toml create mode 100644 docs/content/_index.md create mode 100644 docs/content/api/_index.md create mode 100644 docs/content/api/endpoints/_index.md create mode 100644 docs/content/concepts/_index.md create mode 100644 docs/content/concepts/architecture.md create mode 100644 docs/content/concepts/embeddings.md create mode 100644 docs/content/concepts/llm-services.md create mode 100644 docs/content/concepts/metadata.md create mode 100644 docs/content/concepts/projects.md create mode 100644 docs/content/concepts/similarity-search.md create mode 100644 docs/content/concepts/users-and-auth.md create mode 100644 docs/content/deployment/_index.md create mode 100644 docs/content/development/_index.md create mode 100644 docs/content/getting-started/_index.md create mode 100644 docs/content/getting-started/configuration.md create mode 100644 docs/content/getting-started/docker.md create mode 100644 docs/content/getting-started/first-project.md create mode 100644 docs/content/getting-started/installation.md create mode 100644 docs/content/getting-started/quick-start.md create mode 100644 docs/content/guides/_index.md create mode 100644 docs/content/guides/batch-operations.md create mode 100644 docs/content/guides/instance-management.md create mode 100644 docs/content/guides/metadata-filtering.md create mode 100644 docs/content/guides/metadata-validation.md create mode 100644 docs/content/guides/ownership-transfer.md create mode 100644 docs/content/guides/project-sharing.md create mode 100644 docs/content/guides/public-projects.md create mode 100644 docs/content/guides/rag-workflow.md create mode 100644 docs/content/reference/_index.md create mode 100644 docs/public/404.html create mode 100644 docs/public/asciinema/asciinema-auto.js create mode 100644 docs/public/asciinema/asciinema-player.css create mode 100644 docs/public/asciinema/asciinema-player.min.js create mode 100644 docs/public/book.min.cc2c524ed250aac81b23d1f4af87344917b325208841feca0968fe450f570575.css create mode 100644 docs/public/categories/index.html create mode 100644 docs/public/categories/index.xml create mode 100644 docs/public/en.search-data.min.4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945.json create mode 100644 docs/public/en.search.min.e5f99ffd1cb4862bdf1bad2554c0460e5894c72e7286c511967078b24f321f5e.js create mode 100644 docs/public/favicon.png create mode 100644 docs/public/favicon.svg create mode 100644 docs/public/fuse.min.js create mode 100644 docs/public/icons/menu.svg create mode 100644 docs/public/index.html create mode 100644 docs/public/index.xml create mode 100644 docs/public/katex/auto-render.min.js create mode 100644 docs/public/katex/fonts/KaTeX_AMS-Regular.ttf create mode 100644 docs/public/katex/fonts/KaTeX_AMS-Regular.woff create mode 100644 docs/public/katex/fonts/KaTeX_AMS-Regular.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Caligraphic-Bold.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Caligraphic-Bold.woff create mode 100644 docs/public/katex/fonts/KaTeX_Caligraphic-Bold.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Caligraphic-Regular.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Caligraphic-Regular.woff create mode 100644 docs/public/katex/fonts/KaTeX_Caligraphic-Regular.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Fraktur-Bold.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Fraktur-Bold.woff create mode 100644 docs/public/katex/fonts/KaTeX_Fraktur-Bold.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Fraktur-Regular.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Fraktur-Regular.woff create mode 100644 docs/public/katex/fonts/KaTeX_Fraktur-Regular.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Main-Bold.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Main-Bold.woff create mode 100644 docs/public/katex/fonts/KaTeX_Main-Bold.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Main-BoldItalic.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Main-BoldItalic.woff create mode 100644 docs/public/katex/fonts/KaTeX_Main-BoldItalic.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Main-Italic.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Main-Italic.woff create mode 100644 docs/public/katex/fonts/KaTeX_Main-Italic.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Main-Regular.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Main-Regular.woff create mode 100644 docs/public/katex/fonts/KaTeX_Main-Regular.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Math-BoldItalic.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Math-BoldItalic.woff create mode 100644 docs/public/katex/fonts/KaTeX_Math-BoldItalic.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Math-Italic.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Math-Italic.woff create mode 100644 docs/public/katex/fonts/KaTeX_Math-Italic.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_SansSerif-Bold.ttf create mode 100644 docs/public/katex/fonts/KaTeX_SansSerif-Bold.woff create mode 100644 docs/public/katex/fonts/KaTeX_SansSerif-Bold.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_SansSerif-Italic.ttf create mode 100644 docs/public/katex/fonts/KaTeX_SansSerif-Italic.woff create mode 100644 docs/public/katex/fonts/KaTeX_SansSerif-Italic.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_SansSerif-Regular.ttf create mode 100644 docs/public/katex/fonts/KaTeX_SansSerif-Regular.woff create mode 100644 docs/public/katex/fonts/KaTeX_SansSerif-Regular.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Script-Regular.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Script-Regular.woff create mode 100644 docs/public/katex/fonts/KaTeX_Script-Regular.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Size1-Regular.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Size1-Regular.woff create mode 100644 docs/public/katex/fonts/KaTeX_Size1-Regular.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Size2-Regular.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Size2-Regular.woff create mode 100644 docs/public/katex/fonts/KaTeX_Size2-Regular.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Size3-Regular.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Size3-Regular.woff create mode 100644 docs/public/katex/fonts/KaTeX_Size3-Regular.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Size4-Regular.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Size4-Regular.woff create mode 100644 docs/public/katex/fonts/KaTeX_Size4-Regular.woff2 create mode 100644 docs/public/katex/fonts/KaTeX_Typewriter-Regular.ttf create mode 100644 docs/public/katex/fonts/KaTeX_Typewriter-Regular.woff create mode 100644 docs/public/katex/fonts/KaTeX_Typewriter-Regular.woff2 create mode 100644 docs/public/katex/katex.min.css create mode 100644 docs/public/katex/katex.min.js create mode 100644 docs/public/manifest.json create mode 100644 docs/public/mermaid.min.js create mode 100644 docs/public/sitemap.xml create mode 100644 docs/public/tags/index.html create mode 100644 docs/public/tags/index.xml create mode 100644 docs/resources/_gen/assets/book.scss_b807c86e8030af4cdc30edccea379f5f.content create mode 100644 docs/resources/_gen/assets/book.scss_b807c86e8030af4cdc30edccea379f5f.json create mode 160000 docs/themes/book diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6cb7e5c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "docs/themes/book"] + path = docs/themes/book + url = https://github.com/alex-shpak/hugo-book diff --git a/docs/.hugo_build.lock b/docs/.hugo_build.lock new file mode 100644 index 0000000..e69de29 diff --git a/docs/archetypes/default.md b/docs/archetypes/default.md new file mode 100644 index 0000000..25b6752 --- /dev/null +++ b/docs/archetypes/default.md @@ -0,0 +1,5 @@ ++++ +date = '{{ .Date }}' +draft = true +title = '{{ replace .File.ContentBaseName "-" " " | title }}' ++++ diff --git a/docs/config.toml b/docs/config.toml new file mode 100644 index 0000000..6b5d75c --- /dev/null +++ b/docs/config.toml @@ -0,0 +1,9 @@ +baseURL = 'https://mpilhlt.github.io/dhamps-vdb/' +languageCode = 'en-us' +title = 'dhamps-vdb Documentation' +theme = 'book' + +[params] + BookRepo = 'https://github.com/mpilhlt/dhamps-vdb' + BookEditPath = 'edit/main' + BookSearch = true diff --git a/docs/content/_index.md b/docs/content/_index.md new file mode 100644 index 0000000..62e336a --- /dev/null +++ b/docs/content/_index.md @@ -0,0 +1,54 @@ +--- +title: dhamps-vdb Documentation +type: docs +--- + +# dhamps-vdb Documentation + +Welcome to the documentation for **dhamps-vdb**, a vector database designed for Digital Humanities applications at the Max Planck Society initiative. + +## What is dhamps-vdb? + +dhamps-vdb is a PostgreSQL-backed vector database with pgvector support, providing a RESTful API for managing embeddings in Retrieval Augmented Generation (RAG) workflows. It offers multi-user support, project management, and flexible embedding configurations. + +## Key Features + +- **Multi-user Support**: Role-based access control (admin, owner, reader, editor) +- **Project Management**: Organize embeddings into projects with sharing capabilities +- **LLM Service Management**: Flexible service definitions and instances with encrypted API keys +- **Metadata Support**: JSON Schema validation and filtering in similarity search +- **PostgreSQL Backend**: Reliable storage with pgvector extension +- **RESTful API**: OpenAPI-documented endpoints +- **Docker Ready**: Easy deployment with Docker Compose + +## Quick Links + +- [Getting Started](/getting-started/) - Installation and first steps +- [Concepts](/concepts/) - Understand how dhamps-vdb works +- [API Reference](/api/) - Complete API documentation +- [Guides](/guides/) - How-to guides for common tasks + +## Getting Help + +- 📖 Browse this documentation +- 🐛 [Report issues](https://github.com/mpilhlt/dhamps-vdb/issues) +- 💬 [GitHub Discussions](https://github.com/mpilhlt/dhamps-vdb/discussions) + +## Quick Example + +```bash +# Start the service with Docker +./docker-setup.sh +docker-compose up -d + +# Create a user +curl -X POST http://localhost:8880/v1/users \ + -H "Authorization: Bearer YOUR_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{"user_handle": "alice", "name": "Alice Smith"}' + +# Create a project and start working with embeddings +# See the Getting Started guide for a complete walkthrough +``` + +Ready to get started? Head over to the [Installation Guide](/getting-started/installation/). diff --git a/docs/content/api/_index.md b/docs/content/api/_index.md new file mode 100644 index 0000000..5930d11 --- /dev/null +++ b/docs/content/api/_index.md @@ -0,0 +1,39 @@ +--- +title: "API Reference" +weight: 4 +--- + +# API Reference + +Complete reference for the dhamps-vdb REST API. + +## API Version + +Current version: **v1** + +All endpoints are prefixed with `/v1/` (e.g., `POST /v1/embeddings/{user}/{project}`). + +## API Documentation + +The complete, always up-to-date API specification is available at: + +- **OpenAPI YAML**: `/openapi.yaml` +- **Interactive Documentation**: `/docs` + +## Reference Sections + +- [Authentication](authentication/) - API key authentication +- [Endpoints](endpoints/) - All available API endpoints +- [Query Parameters](query-parameters/) - Filtering and pagination +- [PATCH Updates](patch-updates/) - Partial resource updates +- [Error Handling](error-handling/) - Error responses and codes + +## Quick Example + +```bash +# Authenticate with API key +curl -X GET "https://api.example.com/v1/projects/alice" \ + -H "Authorization: Bearer your_api_key_here" +``` + +All API requests require authentication except for public project read operations. diff --git a/docs/content/api/endpoints/_index.md b/docs/content/api/endpoints/_index.md new file mode 100644 index 0000000..f7a9d92 --- /dev/null +++ b/docs/content/api/endpoints/_index.md @@ -0,0 +1,41 @@ +--- +title: "Endpoints" +weight: 1 +--- + +# API Endpoints + +Complete reference for all dhamps-vdb API endpoints. + +## Endpoint Categories + +- [Users](users/) - User management +- [Projects](projects/) - Project operations +- [LLM Services](llm-services/) - LLM service instances +- [API Standards](api-standards/) - API standard definitions +- [Embeddings](embeddings/) - Embedding storage and retrieval +- [Similars](similars/) - Similarity search + +## Endpoint Format + +All endpoints follow the pattern: + +``` +{METHOD} /v1/{resource}/{user}/{identifier} +``` + +Where: +- `METHOD`: HTTP method (GET, POST, PUT, DELETE, PATCH) +- `resource`: Resource type (users, projects, embeddings, etc.) +- `user`: User handle (owner of the resource) +- `identifier`: Specific resource identifier + +## Authentication + +Most endpoints require authentication via the `Authorization` header: + +``` +Authorization: Bearer your_api_key_here +``` + +Public projects allow unauthenticated access to read operations. diff --git a/docs/content/concepts/_index.md b/docs/content/concepts/_index.md new file mode 100644 index 0000000..9d49ae0 --- /dev/null +++ b/docs/content/concepts/_index.md @@ -0,0 +1,24 @@ +--- +title: "Concepts" +weight: 2 +--- + +# Core Concepts + +Understanding the key concepts behind dhamps-vdb helps you make the most of its features. This section explains the fundamental building blocks and how they work together. + +## Overview + +dhamps-vdb is a vector database designed for Retrieval Augmented Generation (RAG) workflows. It stores embeddings with metadata and provides fast similarity search capabilities. + +## Key Components + +- **Users** - Individual accounts with authentication +- **Projects** - Containers for embeddings with access control +- **Embeddings** - Vector representations of text with metadata +- **LLM Services** - Configurations for embedding models +- **Similarity Search** - Find similar documents using vector distance + +## Architecture + +dhamps-vdb uses PostgreSQL with the pgvector extension for vector operations. It provides a RESTful API with token-based authentication and supports multi-user environments with project sharing. diff --git a/docs/content/concepts/architecture.md b/docs/content/concepts/architecture.md new file mode 100644 index 0000000..1151663 --- /dev/null +++ b/docs/content/concepts/architecture.md @@ -0,0 +1,341 @@ +--- +title: "Architecture" +weight: 1 +--- + +# Architecture + +dhamps-vdb is a vector database API designed for RAG (Retrieval Augmented Generation) workflows in Digital Humanities research. + +## System Overview + +``` +┌─────────────┐ +│ Client │ +│ Application │ +└──────┬──────┘ + │ HTTP/REST + │ +┌──────▼──────────────────────────┐ +│ dhamps-vdb API Server │ +│ ┌──────────────────────────┐ │ +│ │ Authentication Layer │ │ +│ └────────┬─────────────────┘ │ +│ ┌────────▼─────────────────┐ │ +│ │ Request Handlers │ │ +│ │ (Users, Projects, etc) │ │ +│ └────────┬─────────────────┘ │ +│ ┌────────▼─────────────────┐ │ +│ │ Validation Layer │ │ +│ │ (Dimensions, Metadata) │ │ +│ └────────┬─────────────────┘ │ +│ ┌────────▼─────────────────┐ │ +│ │ SQLC Queries │ │ +│ │ (Type-safe SQL) │ │ +│ └────────┬─────────────────┘ │ +└───────────┼──────────────────────┘ + │ + ┌───────▼──────────────┐ + │ PostgreSQL + 16 │ + │ with pgvector 0.7 │ + │ │ + │ ┌────────────────┐ │ + │ │ Vector Index │ │ + │ │ (HNSW/IVFFlat) │ │ + │ └────────────────┘ │ + └──────────────────────┘ +``` + +## Core Components + +### API Layer + +Built with [Huma](https://huma.rocks/) framework on top of Go's `http.ServeMux`: + +- OpenAPI documentation generation +- Automatic request/response validation +- JSON schema support +- REST endpoint routing + +### Authentication + +Token-based authentication using API keys: + +- **Admin key**: For administrative operations (user creation, system management) +- **User keys**: SHA-256 hashed, unique per user +- **Bearer token**: Transmitted in `Authorization` header + +### Data Storage + +PostgreSQL with pgvector extension: + +- **Vector storage**: Native pgvector support for embeddings +- **Vector search**: Cosine similarity using `<=>` operator +- **ACID compliance**: Transactional consistency +- **Relational integrity**: Foreign keys and constraints + +### Code Generation + +Uses [sqlc](https://sqlc.dev/) for type-safe database queries: + +- SQL queries → Go functions +- Compile-time type checking +- No ORM overhead +- Direct PostgreSQL integration + +## Data Model + +### Core Entities + +``` +users + ├── projects (1:many) + │ ├── embeddings (1:many) + │ └── instance (1:1) + │ + └── instances (1:many) + └── definition (many:1, optional) + +_system (special user) + └── definitions (1:many) +``` + +### Key Relationships + +**Users → Projects** +- One user owns many projects +- Projects can be shared with other users (reader/editor roles) +- Projects can be public (unauthenticated read access) + +**Projects → Instances** +- Each project references exactly one LLM service instance +- Instance defines embedding dimensions and configuration + +**Projects → Embeddings** +- One project contains many embeddings +- Each embedding has a unique text_id within the project +- Embeddings store vector, metadata, and optional text + +**Users → Instances** +- Users own their instances +- Instances can be shared with other users +- Instances store encrypted API keys + +**Instances → Definitions** +- Instances can optionally reference a definition (template) +- System definitions (`_system` owner) provide defaults +- User definitions allow custom templates + +## Request Flow + +### 1. Create Embedding + +``` +Client Request + ↓ +Authentication Middleware + ↓ +Authorization Check (owner/editor?) + ↓ +Dimension Validation (vector_dim matches instance?) + ↓ +Metadata Validation (matches project schema?) + ↓ +Database Insert (with transaction) + ↓ +Response +``` + +### 2. Similarity Search + +``` +Client Request (text_id or vector) + ↓ +Authentication Middleware (or public check) + ↓ +Authorization Check (owner/reader/public?) + ↓ +Dimension Validation (if raw vector) + ↓ +Vector Similarity Query + ├── Cosine distance calculation + ├── Threshold filtering + ├── Metadata filtering (exclude matches) + └── Limit/offset pagination + ↓ +Results (sorted by similarity) + ↓ +Response +``` + +## Storage Architecture + +### Vector Index + +pgvector supports multiple index types: + +- **IVFFlat**: Faster build, approximate search +- **HNSW**: Slower build, better recall + +Current implementation uses HNSW for better accuracy. + +### Vector Storage Format + +```sql +CREATE TABLE embeddings ( + embedding_id SERIAL PRIMARY KEY, + text_id TEXT NOT NULL, + project_id INT REFERENCES projects, + vector vector(3072), -- Dimension varies + vector_dim INT NOT NULL, + metadata JSONB, + text TEXT, + ... +) +``` + +### Index Strategy + +```sql +CREATE INDEX embedding_vector_idx +ON embeddings +USING hnsw (vector vector_cosine_ops); +``` + +Optimized for cosine similarity searches. + +## Security Architecture + +### API Key Encryption + +- **Algorithm**: AES-256-GCM +- **Key Source**: `ENCRYPTION_KEY` environment variable +- **Key Derivation**: SHA-256 hash to ensure 32-byte key +- **Storage**: Binary (BYTEA) in database + +### Access Control + +**Three-tier access model:** + +1. **Owner**: Full control (read, write, delete, share, transfer) +2. **Editor**: Read and write embeddings +3. **Reader**: Read-only access to embeddings and search + +**Special access:** +- **Admin**: System-wide operations (user management, sanity checks) +- **Public**: Unauthenticated read access (if `public_read=true`) + +### Data Isolation + +- Users can only access their own resources or shared resources +- Cross-user queries are prevented at the database level +- Project ownership enforced via foreign keys + +## Migration System + +Uses [tern](https://github.com/jackc/tern) for database migrations: + +``` +migrations/ + ├── 001_create_initial_scheme.sql + ├── 002_create_emb_index.sql + ├── 003_add_public_read_flag.sql + └── 004_refactor_llm_services_architecture.sql +``` + +Migrations run automatically on startup with rollback support. + +## Performance Characteristics + +### Vector Search Performance + +- **Small datasets** (<10K embeddings): <10ms per query +- **Medium datasets** (10K-100K): 10-50ms per query +- **Large datasets** (>100K): 50-200ms per query + +Performance depends on: +- Vector dimensions +- Index type and parameters +- Hardware (CPU, RAM, disk) +- Number of results requested + +### Scaling Considerations + +**Vertical Scaling:** +- More RAM = faster searches (more vectors in memory) +- Faster CPUs = faster vector comparisons +- SSD storage = faster index scans + +**Horizontal Scaling:** +- Read replicas for search queries +- Separate write/read workloads +- Connection pooling for concurrent requests + +## Technology Stack + +### Core Technologies + +- **Language**: Go 1.21+ +- **Web Framework**: Huma 2.x +- **Database**: PostgreSQL 16+ +- **Vector Extension**: pgvector 0.7.4 +- **Query Generator**: sqlc 1.x +- **Migration Tool**: tern 2.x + +### Development Tools + +- **Testing**: Go standard library + testcontainers +- **Documentation**: OpenAPI 3.0 (auto-generated) +- **Building**: Docker multi-stage builds +- **Deployment**: Docker Compose + +## Design Principles + +### 1. Type Safety + +- sqlc generates type-safe Go code from SQL +- Strong typing prevents SQL injection +- Compile-time validation of queries + +### 2. Simplicity + +- REST API (not GraphQL) +- Straightforward URL patterns +- Standard HTTP methods + +### 3. Security + +- API key encryption at rest +- No API keys in responses +- Role-based access control + +### 4. Validation + +- Automatic dimension validation +- Optional metadata schema validation +- Request/response validation via OpenAPI + +### 5. Extensibility + +- User-defined metadata schemas +- Custom LLM service configurations +- Flexible sharing model + +## Limitations + +### Current Constraints + +- **No multi-tenancy**: Each installation is single-tenant +- **No replication**: Manual setup required for HA +- **No caching**: All queries hit database +- **Synchronous API**: No async/batch upload endpoints + +### Future Enhancements + +See [Roadmap](../../reference/roadmap/) for planned improvements. + +## Next Steps + +- [Learn about users and authentication](users-and-auth/) +- [Understand projects](projects/) +- [Explore LLM services](llm-services/) diff --git a/docs/content/concepts/embeddings.md b/docs/content/concepts/embeddings.md new file mode 100644 index 0000000..a2e057d --- /dev/null +++ b/docs/content/concepts/embeddings.md @@ -0,0 +1,466 @@ +--- +title: "Embeddings" +weight: 4 +--- + +# Embeddings + +Embeddings are vector representations of text stored in dhamps-vdb for similarity search and retrieval. + +## What are Embeddings? + +Embeddings are numerical representations (vectors) of text that capture semantic meaning: + +- **Vector**: Array of floating-point numbers (e.g., 1536 or 3072 dimensions) +- **Dimensions**: Fixed length determined by LLM model +- **Similarity**: Vectors of similar text are close in vector space +- **Purpose**: Enable semantic search and retrieval + +## Embedding Structure + +### Required Fields + +- **text_id**: Unique identifier for the document (max 300 characters) +- **instance_handle**: LLM service instance that generated the embedding +- **vector**: Array of float32 values (embedding vector) +- **vector_dim**: Declared dimension count (must match vector length) + +### Optional Fields + +- **text**: Original text content (for reference) +- **metadata**: Structured JSON data about the document + +### Example + +```json +{ + "text_id": "doc-123", + "instance_handle": "my-openai", + "text": "Introduction to machine learning concepts", + "vector": [0.023, -0.015, 0.087, ..., 0.042], + "vector_dim": 3072, + "metadata": { + "title": "ML Introduction", + "author": "Alice", + "year": 2024, + "category": "tutorial" + } +} +``` + +## Creating Embeddings + +### Single Embedding + +```bash +POST /v1/embeddings/alice/research-docs + +{ + "embeddings": [ + { + "text_id": "doc1", + "instance_handle": "my-openai", + "vector": [0.1, 0.2, ..., 0.3], + "vector_dim": 3072, + "metadata": {"author": "Alice"} + } + ] +} +``` + +### Batch Upload + +```bash +POST /v1/embeddings/alice/research-docs + +{ + "embeddings": [ + { + "text_id": "doc1", + "instance_handle": "my-openai", + "vector": [...], + "vector_dim": 3072 + }, + { + "text_id": "doc2", + "instance_handle": "my-openai", + "vector": [...], + "vector_dim": 3072 + }, + ... + ] +} +``` + +**Batch upload tips:** +- Upload 100-1000 embeddings per request +- Use consistent instance_handle +- Ensure all vectors have same dimensions +- Include metadata for searchability + +## Text Identifiers + +### Format + +Text IDs can be any string up to 300 characters: + +**Common patterns:** +- **URLs**: `https://id.example.com/doc/123` +- **URNs**: `urn:example:doc:123` +- **Paths**: `/corpus/section1/doc123` +- **IDs**: `doc-abc-123-xyz` + +### URL Encoding + +URL-encode text IDs when using them in API paths: + +```bash +# Original ID +text_id="https://id.example.com/texts/W0017:1.3.1" + +# URL-encoded for API +encoded="https%3A%2F%2Fid.example.com%2Ftexts%2FW0017%3A1.3.1" + +# Use in API call +GET /v1/embeddings/alice/project/$encoded +``` + +### Uniqueness + +Text IDs must be unique within a project: +- Same ID in different projects: ✅ Allowed +- Same ID twice in one project: ❌ Conflict error + +## Validation + +### Dimension Validation + +The system automatically validates vector dimensions: + +**Checks performed:** +1. `vector_dim` matches declared instance dimensions +2. Actual `vector` array length matches `vector_dim` +3. All embeddings in project have consistent dimensions + +**Example error:** + +```json +{ + "title": "Bad Request", + "status": 400, + "detail": "dimension validation failed: vector dimension mismatch: embedding declares 3072 dimensions but LLM service 'my-openai' expects 1536 dimensions" +} +``` + +### Metadata Validation + +If project has a metadata schema, all embeddings are validated: + +**Example error:** + +```json +{ + "title": "Bad Request", + "status": 400, + "detail": "metadata validation failed for text_id 'doc1': metadata validation failed:\n - author is required\n - year must be integer" +} +``` + +See [Metadata Validation Guide](../guides/metadata-validation/) for details. + +## Retrieving Embeddings + +### List All Embeddings + +```bash +GET /v1/embeddings/alice/research-docs?limit=100&offset=0 +``` + +Returns paginated list of embeddings with: +- text_id +- metadata +- vector_dim +- created_at + +Vectors are included by default (can be large). + +### Get Single Embedding + +```bash +GET /v1/embeddings/alice/research-docs/doc1 +``` + +Returns complete embedding including vector. + +### Pagination + +Use `limit` and `offset` for large projects: + +```bash +# First page (0-99) +GET /v1/embeddings/alice/research-docs?limit=100&offset=0 + +# Second page (100-199) +GET /v1/embeddings/alice/research-docs?limit=100&offset=100 + +# Third page (200-299) +GET /v1/embeddings/alice/research-docs?limit=100&offset=200 +``` + +## Updating Embeddings + +Currently, embeddings cannot be updated directly. To modify: + +1. **Delete** existing embedding +2. **Upload** new version with same text_id + +```bash +# Delete old version +DELETE /v1/embeddings/alice/research-docs/doc1 + +# Upload new version +POST /v1/embeddings/alice/research-docs +{ + "embeddings": [{ + "text_id": "doc1", + "instance_handle": "my-openai", + "vector": [...new vector...], + "vector_dim": 3072, + "metadata": {...updated metadata...} + }] +} +``` + +## Deleting Embeddings + +### Delete Single Embedding + +```bash +DELETE /v1/embeddings/alice/research-docs/doc1 +``` + +### Delete All Embeddings + +```bash +DELETE /v1/embeddings/alice/research-docs +``` + +**Warning:** This deletes all embeddings in the project permanently. + +## Metadata + +### Purpose + +Metadata provides structured information about documents: + +- **Filtering**: Exclude documents in similarity searches +- **Organization**: Categorize and group documents +- **Context**: Store additional document information +- **Validation**: Ensure consistent structure (with schema) + +### Structure + +Metadata is stored as JSONB in PostgreSQL: + +```json +{ + "author": "William Shakespeare", + "title": "Hamlet", + "year": 1603, + "act": 1, + "scene": 1, + "genre": "drama", + "language": "English" +} +``` + +### Nested Metadata + +Complex structures are supported: + +```json +{ + "author": { + "name": "William Shakespeare", + "birth_year": 1564, + "nationality": "English" + }, + "publication": { + "year": 1603, + "publisher": "First Folio", + "edition": 1 + }, + "tags": ["tragedy", "revenge", "madness"] +} +``` + +### Filtering by Metadata + +Use metadata to exclude documents from similarity searches: + +```bash +# Exclude documents from same author +GET /v1/similars/alice/project/doc1?metadata_path=author&metadata_value=Shakespeare +``` + +See [Metadata Filtering Guide](../guides/metadata-filtering/) for details. + +## Storage Considerations + +### Vector Storage + +Vectors are stored using pgvector extension: + +- **Type**: `vector(N)` where N is dimension count +- **Size**: 4 bytes per dimension + overhead +- **Example**: 3072-dimension vector ≈ 12KB + +### Storage Calculation + +Estimate storage per embedding: + +``` +Vector: 4 bytes × dimensions +Text ID: length in bytes (avg ~50 bytes) +Text: length in bytes (optional) +Metadata: JSON size (varies, avg ~500 bytes) +Overhead: ~100 bytes (indexes, etc.) + +Example (3072-dim with metadata): +4 × 3072 + 50 + 500 + 100 ≈ 13KB per embedding +``` + +### Large Projects + +For projects with millions of embeddings: + +- Use pagination when listing +- Consider partial indexes for metadata +- Monitor database size +- Plan backup strategy + +## Performance + +### Upload Performance + +- **Small batches** (1-10): ~100ms per request +- **Medium batches** (100-500): ~500ms-2s per request +- **Large batches** (1000+): ~2-10s per request + +### Retrieval Performance + +- **Single embedding**: <10ms +- **Paginated list** (100 items): ~50ms +- **Large project scan**: Use pagination + +### Optimization Tips + +- Batch uploads when possible +- Use appropriate page sizes +- Include only needed fields +- Monitor query performance + +## Common Patterns + +### Document Chunking + +Split long documents into chunks: + +```json +{ + "embeddings": [ + { + "text_id": "doc1:chunk1", + "text": "First part of document...", + "vector": [...], + "metadata": {"doc_id": "doc1", "chunk": 1} + }, + { + "text_id": "doc1:chunk2", + "text": "Second part of document...", + "vector": [...], + "metadata": {"doc_id": "doc1", "chunk": 2} + } + ] +} +``` + +### Versioned Documents + +Track document versions: + +```json +{ + "text_id": "doc1:v2", + "vector": [...], + "metadata": { + "doc_id": "doc1", + "version": 2, + "updated_at": "2024-01-15T10:30:00Z" + } +} +``` + +### Multi-Language Documents + +Store embeddings for different languages: + +```json +{ + "embeddings": [ + { + "text_id": "doc1:en", + "text": "English version...", + "vector": [...], + "metadata": {"doc_id": "doc1", "language": "en"} + }, + { + "text_id": "doc1:de", + "text": "Deutsche Version...", + "vector": [...], + "metadata": {"doc_id": "doc1", "language": "de"} + } + ] +} +``` + +## Troubleshooting + +### Dimension Mismatch + +**Error**: "vector dimension mismatch" + +**Cause**: Vector dimensions don't match instance configuration + +**Solution**: +- Check instance dimensions: `GET /v1/llm-services/owner/instance` +- Regenerate embeddings with correct model +- Ensure `vector_dim` matches actual vector length + +### Metadata Validation Failed + +**Error**: "metadata validation failed" + +**Cause**: Metadata doesn't match project schema + +**Solution**: +- Check project schema: `GET /v1/projects/owner/project` +- Update metadata to match schema +- Or update schema to accept metadata + +### Text ID Conflict + +**Error**: "embedding with text_id already exists" + +**Cause**: Attempting to upload duplicate text_id + +**Solution**: +- Use different text_id +- Delete existing embedding first +- Check for unintended duplicates + +## Next Steps + +- [Learn about similarity search](similarity-search/) +- [Explore metadata filtering](../guides/metadata-filtering/) +- [Understand LLM services](llm-services/) diff --git a/docs/content/concepts/llm-services.md b/docs/content/concepts/llm-services.md new file mode 100644 index 0000000..d1c74b9 --- /dev/null +++ b/docs/content/concepts/llm-services.md @@ -0,0 +1,428 @@ +--- +title: "LLM Services" +weight: 5 +--- + +# LLM Services + +LLM Services configure embedding generation, defining models, dimensions, and API access. + +## Architecture + +dhamps-vdb separates LLM services into two concepts: + +### LLM Service Definitions + +Reusable configuration templates owned by `_system` or users: + +- **Purpose**: Provide standard configurations +- **Ownership**: `_system` (global) or individual users +- **Contents**: Endpoint, model, dimensions, API standard +- **API Keys**: Not stored (templates only) +- **Usage**: Templates for creating instances + +### LLM Service Instances + +User-specific configurations with encrypted API keys: + +- **Purpose**: Actual service configurations users employ +- **Ownership**: Individual users +- **Contents**: Endpoint, model, dimensions, API key (encrypted) +- **Sharing**: Can be shared with other users +- **Projects**: Each project references exactly one instance + +## System Definitions + +### Available Definitions + +The `_system` user provides default definitions: + +| Handle | Model | Dimensions | API Standard | +|--------|-------|------------|--------------| +| openai-large | text-embedding-3-large | 3072 | openai | +| openai-small | text-embedding-3-small | 1536 | openai | +| cohere-v4 | embed-multilingual-v4.0 | 1536 | cohere | +| gemini-embedding-001 | text-embedding-004 | 3072 | gemini | + +### Viewing System Definitions + +```bash +GET /v1/llm-services/_system +Authorization: Bearer user_vdb_key +``` + +Returns list of available system definitions. + +## Creating Instances + +### From System Definition + +Use a predefined system configuration: + +```bash +PUT /v1/llm-services/alice/my-openai + +{ + "definition_owner": "_system", + "definition_handle": "openai-large", + "description": "My OpenAI embeddings", + "api_key_encrypted": "sk-proj-your-openai-key" +} +``` + +Inherits endpoint, model, dimensions from system definition. + +### From User Definition + +Reference a user-created definition: + +```bash +PUT /v1/llm-services/alice/custom-instance + +{ + "definition_owner": "alice", + "definition_handle": "my-custom-config", + "api_key_encrypted": "your-api-key" +} +``` + +### Standalone Instance + +Create without a definition: + +```bash +PUT /v1/llm-services/alice/standalone + +{ + "endpoint": "https://api.openai.com/v1/embeddings", + "api_standard": "openai", + "model": "text-embedding-3-large", + "dimensions": 3072, + "description": "Standalone OpenAI instance", + "api_key_encrypted": "sk-proj-your-key" +} +``` + +All fields must be specified. + +## Instance Properties + +### Core Fields + +- **instance_handle**: Unique identifier (3-20 characters) +- **owner**: User who owns the instance +- **endpoint**: API endpoint URL +- **api_standard**: Authentication mechanism (openai, cohere, gemini) +- **model**: Model identifier +- **dimensions**: Vector dimensionality +- **description**: Human-readable description (optional) +- **definition_id**: Reference to definition (optional) + +### API Key Storage + +- **Write-only**: Provided on create/update +- **Encrypted**: AES-256-GCM encryption +- **Never returned**: Not included in GET responses +- **Secure**: Cannot be retrieved after creation + +## API Standards + +### Supported Standards + +| Standard | Key Method | Documentation | +|----------|------------|---------------| +| openai | Authorization: Bearer | [OpenAI Docs](https://platform.openai.com/docs/api-reference/embeddings) | +| cohere | Authorization: Bearer | [Cohere Docs](https://docs.cohere.com/reference/embed) | +| gemini | x-goog-api-key header | [Gemini Docs](https://ai.google.dev/gemini-api/docs/embeddings) | + +### Creating API Standards + +Admins can add new standards: + +```bash +POST /v1/api-standards +Authorization: Bearer ADMIN_KEY + +{ + "api_standard_handle": "custom", + "description": "Custom LLM API", + "key_method": "auth_bearer", + "key_field": null +} +``` + +## Instance Management + +### List Instances + +List all accessible instances (owned + shared): + +```bash +GET /v1/llm-services/alice +Authorization: Bearer alice_vdb_key +``` + +Returns instances where alice is owner or has been granted access. + +### Get Instance Details + +```bash +GET /v1/llm-services/alice/my-openai +Authorization: Bearer alice_vdb_key +``` + +Returns instance configuration (API key not included). + +### Update Instance + +```bash +PATCH /v1/llm-services/alice/my-openai + +{ + "description": "Updated description", + "api_key_encrypted": "new-api-key" +} +``` + +Only owner can update instances. + +### Delete Instance + +```bash +DELETE /v1/llm-services/alice/my-openai +Authorization: Bearer alice_vdb_key +``` + +**Constraints:** +- Cannot delete instance used by projects +- Delete projects first, then instance + +## Instance Sharing + +### Share with User + +```bash +POST /v1/llm-services/alice/my-openai/share + +{ + "share_with_handle": "bob", + "role": "reader" +} +``` + +**Shared users can:** +- Use instance in their projects +- View instance configuration +- **Cannot:** + - See API key + - Modify instance + - Delete instance + +### Unshare from User + +```bash +DELETE /v1/llm-services/alice/my-openai/share/bob +``` + +### List Shared Users + +```bash +GET /v1/llm-services/alice/my-openai/shared-with +``` + +Only owner can view shared users. + +## Instance References + +### In Projects + +Projects reference instances by owner and handle: + +```json +{ + "project_handle": "my-project", + "instance_owner": "alice", + "instance_handle": "my-openai" +} +``` + +### In Embeddings + +Embeddings reference instances by handle: + +```json +{ + "text_id": "doc1", + "instance_handle": "my-openai", + "vector": [...] +} +``` + +The instance owner is inferred from the project. + +### Shared Instance Format + +When using shared instances: + +**Own instance**: `"instance_handle": "my-openai"` +**Shared instance**: Reference via project configuration + +## Encryption + +### API Key Encryption + +API keys are encrypted using AES-256-GCM: + +**Encryption process:** +1. User provides plaintext API key +2. Server encrypts using `ENCRYPTION_KEY` from environment +3. Encrypted bytes stored in database +4. Key derivation: SHA-256 hash ensures 32-byte key + +**Decryption:** +- Only occurs internally for LLM API calls +- Never exposed via API responses +- Requires same `ENCRYPTION_KEY` + +### Security Notes + +- **Encryption key**: Set via `ENCRYPTION_KEY` environment variable +- **Key loss**: Losing encryption key means losing access to API keys +- **Key rotation**: Not currently supported +- **Backup**: Back up encryption key securely + +## LLM Processing + +### Current Status + +LLM processing (generating embeddings) is **not yet implemented**. + +### Future Implementation + +Planned features: +- Process text to generate embeddings +- Call external LLM APIs +- Store generated embeddings +- Batch processing support + +### Current Workflow + +Users must generate embeddings externally: + +1. Generate embeddings using LLM API (OpenAI, Cohere, etc.) +2. Upload pre-generated embeddings to dhamps-vdb +3. Use dhamps-vdb for storage and similarity search + +## Common Patterns + +### Per-Environment Instances + +```bash +# Development instance +PUT /v1/llm-services/alice/dev-embeddings +{ + "definition_owner": "_system", + "definition_handle": "openai-small", + "api_key_encrypted": "dev-api-key" +} + +# Production instance +PUT /v1/llm-services/alice/prod-embeddings +{ + "definition_owner": "_system", + "definition_handle": "openai-large", + "api_key_encrypted": "prod-api-key" +} +``` + +### Team Shared Instance + +```bash +# Owner creates instance +PUT /v1/llm-services/team-lead/team-embeddings +{ + "definition_owner": "_system", + "definition_handle": "openai-large", + "api_key_encrypted": "team-api-key" +} + +# Share with team members +POST /v1/llm-services/team-lead/team-embeddings/share +{"share_with_handle": "member1", "role": "reader"} + +POST /v1/llm-services/team-lead/team-embeddings/share +{"share_with_handle": "member2", "role": "reader"} + +# Members use in their projects +POST /v1/projects/member1 +{ + "project_handle": "my-project", + "instance_owner": "team-lead", + "instance_handle": "team-embeddings" +} +``` + +### Multi-Model Setup + +```bash +# Large model for important documents +PUT /v1/llm-services/alice/high-quality +{ + "definition_owner": "_system", + "definition_handle": "openai-large", + "api_key_encrypted": "api-key" +} + +# Small model for drafts +PUT /v1/llm-services/alice/fast-processing +{ + "definition_owner": "_system", + "definition_handle": "openai-small", + "api_key_encrypted": "api-key" +} +``` + +## Troubleshooting + +### Cannot Create Instance + +**Possible causes:** +- Instance handle already exists +- Referenced definition doesn't exist +- Missing required fields +- Invalid API standard + +**Solutions:** +- Choose different handle +- Verify definition: `GET /v1/llm-services/_system` +- Include all required fields +- Use valid API standard: `GET /v1/api-standards` + +### Cannot Use Instance in Project + +**Possible causes:** +- Instance doesn't exist +- Instance not owned or shared with user +- Incorrect owner/handle reference + +**Solutions:** +- Verify instance exists +- Check instance is accessible +- Confirm spelling of owner and handle + +### Dimension Mismatch + +**Error**: "dimension validation failed" + +**Cause**: Embedding dimensions don't match instance + +**Solutions:** +- Check instance dimensions +- Regenerate embeddings with correct model +- Create instance with correct dimensions + +## Next Steps + +- [Understand projects](projects/) +- [Learn about embeddings](embeddings/) +- [Explore instance management](../guides/instance-management/) diff --git a/docs/content/concepts/metadata.md b/docs/content/concepts/metadata.md new file mode 100644 index 0000000..36ddc48 --- /dev/null +++ b/docs/content/concepts/metadata.md @@ -0,0 +1,552 @@ +--- +title: "Metadata" +weight: 7 +--- + +# Metadata + +Structured JSON data attached to embeddings for organization, validation, and filtering. + +## Overview + +Metadata provides context and structure for your embeddings: + +- **Organization**: Categorize and group documents +- **Filtering**: Exclude documents in similarity searches +- **Validation**: Ensure consistent structure (optional) +- **Context**: Store additional document information + +## Metadata Structure + +### Format + +Metadata is JSON stored as JSONB in PostgreSQL: + +```json +{ + "author": "William Shakespeare", + "title": "Hamlet", + "year": 1603, + "genre": "drama" +} +``` + +### Types + +Supported JSON types: +- **String**: `"author": "Shakespeare"` +- **Number**: `"year": 1603` +- **Boolean**: `"published": true` +- **Array**: `"tags": ["tragedy", "revenge"]` +- **Object**: `"author": {"name": "...", "id": "..."}` +- **Null**: `"notes": null` + +### Nested Structure + +Complex hierarchies are supported: + +```json +{ + "document": { + "id": "W0017", + "type": "manuscript" + }, + "author": { + "name": "John Milton", + "birth_year": 1608, + "nationality": "English" + }, + "publication": { + "year": 1667, + "publisher": "First Edition", + "location": "London" + }, + "tags": ["poetry", "epic", "religious"] +} +``` + +## Metadata Schemas + +### Purpose + +JSON Schema validation ensures consistent metadata across all project embeddings. + +### Defining a Schema + +Include `metadataScheme` when creating/updating project: + +```bash +POST /v1/projects/alice + +{ + "project_handle": "research", + "metadataScheme": "{\"type\":\"object\",\"properties\":{\"author\":{\"type\":\"string\"},\"year\":{\"type\":\"integer\"}},\"required\":[\"author\"]}" +} +``` + +### Schema Format + +Use [JSON Schema](https://json-schema.org/) (draft-07+): + +```json +{ + "type": "object", + "properties": { + "author": { + "type": "string", + "minLength": 1 + }, + "year": { + "type": "integer", + "minimum": 1000, + "maximum": 2100 + }, + "genre": { + "type": "string", + "enum": ["poetry", "prose", "drama"] + } + }, + "required": ["author", "year"] +} +``` + +### Validation Behavior + +**With schema defined:** +- All embeddings validated on upload +- Invalid metadata rejected with detailed error +- Schema enforced consistently + +**Without schema:** +- Any JSON metadata accepted +- No validation performed +- Maximum flexibility + +### Common Patterns + +See [Metadata Validation Guide](../guides/metadata-validation/) for examples. + +## Using Metadata + +### On Upload + +Include metadata with each embedding: + +```bash +POST /v1/embeddings/alice/research + +{ + "embeddings": [ + { + "text_id": "doc1", + "instance_handle": "my-embeddings", + "vector": [...], + "metadata": { + "author": "Shakespeare", + "title": "Hamlet", + "year": 1603 + } + } + ] +} +``` + +### In Responses + +Metadata returned when retrieving embeddings: + +```bash +GET /v1/embeddings/alice/research/doc1 +``` + +```json +{ + "text_id": "doc1", + "metadata": { + "author": "Shakespeare", + "title": "Hamlet", + "year": 1603 + }, + "vector": [...], + ... +} +``` + +## Metadata Filtering + +### Exclusion Filter + +Exclude documents where metadata matches value: + +```bash +GET /v1/similars/alice/research/doc1?metadata_path=author&metadata_value=Shakespeare +``` + +**Result**: Returns similar documents **excluding** those with `metadata.author == "Shakespeare"`. + +### Path Syntax + +Use JSON path notation: + +**Simple field:** +``` +metadata_path=author +``` + +**Nested field:** +``` +metadata_path=author.name +``` + +**Array element** (not currently supported): +``` +metadata_path=tags[0] +``` + +### URL Encoding + +Encode special characters: + +```bash +# Space +metadata_value=John%20Doe + +# Quotes (if needed) +metadata_value=%22quoted%20value%22 +``` + +### Use Cases + +**Exclude same work:** +```bash +?metadata_path=title&metadata_value=Hamlet +``` + +**Exclude same author:** +```bash +?metadata_path=author&metadata_value=Shakespeare +``` + +**Exclude same source:** +```bash +?metadata_path=source_id&metadata_value=corpus-a +``` + +**Exclude same category:** +```bash +?metadata_path=category&metadata_value=draft +``` + +See [Metadata Filtering Guide](../guides/metadata-filtering/) for detailed examples. + +## Validation Examples + +### Simple Schema + +```json +{ + "type": "object", + "properties": { + "author": {"type": "string"}, + "year": {"type": "integer"} + }, + "required": ["author"] +} +``` + +**Valid metadata:** +```json +{"author": "Shakespeare", "year": 1603} +{"author": "Milton"} +``` + +**Invalid metadata:** +```json +{"year": 1603} // Missing required 'author' +{"author": 123} // Wrong type (should be string) +``` + +### Schema with Constraints + +```json +{ + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "rating": { + "type": "number", + "minimum": 0, + "maximum": 5 + }, + "tags": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1, + "maxItems": 10 + } + } +} +``` + +### Schema with Enums + +```json +{ + "type": "object", + "properties": { + "language": { + "type": "string", + "enum": ["en", "de", "fr", "es", "la"] + }, + "status": { + "type": "string", + "enum": ["draft", "review", "published"] + } + } +} +``` + +## Storage and Performance + +### Storage + +Metadata stored as JSONB in PostgreSQL: + +- **Efficient**: Binary storage format +- **Indexable**: Can create indexes on fields +- **Queryable**: Use PostgreSQL JSON operators + +### Size Considerations + +Typical metadata sizes: + +- **Simple**: 50-200 bytes +- **Moderate**: 200-1000 bytes +- **Complex**: 1-5KB +- **Very large**: >5KB (consider storing elsewhere) + +### Performance + +**Metadata filtering:** +- JSONB queries are efficient +- Add indexes for frequently filtered fields +- Keep metadata reasonably sized + +**Example index (if needed):** +```sql +CREATE INDEX idx_embeddings_author +ON embeddings ((metadata->>'author')); +``` + +## Common Patterns + +### Document Provenance + +Track document source and history: + +```json +{ + "source": { + "corpus": "Shakespeare Works", + "collection": "Tragedies", + "document_id": "hamlet", + "version": 2 + }, + "imported_at": "2024-01-15T10:30:00Z", + "imported_by": "researcher1" +} +``` + +### Hierarchical Documents + +Structure for nested documents: + +```json +{ + "work": "Paradise Lost", + "book": 1, + "line": 1, + "chapter": null, + "section": "Invocation" +} +``` + +### Multi-Language Content + +Track language and translation info: + +```json +{ + "language": "en", + "original_language": "la", + "translated_by": "John Smith", + "translation_year": 1850 +} +``` + +### Research Metadata + +Academic paper metadata: + +```json +{ + "doi": "10.1234/example.2024.001", + "authors": ["Alice Smith", "Bob Jones"], + "journal": "Digital Humanities Review", + "year": 2024, + "keywords": ["NLP", "embeddings", "RAG"] +} +``` + +## Updating Metadata + +### Current Limitation + +Metadata cannot be updated directly. To change: + +1. Delete embedding +2. Re-upload with updated metadata + +```bash +# Delete +DELETE /v1/embeddings/alice/project/doc1 + +# Re-upload with new metadata +POST /v1/embeddings/alice/project +{ + "embeddings": [{ + "text_id": "doc1", + "metadata": {...updated...}, + ... + }] +} +``` + +## Schema Updates + +### Updating Project Schema + +Use PATCH to update schema: + +```bash +PATCH /v1/projects/alice/research + +{ + "metadataScheme": "{...new schema...}" +} +``` + +### Effect on Existing Embeddings + +- **Existing embeddings**: Not revalidated +- **New embeddings**: Validated against new schema +- **Updates**: Validated against current schema + +### Migration Strategy + +When updating schema: + +1. Update project schema +2. Verify new embeddings work +3. Optionally re-upload existing embeddings + +## Validation Errors + +### Common Errors + +**Missing required field:** +```json +{ + "status": 400, + "detail": "metadata validation failed: author is required" +} +``` + +**Wrong type:** +```json +{ + "status": 400, + "detail": "metadata validation failed: year must be integer" +} +``` + +**Enum violation:** +```json +{ + "status": 400, + "detail": "metadata validation failed: genre must be one of [poetry, prose, drama]" +} +``` + +### Debugging + +To debug validation errors: + +1. Check project schema: `GET /v1/projects/owner/project` +2. Validate metadata with online tool: [jsonschemavalidator.net](https://www.jsonschemavalidator.net/) +3. Review error message for specific field +4. Update metadata or schema as needed + +## Best Practices + +### Schema Design + +- Start simple, add complexity as needed +- Use required fields for critical data +- Use enums for controlled vocabularies +- Document your schema + +### Metadata Content + +- Keep metadata focused and relevant +- Avoid redundant data +- Use consistent field names +- Consider future queries and filters + +### Performance + +- Keep metadata reasonably sized (<5KB) +- Index frequently queried fields +- Avoid deeply nested structures when possible + +## Troubleshooting + +### Validation Fails + +**Problem**: Metadata doesn't validate + +**Solutions:** +- Check project schema +- Verify metadata structure +- Test with JSON Schema validator +- Review error message details + +### Filtering Not Working + +**Problem**: Metadata filter doesn't exclude documents + +**Solutions:** +- Verify field path is correct +- Check value matches exactly (case-sensitive) +- URL-encode special characters +- Confirm metadata field exists + +### Schema Too Restrictive + +**Problem**: Cannot upload valid documents + +**Solutions:** +- Make fields optional (remove from `required`) +- Broaden type constraints +- Use `oneOf` for multiple valid formats +- Remove unnecessary validations + +## Next Steps + +- [Learn about metadata validation](../guides/metadata-validation/) +- [Explore metadata filtering](../guides/metadata-filtering/) +- [Understand similarity search](similarity-search/) diff --git a/docs/content/concepts/projects.md b/docs/content/concepts/projects.md new file mode 100644 index 0000000..18fecb2 --- /dev/null +++ b/docs/content/concepts/projects.md @@ -0,0 +1,439 @@ +--- +title: "Projects" +weight: 3 +--- + +# Projects + +Projects organize embeddings and define their configuration, including LLM service instances and optional metadata validation. + +## What is a Project? + +A project is a collection of document embeddings that share: + +- A single LLM service instance (embedding configuration) +- Optional metadata schema for validation +- Access control (ownership and sharing) +- Consistent vector dimensions + +## Project Properties + +### Core Fields + +- **project_handle**: Unique identifier within owner's namespace (3-20 characters) +- **owner**: User who owns the project +- **description**: Human-readable project description +- **instance_id**: Reference to LLM service instance (required, 1:1 relationship) +- **metadataScheme**: Optional JSON Schema for metadata validation +- **public_read**: Boolean flag for public read access +- **created_at**: Creation timestamp +- **updated_at**: Last modification timestamp + +### Unique Constraints + +Projects are uniquely identified by `(owner, project_handle)`: +- User "alice" can have project "research" +- User "bob" can also have project "research" +- Same user cannot have two projects with same handle + +## Creating Projects + +### Basic Project + +```bash +POST /v1/projects/alice + +{ + "project_handle": "literature-study", + "description": "Literary text analysis", + "instance_owner": "alice", + "instance_handle": "my-openai" +} +``` + +### Project with Metadata Schema + +```bash +POST /v1/projects/alice + +{ + "project_handle": "research-papers", + "description": "Academic papers with structured metadata", + "instance_owner": "alice", + "instance_handle": "my-embeddings", + "metadataScheme": "{\"type\":\"object\",\"properties\":{\"author\":{\"type\":\"string\"},\"year\":{\"type\":\"integer\"},\"doi\":{\"type\":\"string\"}},\"required\":[\"author\",\"year\"]}" +} +``` + +### Public Project + +```bash +POST /v1/projects/alice + +{ + "project_handle": "open-dataset", + "description": "Publicly accessible research data", + "instance_owner": "alice", + "instance_handle": "my-embeddings", + "public_read": true +} +``` + +### Shared Project + +```bash +POST /v1/projects/alice + +{ + "project_handle": "collaborative", + "description": "Team collaboration project", + "instance_owner": "alice", + "instance_handle": "my-embeddings", + "shared_with": [ + { + "user_handle": "bob", + "role": "editor" + }, + { + "user_handle": "charlie", + "role": "reader" + } + ] +} +``` + +## Project-Instance Relationship + +### One-to-One Constraint + +Each project references exactly one LLM service instance: + +``` +Project → Instance (1:1) + ├── Defines vector dimensions + ├── Specifies embedding model + └── Contains API configuration +``` + +**Why 1:1?** +- Ensures consistent dimensions across all embeddings +- Prevents dimension mismatches in similarity searches +- Simplifies validation and error handling + +### Specifying Instance + +Use owner and handle to reference an instance: + +```json +{ + "instance_owner": "alice", + "instance_handle": "my-openai" +} +``` + +The instance can be: +- Owned by project owner: `"instance_owner": "alice"` +- Shared with project owner: `"instance_owner": "bob"` (if bob shared with alice) + +## Metadata Schemas + +### Purpose + +Metadata schemas ensure consistent, structured metadata across all embeddings in a project. + +### Schema Format + +Use JSON Schema (draft-07 or later): + +```json +{ + "type": "object", + "properties": { + "author": {"type": "string"}, + "title": {"type": "string"}, + "year": { + "type": "integer", + "minimum": 1000, + "maximum": 2100 + }, + "genre": { + "type": "string", + "enum": ["fiction", "non-fiction", "poetry"] + } + }, + "required": ["author", "title", "year"] +} +``` + +### Validation Behavior + +- **With schema**: All embeddings validated on upload +- **Without schema**: Any JSON metadata accepted +- **Validation failure**: Upload rejected with detailed error +- **Schema updates**: Only apply to new/updated embeddings + +### Example Schemas + +See [Metadata Validation Guide](../guides/metadata-validation/) for detailed examples. + +## Access Control + +### Ownership + +- **Owner**: User who created the project +- **Full control**: Read, write, delete, share, transfer +- **Cannot be removed**: Owner always has access + +### Sharing + +Projects can be shared with specific users: + +**Reader Role** +- View embeddings +- Search for similar documents +- View project metadata +- Cannot modify anything + +**Editor Role** +- All reader permissions +- Add embeddings +- Modify embeddings +- Delete embeddings +- Cannot delete project or change settings + +**Managing Sharing:** + +```bash +# Share with user +POST /v1/projects/alice/my-project/share +{ + "share_with_handle": "bob", + "role": "reader" +} + +# Unshare from user +DELETE /v1/projects/alice/my-project/share/bob + +# List shared users +GET /v1/projects/alice/my-project/shared-with +``` + +### Public Access + +Projects can allow unauthenticated read access: + +```bash +PATCH /v1/projects/alice/my-project +{ + "public_read": true +} +``` + +With `public_read: true`: +- Anyone can view embeddings (no authentication) +- Anyone can search for similar documents +- Write operations still require authentication + +See [Public Projects Guide](../guides/public-projects/) for details. + +## Project Operations + +### List Projects + +List all projects owned by a user: + +```bash +GET /v1/projects/alice +Authorization: Bearer alice_vdb_key +``` + +Returns array of project objects. + +### Get Project Details + +```bash +GET /v1/projects/alice/research +Authorization: Bearer alice_vdb_key +``` + +Returns full project object including: +- Configuration +- Instance reference +- Metadata schema +- Sharing information (owner only) + +### Update Project + +Use PATCH for partial updates: + +```bash +PATCH /v1/projects/alice/research +{ + "description": "Updated description" +} +``` + +Use PUT for full replacement: + +```bash +PUT /v1/projects/alice/research +{ + "project_handle": "research", + "description": "Complete project configuration", + "instance_owner": "alice", + "instance_handle": "my-embeddings", + "metadataScheme": "{...}" +} +``` + +### Delete Project + +```bash +DELETE /v1/projects/alice/research +Authorization: Bearer alice_vdb_key +``` + +**Cascading deletion:** +- All embeddings in project deleted +- All sharing grants removed +- Project metadata removed + +## Ownership Transfer + +Transfer project to another user: + +```bash +POST /v1/projects/alice/research/transfer-ownership +{ + "new_owner_handle": "bob" +} +``` + +**Effects:** +- Project owner changes to bob +- Project URL changes: `/v1/projects/bob/research` +- Alice loses all access (unless re-shared) +- All embeddings transferred +- Bob cannot already have project with same handle + +See [Ownership Transfer Guide](../guides/ownership-transfer/) for details. + +## Project Limits + +### Current Implementation + +No enforced limits on: +- Number of embeddings per project +- Project storage size +- Number of shared users + +### Recommended Practices + +For large projects: +- Use pagination when listing embeddings +- Batch upload embeddings +- Monitor database size +- Consider archiving old projects + +## Common Patterns + +### Research Project Workflow + +```bash +# 1. Create project +POST /v1/projects/alice +{ + "project_handle": "study-2024", + "description": "2024 Research Study", + "instance_owner": "alice", + "instance_handle": "my-embeddings" +} + +# 2. Upload data +POST /v1/embeddings/alice/study-2024 +{ ... embeddings ... } + +# 3. Share with team +POST /v1/projects/alice/study-2024/share +{"share_with_handle": "bob", "role": "reader"} + +# 4. Make public when published +PATCH /v1/projects/alice/study-2024 +{"public_read": true} +``` + +### Multi-Project Organization + +```bash +# Development project +POST /v1/projects/alice +{ + "project_handle": "dev-experiments", + "instance_owner": "alice", + "instance_handle": "dev-embeddings" +} + +# Production project +POST /v1/projects/alice +{ + "project_handle": "prod-dataset", + "instance_owner": "alice", + "instance_handle": "prod-embeddings", + "metadataScheme": "{...}" +} + +# Archive project +POST /v1/projects/alice +{ + "project_handle": "archive-2023", + "instance_owner": "alice", + "instance_handle": "archive-embeddings", + "public_read": true +} +``` + +## Troubleshooting + +### Cannot Create Project + +**Possible causes:** +- Project handle already exists for this user +- Invalid project handle format +- Instance doesn't exist or not accessible +- Missing required fields + +**Solutions:** +- Choose different project handle +- Verify instance exists: `GET /v1/llm-services/owner` +- Check instance is owned or shared with you +- Include all required fields (instance_owner, instance_handle) + +### Metadata Validation Fails + +**Possible causes:** +- Metadata doesn't match schema +- Invalid JSON Schema format +- Schema too restrictive + +**Solutions:** +- Test schema with online validator +- Verify embedding metadata matches schema +- Update schema or metadata as needed + +### Cannot Share Project + +**Possible causes:** +- Not project owner +- Target user doesn't exist +- Invalid role specified + +**Solutions:** +- Only owner can share projects +- Verify user exists: `GET /v1/users/target` +- Use valid role: "reader" or "editor" + +## Next Steps + +- [Learn about embeddings](embeddings/) +- [Explore metadata validation](../guides/metadata-validation/) +- [Understand project sharing](../guides/project-sharing/) diff --git a/docs/content/concepts/similarity-search.md b/docs/content/concepts/similarity-search.md new file mode 100644 index 0000000..6e13155 --- /dev/null +++ b/docs/content/concepts/similarity-search.md @@ -0,0 +1,445 @@ +--- +title: "Similarity Search" +weight: 6 +--- + +# Similarity Search + +Find documents with similar semantic meaning using vector similarity. + +## How it Works + +Similarity search compares embedding vectors using cosine distance: + +1. **Query vector**: Either from stored embedding or raw vector +2. **Comparison**: Calculate cosine similarity with all project embeddings +3. **Filtering**: Apply threshold and metadata filters +4. **Ranking**: Sort by similarity score (highest first) +5. **Return**: Top N most similar documents + +## Search Methods + +### Stored Document Search (GET) + +Find documents similar to an already-stored embedding: + +```bash +GET /v1/similars/alice/research/doc1?count=10&threshold=0.7 +``` + +**Use cases:** +- Find related documents +- Discover similar passages +- Identify duplicates + +### Raw Vector Search (POST) + +Search using a new embedding without storing it: + +```bash +POST /v1/similars/alice/research?count=10&threshold=0.7 + +{ + "vector": [0.023, -0.015, ..., 0.042] +} +``` + +**Use cases:** +- Query without saving +- Test embeddings +- Real-time search + +## Query Parameters + +### count + +Number of similar documents to return. + +- **Type**: Integer +- **Range**: 1-200 +- **Default**: 10 + +```bash +GET /v1/similars/alice/project/doc1?count=5 +``` + +### threshold + +Minimum similarity score (0-1). + +- **Type**: Float +- **Range**: 0.0-1.0 +- **Default**: 0.5 +- **Meaning**: 1.0 = identical, 0.0 = unrelated + +```bash +GET /v1/similars/alice/project/doc1?threshold=0.8 +``` + +### limit + +Maximum number of results (same as count). + +- **Type**: Integer +- **Range**: 1-200 +- **Default**: 10 + +### offset + +Skip first N results (pagination). + +- **Type**: Integer +- **Minimum**: 0 +- **Default**: 0 + +```bash +# First page +GET /v1/similars/alice/project/doc1?limit=10&offset=0 + +# Second page +GET /v1/similars/alice/project/doc1?limit=10&offset=10 +``` + +### metadata_path + +JSON path to metadata field for filtering. + +- **Type**: String +- **Purpose**: Specify metadata field to filter +- **Must be used with**: metadata_value + +```bash +?metadata_path=author +``` + +### metadata_value + +Value to **exclude** from results. + +- **Type**: String +- **Purpose**: Exclude documents matching this value +- **Must be used with**: metadata_path + +```bash +?metadata_path=author&metadata_value=Shakespeare +``` + +**Important**: Excludes matches, doesn't include them. + +## Similarity Scores + +### Cosine Similarity + +dhamps-vdb uses cosine similarity: + +``` +similarity = 1 - cosine_distance +``` + +**Score ranges:** +- **1.0**: Identical vectors +- **0.9-1.0**: Very similar +- **0.7-0.9**: Similar +- **0.5-0.7**: Somewhat similar +- **<0.5**: Not similar + +### Interpreting Scores + +Typical thresholds: + +- **0.9+**: Duplicates or near-duplicates +- **0.8+**: Strong semantic similarity +- **0.7+**: Related topics +- **0.5-0.7**: Weak relation +- **<0.5**: Unrelated + +Optimal threshold depends on your use case and model. + +## Metadata Filtering + +### Exclude by Field + +Exclude documents where metadata field matches value: + +```bash +# Exclude documents from same author +GET /v1/similars/alice/lit-study/hamlet-act1?metadata_path=author&metadata_value=Shakespeare +``` + +**Result**: Returns similar documents, excluding those with `metadata.author == "Shakespeare"`. + +### Nested Fields + +Use dot notation for nested metadata: + +```bash +# Exclude documents from same author.name +GET /v1/similars/alice/project/doc1?metadata_path=author.name&metadata_value=John%20Doe +``` + +### Common Patterns + +**Exclude same work:** +```bash +?metadata_path=title&metadata_value=Hamlet +``` + +**Exclude same source:** +```bash +?metadata_path=source_id&metadata_value=corpus-A +``` + +**Exclude same category:** +```bash +?metadata_path=category&metadata_value=tutorial +``` + +See [Metadata Filtering Guide](../guides/metadata-filtering/) for details. + +## Response Format + +```json +{ + "user_handle": "alice", + "project_handle": "research", + "results": [ + { + "id": "doc2", + "similarity": 0.95 + }, + { + "id": "doc5", + "similarity": 0.87 + }, + { + "id": "doc8", + "similarity": 0.82 + } + ] +} +``` + +**Fields:** +- **user_handle**: Project owner +- **project_handle**: Project identifier +- **results**: Array of similar documents + - **id**: Document text_id + - **similarity**: Similarity score (0-1) + +Results are sorted by similarity (highest first). + +## Performance + +### Query Speed + +Typical performance: +- **<10K embeddings**: <10ms +- **10K-100K embeddings**: 10-50ms +- **100K-1M embeddings**: 50-200ms +- **>1M embeddings**: 200-1000ms + +### Optimization + +**HNSW Index:** +- Faster queries than IVFFlat +- Better recall +- Larger index size + +**Query optimization:** +- Use appropriate threshold (higher = fewer results) +- Limit result count (lower = faster) +- Consider dimension reduction for large projects + +### Scaling + +For large datasets: +- Monitor query performance +- Consider read replicas +- Use connection pooling +- Cache frequent queries (application level) + +## Common Use Cases + +### RAG Workflow + +Retrieval Augmented Generation: + +```bash +# 1. User query +query="What is machine learning?" + +# 2. Generate query embedding (external) +query_vector=[...] + +# 3. Find similar documents +POST /v1/similars/alice/knowledge-base?count=5&threshold=0.7 +{"vector": $query_vector} + +# 4. Retrieve full text for top results +for each result: + GET /v1/embeddings/alice/knowledge-base/$result_id + +# 5. Send context to LLM for generation +``` + +### Duplicate Detection + +Find near-duplicate documents: + +```bash +# High threshold for duplicates +GET /v1/similars/alice/corpus/doc1?count=10&threshold=0.95 +``` + +Documents with similarity > 0.95 are likely duplicates. + +### Content Discovery + +Find related content: + +```bash +# Moderate threshold for recommendations +GET /v1/similars/alice/articles/article1?count=10&threshold=0.7&metadata_path=article_id&metadata_value=article1 +``` + +Excludes the source article itself. + +### Topic Clustering + +Find documents on similar topics: + +```bash +# For each document, find similar ones +for doc in documents: + GET /v1/similars/alice/corpus/$doc?count=20&threshold=0.8 +``` + +Group documents by similarity for clustering. + +## Dimension Consistency + +### Automatic Filtering + +Similarity queries only compare embeddings with matching dimensions: + +``` +Project embeddings: + - doc1: 3072 dimensions + - doc2: 3072 dimensions + - doc3: 1536 dimensions (different model) + +Query for doc1 similars: + → Only compares with doc2 + → Ignores doc3 (dimension mismatch) +``` + +### Multiple Instances + +Projects can have embeddings from multiple instances (if dimensions match): + +```json +{ + "text_id": "doc1", + "instance_handle": "openai-large", + "vector_dim": 3072 +} + +{ + "text_id": "doc2", + "instance_handle": "custom-model", + "vector_dim": 3072 +} +``` + +Both searchable together (same dimensions). + +## Access Control + +### Authentication + +Similarity search respects project access control: + +**Owner**: Full access +**Editor**: Can search (read permission) +**Reader**: Can search (read permission) +**Public** (if public_read=true): Can search (no auth required) + +### Public Projects + +Public projects allow unauthenticated similarity search: + +```bash +# No Authorization header needed +GET /v1/similars/alice/public-project/doc1?count=10 +``` + +See [Public Projects Guide](../guides/public-projects/). + +## Limitations + +### Current Constraints + +- **No cross-project search**: Similarity search is per-project only +- **No filtering by multiple metadata fields**: One field at a time +- **No custom distance metrics**: Cosine similarity only +- **No approximate search tuning**: Uses default HNSW parameters + +### Workarounds + +**Cross-project search:** +- Query each project separately +- Merge results in application + +**Multiple metadata filters:** +- Filter by one field in query +- Apply additional filters in application + +## Troubleshooting + +### No Results Returned + +**Possible causes:** +- Threshold too high +- No embeddings in project +- Dimension mismatch +- All results filtered by metadata + +**Solutions:** +- Lower threshold (try 0.5) +- Verify embeddings exist +- Check dimensions match +- Remove metadata filter + +### Unexpected Results + +**Possible causes:** +- Threshold too low +- Poor quality embeddings +- Incorrect model used +- Metadata filter excluding desired results + +**Solutions:** +- Increase threshold +- Regenerate embeddings +- Verify correct model/dimensions +- Adjust metadata filter + +### Slow Queries + +**Possible causes:** +- Large dataset (>100K embeddings) +- No vector index +- High result count +- Complex metadata filtering + +**Solutions:** +- Reduce result count +- Check index exists +- Optimize database +- Use read replicas + +## Next Steps + +- [Learn about metadata filtering](../guides/metadata-filtering/) +- [Understand RAG workflows](../guides/rag-workflow/) +- [Explore embeddings](embeddings/) diff --git a/docs/content/concepts/users-and-auth.md b/docs/content/concepts/users-and-auth.md new file mode 100644 index 0000000..c1c7c1f --- /dev/null +++ b/docs/content/concepts/users-and-auth.md @@ -0,0 +1,368 @@ +--- +title: "Users and Authentication" +weight: 2 +--- + +# Users and Authentication + +dhamps-vdb uses token-based authentication with API keys for all operations. + +## User Model + +### User Properties + +- **user_handle**: Unique identifier (3-20 characters, alphanumeric + underscore) +- **name**: Full name (optional) +- **email**: Email address (unique, required) +- **vdb_key**: API key (SHA-256 hash, 64 characters) +- **created_at**: Timestamp of creation +- **updated_at**: Timestamp of last update + +### Special Users + +**`_system` User** + +- Created automatically during database migration +- Owns system-wide LLM service definitions +- Cannot be used for authentication +- Provides default configurations for all users + +## Authentication Flow + +### API Key Authentication + +All requests (except public endpoints) require authentication: + +```http +GET /v1/projects/alice/my-project +Authorization: Bearer 024v2013621509245f2e24... +``` + +### Authentication Process + +1. Client sends API key in `Authorization` header with `Bearer` prefix +2. Server extracts key and looks up user in database +3. If user found, request proceeds with user context +4. If not found or missing, returns `401 Unauthorized` + +### Admin Authentication + +Administrative operations require the admin API key: + +```bash +curl -X POST http://localhost:8880/v1/users \ + -H "Authorization: Bearer YOUR_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{"user_handle":"alice","email":"alice@example.com"}' +``` + +Admin key is set via `SERVICE_ADMINKEY` environment variable. + +## User Creation + +### By Admin + +Only admin users can create new users: + +```bash +POST /v1/users +Authorization: Bearer ADMIN_KEY + +{ + "user_handle": "researcher1", + "name": "Research User", + "email": "researcher@example.com" +} +``` + +**Response:** + +```json +{ + "user_handle": "researcher1", + "name": "Research User", + "email": "researcher@example.com", + "vdb_key": "024v2013621509245f2e24abcdef...", + "created_at": "2024-01-15T10:30:00Z" +} +``` + +**Important:** Save the `vdb_key` immediately - it cannot be recovered later. + +### User Handle Restrictions + +- Must be 3-20 characters +- Alphanumeric characters and underscores only +- Must be unique +- Cannot be `_system` + +## User Management + +### Retrieve User Information + +Users can view their own information: + +```bash +GET /v1/users/alice +Authorization: Bearer alice_vdb_key +``` + +Admins can view any user: + +```bash +GET /v1/users/alice +Authorization: Bearer ADMIN_KEY +``` + +### List All Users + +Only admins can list all users: + +```bash +GET /v1/users +Authorization: Bearer ADMIN_KEY +``` + +Returns array of user handles (not full user objects). + +### Update User + +Users can update their own information: + +```bash +PATCH /v1/users/alice +Authorization: Bearer alice_vdb_key + +{ + "name": "Alice Smith-Jones", + "email": "alice.smith@example.com" +} +``` + +### Delete User + +Users can delete their own account: + +```bash +DELETE /v1/users/alice +Authorization: Bearer alice_vdb_key +``` + +Admins can delete any user: + +```bash +DELETE /v1/users/alice +Authorization: Bearer ADMIN_KEY +``` + +**Cascading Deletion:** +- All user's projects are deleted +- All user's LLM service instances are deleted +- All embeddings in user's projects are deleted +- Sharing grants from this user to others are removed + +## Authorization Model + +### Resource Ownership + +Users own three types of resources: + +1. **Projects**: Collections of embeddings +2. **LLM Service Instances**: Embedding configurations with API keys +3. **LLM Service Definitions**: Reusable configuration templates (optional) + +### Access Levels + +**Owner** +- Full control over resource +- Can read, write, delete +- Can share with others +- Can transfer ownership (projects only) + +**Editor** (via sharing) +- Read and write access +- Cannot delete resource +- Cannot modify sharing +- Cannot change project settings + +**Reader** (via sharing) +- Read-only access +- Can view embeddings +- Can search for similar documents +- Cannot modify anything + +**Public** (if project.public_read = true) +- Unauthenticated read access +- Can view embeddings +- Can search for similar documents +- Cannot write or modify + +## Security Best Practices + +### API Key Management + +**Storage** +- Store API keys securely (e.g., environment variables, secret managers) +- Never commit API keys to version control +- Use different keys for development and production + +**Rotation** +- Currently, API keys cannot be rotated +- To change a key, delete and recreate the user +- Plan key rotation strategy before production deployment + +**Transmission** +- Always use HTTPS in production +- API keys are transmitted in Authorization header +- Never pass API keys in URL query parameters + +### User Key vs. LLM API Keys + +dhamps-vdb handles two types of keys: + +1. **User API keys** (`vdb_key`): Authenticate users to dhamps-vdb + - Stored as SHA-256 hash in database + - Never encrypted (one-way hash) + - Returned only once on user creation + +2. **LLM API keys** (`api_key_encrypted`): Authenticate to LLM services + - Stored encrypted (AES-256-GCM) in database + - Never returned in API responses + - Used internally for LLM processing + +## Multi-User Workflows + +### Collaboration Pattern + +1. **Admin** creates user accounts for team members +2. **Project Owner** creates project with embeddings +3. **Owner** shares project with collaborators +4. **Readers** can search and view embeddings +5. **Editors** can add/modify embeddings + +### Organization Pattern + +1. **Admin** creates organizational users +2. **Each user** creates LLM service instances with their own API keys +3. **Users** create projects using their instances +4. **Projects** shared within organization as needed + +### Public Access Pattern + +1. **User** creates project with research data +2. **User** sets `public_read: true` +3. **Anyone** can access embeddings and search without authentication +4. **Only owner** can modify project + +## User Limits + +### Current Implementation + +No enforced limits on: +- Number of projects per user +- Number of embeddings per project +- Number of LLM service instances per user +- Storage size per user + +### Recommended Limits + +For production deployments, consider implementing: +- Rate limiting per API key +- Storage quotas per user +- Maximum project count per user + +## Example Workflows + +### Create and Use User Account + +```bash +# Admin creates user +curl -X POST http://localhost:8880/v1/users \ + -H "Authorization: Bearer $ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "user_handle": "researcher1", + "email": "researcher@example.com", + "name": "Research User" + }' + +# Save returned vdb_key +export USER_KEY="returned-vdb-key" + +# User verifies access +curl -X GET http://localhost:8880/v1/users/researcher1 \ + -H "Authorization: Bearer $USER_KEY" + +# User creates project +curl -X POST http://localhost:8880/v1/projects/researcher1 \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "my-project", + "description": "My research project" + }' +``` + +### Share Resources + +```bash +# User shares project with colleague +curl -X POST http://localhost:8880/v1/projects/researcher1/my-project/share \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "share_with_handle": "colleague1", + "role": "reader" + }' + +# Colleague accesses shared project +curl -X GET http://localhost:8880/v1/projects/researcher1/my-project \ + -H "Authorization: Bearer $COLLEAGUE_KEY" +``` + +## Troubleshooting + +### 401 Unauthorized + +**Possible causes:** +- Missing Authorization header +- Incorrect API key +- Expired or invalid key +- Using user key instead of admin key (or vice versa) + +**Solution:** +- Verify API key is correct +- Check Authorization header format: `Bearer KEY` +- Ensure operation matches key type (admin vs. user) + +### 403 Forbidden + +**Possible causes:** +- User doesn't own resource +- User not granted access to shared resource +- Insufficient permissions (reader trying to edit) + +**Solution:** +- Verify resource ownership +- Check sharing grants +- Ensure user has required role (editor for writes) + +### User Creation Failed + +**Possible causes:** +- User handle already exists +- Email already registered +- Invalid user handle format +- Not using admin key + +**Solution:** +- Choose different user handle +- Use unique email address +- Check user handle format (3-20 chars, alphanumeric + underscore) +- Verify using admin API key + +## Next Steps + +- [Learn about projects](projects/) +- [Understand LLM services](llm-services/) +- [Explore project sharing](../guides/project-sharing/) diff --git a/docs/content/deployment/_index.md b/docs/content/deployment/_index.md new file mode 100644 index 0000000..f423702 --- /dev/null +++ b/docs/content/deployment/_index.md @@ -0,0 +1,35 @@ +--- +title: "Deployment" +weight: 5 +--- + +# Deployment Guide + +Deploy dhamps-vdb to production environments. + +## Deployment Options + +dhamps-vdb can be deployed in several ways: + +- **Docker Compose** - Simplest option, includes PostgreSQL +- **Docker with External Database** - Production-ready setup +- **Standalone Binary** - For custom environments +- **Kubernetes** - For orchestrated deployments + +## Production Considerations + +When deploying to production: + +- Use strong, randomly generated keys +- Enable HTTPS/TLS for all API endpoints +- Configure database backups +- Set up monitoring and logging +- Restrict network access to database +- Use environment variables for sensitive configuration + +## Guides + +- [Docker Deployment](docker/) - Complete Docker guide +- [Database Setup](database/) - PostgreSQL configuration +- [Environment Variables](environment-variables/) - All configuration options +- [Security](security/) - Security best practices diff --git a/docs/content/development/_index.md b/docs/content/development/_index.md new file mode 100644 index 0000000..367e09a --- /dev/null +++ b/docs/content/development/_index.md @@ -0,0 +1,47 @@ +--- +title: "Development" +weight: 6 +--- + +# Development Guide + +Information for developers contributing to dhamps-vdb. + +## Getting Started with Development + +This section covers: + +- Setting up a development environment +- Running tests +- Understanding the codebase architecture +- Contributing guidelines +- Performance optimization + +## Project Structure + +``` +dhamps-vdb/ +├── main.go # Application entry point +├── internal/ +│ ├── auth/ # Authentication logic +│ ├── database/ # Database layer (sqlc) +│ ├── handlers/ # HTTP handlers +│ └── models/ # Data models +├── testdata/ # Test fixtures +└── docs/ # Documentation +``` + +## Development Workflow + +1. Make changes to code +2. Generate sqlc code if database queries changed: `sqlc generate` +3. Run tests: `go test -v ./...` +4. Build: `go build -o dhamps-vdb main.go` +5. Submit pull request + +## Resources + +- [Testing](testing/) - How to run tests +- [Contributing](contributing/) - Contribution guidelines +- [Architecture](architecture/) - Technical deep-dive +- [Performance](performance/) - Optimization notes diff --git a/docs/content/getting-started/_index.md b/docs/content/getting-started/_index.md new file mode 100644 index 0000000..2b30682 --- /dev/null +++ b/docs/content/getting-started/_index.md @@ -0,0 +1,31 @@ +--- +title: "Getting Started" +weight: 1 +--- + +# Getting Started with dhamps-vdb + +This section helps you get dhamps-vdb up and running quickly. Whether you're using Docker or compiling from source, you'll find everything you need to start using the vector database API. + +## What You'll Learn + +- How to install and run dhamps-vdb +- How to configure the service for your environment +- Basic usage patterns and workflows +- Creating your first project and embeddings + +## Prerequisites + +Before you begin, ensure you have: + +- PostgreSQL 11+ with pgvector extension (or use the provided Docker setup) +- Go 1.21+ (if compiling from source) +- Docker and Docker Compose (for containerized deployment) + +## Quick Links + +- [Installation](installation/) - Compile and install dhamps-vdb +- [Docker Deployment](docker/) - Run with Docker (recommended) +- [Configuration](configuration/) - Environment variables and options +- [Quick Start](quick-start/) - Your first API requests +- [First Project](first-project/) - Complete walkthrough diff --git a/docs/content/getting-started/configuration.md b/docs/content/getting-started/configuration.md new file mode 100644 index 0000000..8997b24 --- /dev/null +++ b/docs/content/getting-started/configuration.md @@ -0,0 +1,156 @@ +--- +title: "Configuration" +weight: 2 +--- + +# Configuration + +Configure dhamps-vdb using environment variables or command-line options. + +## Environment Variables + +All configuration can be set via environment variables. Use a `.env` file to keep sensitive information secure. + +### Service Configuration + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `SERVICE_DEBUG` | Enable debug logging | `true` | No | +| `SERVICE_HOST` | Hostname to listen on | `localhost` | No | +| `SERVICE_PORT` | Port to listen on | `8880` | No | + +### Database Configuration + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `SERVICE_DBHOST` | Database hostname | `localhost` | Yes | +| `SERVICE_DBPORT` | Database port | `5432` | No | +| `SERVICE_DBUSER` | Database username | `postgres` | Yes | +| `SERVICE_DBPASSWORD` | Database password | `password` | Yes | +| `SERVICE_DBNAME` | Database name | `postgres` | Yes | + +### Security Configuration + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `SERVICE_ADMINKEY` | Admin API key for administrative operations | - | Yes | +| `ENCRYPTION_KEY` | Encryption key for API keys (32+ characters) | - | Yes | + +## Configuration File + +Create a `.env` file in the project root: + +```bash +# Service Configuration +SERVICE_DEBUG=false +SERVICE_HOST=0.0.0.0 +SERVICE_PORT=8880 + +# Database Configuration +SERVICE_DBHOST=localhost +SERVICE_DBPORT=5432 +SERVICE_DBUSER=dhamps_user +SERVICE_DBPASSWORD=secure_password +SERVICE_DBNAME=dhamps_vdb + +# Security +SERVICE_ADMINKEY=your-secure-admin-key-here +ENCRYPTION_KEY=your-32-character-encryption-key-minimum +``` + +## Command-Line Options + +You can also provide configuration via command-line flags: + +```bash +./dhamps-vdb \ + --debug \ + -p 8880 \ + --db-host localhost \ + --db-port 5432 \ + --db-user dhamps_user \ + --db-password secure_password \ + --db-name dhamps_vdb \ + --admin-key your-admin-key +``` + +## Generating Secure Keys + +### Admin Key + +Generate a secure admin key: + +```bash +openssl rand -base64 32 +``` + +### Encryption Key + +Generate a secure encryption key (minimum 32 characters): + +```bash +openssl rand -hex 32 +``` + +## Configuration Priority + +Configuration is loaded in the following order (later sources override earlier ones): + +1. Default values (from `options.go`) +2. Environment variables +3. `.env` file +4. Command-line flags + +## Security Best Practices + +- **Never commit `.env` files** to version control +- Use **strong, randomly generated keys** for production +- Ensure `.env` file permissions are restrictive (`chmod 600 .env`) +- **Store encryption key securely** - losing it means losing access to encrypted API keys +- Use different keys for development and production environments + +## Example Configuration + +### Development + +```bash +# .env (development) +SERVICE_DEBUG=true +SERVICE_HOST=localhost +SERVICE_PORT=8880 +SERVICE_DBHOST=localhost +SERVICE_DBPORT=5432 +SERVICE_DBUSER=postgres +SERVICE_DBPASSWORD=password +SERVICE_DBNAME=dhamps_vdb_dev +SERVICE_ADMINKEY=dev-admin-key-change-me +ENCRYPTION_KEY=dev-encryption-key-32-chars-min +``` + +### Production + +```bash +# .env (production) +SERVICE_DEBUG=false +SERVICE_HOST=0.0.0.0 +SERVICE_PORT=8880 +SERVICE_DBHOST=prod-db.example.com +SERVICE_DBPORT=5432 +SERVICE_DBUSER=dhamps_prod +SERVICE_DBPASSWORD=$(cat /run/secrets/db_password) +SERVICE_DBNAME=dhamps_vdb +SERVICE_ADMINKEY=$(cat /run/secrets/admin_key) +ENCRYPTION_KEY=$(cat /run/secrets/encryption_key) +``` + +## Validation + +The service validates configuration on startup and will exit with an error if required variables are missing. + +## Next Steps + +After configuration: + +1. [Run the Quick Start tutorial](quick-start/) +2. [Create your first project](first-project/) +3. Review [deployment options](../deployment/) diff --git a/docs/content/getting-started/docker.md b/docs/content/getting-started/docker.md new file mode 100644 index 0000000..82504d8 --- /dev/null +++ b/docs/content/getting-started/docker.md @@ -0,0 +1,537 @@ +--- +title: "Docker Deployment" +weight: 2 +--- + +# Docker Deployment + +Deploy dhamps-vdb using Docker containers. This is the recommended approach for most users. + +## Quick Start + +The fastest way to get dhamps-vdb running with Docker: + +```bash +# Clone the repository +git clone https://github.com/mpilhlt/dhamps-vdb.git +cd dhamps-vdb + +# Run automated setup (generates secure keys) +./docker-setup.sh + +# Start services with docker-compose +docker-compose up -d + +# Check logs +docker-compose logs -f dhamps-vdb + +# Access the API +curl http://localhost:8880/docs +``` + +## What's Included + +The Docker Compose setup includes: + +- **dhamps-vdb**: The vector database API service +- **PostgreSQL 16**: Database with pgvector extension +- **Persistent storage**: Named volume for database data + +## Configuration Files + +### .env File + +All configuration is managed through environment variables. Copy the template: + +```bash +cp .env.docker.template .env +``` + +Edit `.env` to set required values: + +```bash +# Admin API key for administrative operations +SERVICE_ADMINKEY=your-secure-admin-key-here + +# Encryption key for API keys (32+ characters) +ENCRYPTION_KEY=your-secure-encryption-key-min-32-chars + +# Database password +SERVICE_DBPASSWORD=secure-database-password + +# Optional: Debug mode +SERVICE_DEBUG=false + +# Optional: Change ports +API_PORT=8880 +POSTGRES_PORT=5432 +``` + +### docker-compose.yml + +The compose file defines two services: + +```yaml +services: + postgres: + image: pgvector/pgvector:0.7.4-pg16 + # PostgreSQL with pgvector support + + dhamps-vdb: + build: . + # The API service + depends_on: + - postgres +``` + +## Deployment Options + +### Option 1: Docker Compose with Included Database (Recommended) + +Use the provided `docker-compose.yml`: + +```bash +docker-compose up -d +``` + +**Advantages:** +- Everything included +- Automatic networking +- Data persistence +- Easy to manage + +**Use when:** +- Getting started +- Development/testing +- Small to medium deployments + +### Option 2: Standalone Container with External Database + +Run only the dhamps-vdb container: + +```bash +# Build the image +docker build -t dhamps-vdb:latest . + +# Run the container +docker run -d \ + --name dhamps-vdb \ + -p 8880:8880 \ + -e SERVICE_DBHOST=your-db-host \ + -e SERVICE_DBPORT=5432 \ + -e SERVICE_DBUSER=dbuser \ + -e SERVICE_DBPASSWORD=dbpass \ + -e SERVICE_DBNAME=dhamps_vdb \ + -e SERVICE_ADMINKEY=admin-key \ + -e ENCRYPTION_KEY=encryption-key \ + dhamps-vdb:latest +``` + +**Use when:** +- You have an existing PostgreSQL server +- Production deployments +- Need database separation + +### Option 3: Docker Compose with External Database + +Modify `docker-compose.yml` to remove the postgres service: + +```yaml +services: + dhamps-vdb: + build: . + ports: + - "${API_PORT:-8880}:8880" + environment: + SERVICE_DBHOST: external-db.example.com + # ... other variables +``` + +## Building the Image + +### Standard Build + +```bash +docker build -t dhamps-vdb:latest . +``` + +### Custom Tag + +```bash +docker build -t dhamps-vdb:v0.1.0 . +``` + +### Clean Build (No Cache) + +```bash +docker build --no-cache -t dhamps-vdb:latest . +``` + +### Multi-Stage Build Details + +The Dockerfile uses multi-stage builds for efficiency: + +1. **Builder stage**: Compiles Go code with sqlc generation +2. **Runtime stage**: Minimal Alpine image with only the binary + +Result: Small, secure image (~20MB vs 800MB+) + +## Managing Services + +### Start Services + +```bash +# Start in background +docker-compose up -d + +# Start with logs visible +docker-compose up + +# Rebuild and start +docker-compose up -d --build +``` + +### View Logs + +```bash +# All logs +docker-compose logs + +# Follow logs in real-time +docker-compose logs -f + +# Specific service +docker-compose logs -f dhamps-vdb +docker-compose logs -f postgres +``` + +### Stop Services + +```bash +# Stop containers (keeps data) +docker-compose stop + +# Stop and remove containers (keeps data) +docker-compose down + +# Stop and remove everything including data +docker-compose down -v +``` + +### Restart Services + +```bash +# Restart all +docker-compose restart + +# Restart specific service +docker-compose restart dhamps-vdb +``` + +## Data Persistence + +### Docker Volumes + +The compose file creates a named volume: + +```yaml +volumes: + postgres_data: +``` + +This ensures database data persists across container restarts. + +### View Volumes + +```bash +docker volume ls +``` + +### Inspect Volume + +```bash +docker volume inspect dhamps-vdb_postgres_data +``` + +### Backup Database + +```bash +# Create backup +docker-compose exec postgres pg_dump -U postgres dhamps_vdb > backup.sql + +# Restore from backup +docker-compose exec -T postgres psql -U postgres dhamps_vdb < backup.sql +``` + +## Networking + +### Access from Host + +The API is accessible at: + +``` +http://localhost:8880 +``` + +### Access from Other Containers + +Use the service name as hostname: + +``` +http://dhamps-vdb:8880 +``` + +### Custom Network + +To use an existing Docker network: + +```yaml +networks: + default: + external: + name: your-network-name +``` + +## Security + +### Required Environment Variables + +Two critical environment variables must be set: + +1. **SERVICE_ADMINKEY**: Admin API key +2. **ENCRYPTION_KEY**: For encrypting user API keys (32+ chars) + +### Generating Secure Keys + +```bash +# Admin key +openssl rand -base64 32 + +# Encryption key +openssl rand -hex 32 +``` + +### Production Checklist + +- [ ] Use strong, randomly generated keys +- [ ] Never commit `.env` to version control +- [ ] Run behind reverse proxy (nginx, Traefik) +- [ ] Enable HTTPS/TLS +- [ ] Restrict database network access +- [ ] Set resource limits +- [ ] Enable logging and monitoring +- [ ] Use specific image tags (not `latest`) +- [ ] Regular security updates +- [ ] Backup database regularly + +## Verification + +### Check Service Status + +```bash +# Check if containers are running +docker-compose ps + +# Expected: both services "running" or "healthy" +``` + +### Test API Access + +```bash +# Get OpenAPI documentation +curl http://localhost:8880/docs + +# Should return HTML page +``` + +### Test Database Connection + +```bash +# Connect to PostgreSQL +docker-compose exec postgres psql -U postgres -d dhamps_vdb + +# Check pgvector extension +\dx + +# Should show vector extension +``` + +### Create Test User + +```bash +curl -X POST http://localhost:8880/v1/users \ + -H "Authorization: Bearer YOUR_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "user_handle": "testuser", + "full_name": "Test User" + }' +``` + +## Troubleshooting + +### Container Won't Start + +Check logs: +```bash +docker-compose logs dhamps-vdb +``` + +Common issues: +- Missing `SERVICE_ADMINKEY` or `ENCRYPTION_KEY` +- Database connection failure +- Port already in use + +### Database Connection Errors + +```bash +# Check postgres health +docker-compose ps + +# Check database logs +docker-compose logs postgres + +# Test connection +docker-compose exec postgres psql -U postgres -d dhamps_vdb -c "SELECT 1;" +``` + +### Can't Connect to API + +```bash +# Check if container is running +docker ps + +# Check port mapping +docker port dhamps-vdb + +# Test from inside container +docker-compose exec dhamps-vdb wget -O- http://localhost:8880/docs + +# Test from host +curl http://localhost:8880/docs +``` + +### Permission Issues + +The container runs as non-root user `appuser` (UID 1000). If you have permission errors: + +```bash +# Check volume permissions +docker volume inspect dhamps-vdb_postgres_data +``` + +### Reset Everything + +```bash +# Stop and remove everything +docker-compose down -v + +# Remove images +docker rmi dhamps-vdb:latest +docker rmi pgvector/pgvector:0.7.4-pg16 + +# Start fresh +docker-compose up -d --build +``` + +### Build Failures + +If Docker build fails with network errors: + +```bash +# Try with host network +docker build --network=host -t dhamps-vdb:latest . +``` + +## Advanced Configuration + +### Resource Limits + +Add to `docker-compose.yml`: + +```yaml +services: + dhamps-vdb: + deploy: + resources: + limits: + cpus: '2' + memory: 2G + reservations: + cpus: '0.5' + memory: 512M +``` + +### Health Checks + +The Dockerfile includes a health check: + +```dockerfile +HEALTHCHECK --interval=30s --timeout=3s \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8880/ || exit 1 +``` + +View health status: + +```bash +docker inspect --format='{{.State.Health.Status}}' dhamps-vdb +``` + +### Custom Dockerfile Builds + +You can customize the build: + +```bash +docker build \ + --build-arg GO_VERSION=1.24 \ + -t dhamps-vdb:custom . +``` + +## External Database Setup + +If using an external PostgreSQL database: + +### Prepare Database + +```sql +-- Create database +CREATE DATABASE dhamps_vdb; + +-- Create user +CREATE USER dhamps_user WITH PASSWORD 'secure_password'; + +-- Grant privileges +GRANT ALL PRIVILEGES ON DATABASE dhamps_vdb TO dhamps_user; + +-- Connect to database +\c dhamps_vdb + +-- Grant schema permissions +GRANT ALL ON SCHEMA public TO dhamps_user; + +-- Enable pgvector +CREATE EXTENSION IF NOT EXISTS vector; +``` + +### Configure dhamps-vdb + +Update `.env`: + +```bash +SERVICE_DBHOST=external-db.example.com +SERVICE_DBPORT=5432 +SERVICE_DBUSER=dhamps_user +SERVICE_DBPASSWORD=secure_password +SERVICE_DBNAME=dhamps_vdb +``` + +Then run only the dhamps-vdb service or use a standalone container. + +## Next Steps + +After successful deployment: + +1. [Configure the service](../configuration/) +2. [Create your first user](../quick-start/) +3. [Set up a project](../first-project/) +4. Review [security best practices](../../deployment/security/) diff --git a/docs/content/getting-started/first-project.md b/docs/content/getting-started/first-project.md new file mode 100644 index 0000000..a29c571 --- /dev/null +++ b/docs/content/getting-started/first-project.md @@ -0,0 +1,415 @@ +--- +title: "First Project" +weight: 4 +--- + +# First Project + +Step-by-step guide to creating your first complete project in dhamps-vdb. + +## Overview + +This guide walks you through creating a complete RAG (Retrieval Augmented Generation) workflow: + +1. Set up authentication +2. Configure an LLM service +3. Create a project with metadata validation +4. Upload document embeddings +5. Search for similar documents +6. Share your project with collaborators + +## Step 1: Authentication Setup + +### Get Your API Key + +If you're an admin, create your first user: + +```bash +curl -X POST http://localhost:8880/v1/users \ + -H "Authorization: Bearer YOUR_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "user_handle": "researcher1", + "name": "Research User", + "email": "researcher@example.com" + }' +``` + +Save the returned `vdb_key` to a variable: + +```bash +export USER_KEY="your-returned-vdb-key" +``` + +### Verify Authentication + +Test your API key: + +```bash +curl -X GET http://localhost:8880/v1/users/researcher1 \ + -H "Authorization: Bearer $USER_KEY" +``` + +## Step 2: Configure LLM Service + +### Option A: Use System Definition + +List available system definitions: + +```bash +curl -X GET http://localhost:8880/v1/llm-services/_system \ + -H "Authorization: Bearer $USER_KEY" +``` + +Create an instance from a system definition: + +```bash +curl -X PUT http://localhost:8880/v1/llm-services/researcher1/my-embeddings \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "definition_owner": "_system", + "definition_handle": "openai-large", + "description": "My OpenAI embeddings instance", + "api_key_encrypted": "sk-proj-your-openai-api-key" + }' +``` + +### Option B: Create Custom Instance + +Create a standalone instance with custom configuration: + +```bash +curl -X PUT http://localhost:8880/v1/llm-services/researcher1/custom-embeddings \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "endpoint": "https://api.openai.com/v1/embeddings", + "api_standard": "openai", + "model": "text-embedding-3-small", + "dimensions": 1536, + "description": "Custom OpenAI small embeddings", + "api_key_encrypted": "sk-proj-your-api-key" + }' +``` + +## Step 3: Create Project with Metadata Schema + +Define a metadata schema to ensure consistent document metadata: + +```bash +curl -X POST http://localhost:8880/v1/projects/researcher1 \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "literature-analysis", + "description": "Literary texts for research analysis", + "instance_owner": "researcher1", + "instance_handle": "my-embeddings", + "metadataScheme": "{\"type\":\"object\",\"properties\":{\"author\":{\"type\":\"string\"},\"title\":{\"type\":\"string\"},\"year\":{\"type\":\"integer\"},\"genre\":{\"type\":\"string\",\"enum\":[\"poetry\",\"prose\",\"drama\"]},\"language\":{\"type\":\"string\"}},\"required\":[\"author\",\"title\",\"year\"]}" + }' +``` + +This schema requires `author`, `title`, and `year` fields, with optional `genre` and `language` fields. + +## Step 4: Upload Document Embeddings + +### Prepare Your Data + +Create a file `embeddings.json` with your document embeddings: + +```json +{ + "embeddings": [ + { + "text_id": "hamlet-act1-scene1", + "instance_handle": "my-embeddings", + "text": "Who's there? Nay, answer me: stand, and unfold yourself.", + "vector": [0.023, -0.015, 0.087, ...], + "vector_dim": 3072, + "metadata": { + "author": "William Shakespeare", + "title": "Hamlet", + "year": 1603, + "genre": "drama", + "language": "English" + } + }, + { + "text_id": "paradise-lost-book1-line1", + "instance_handle": "my-embeddings", + "text": "Of Man's first disobedience, and the fruit...", + "vector": [0.045, -0.032, 0.091, ...], + "vector_dim": 3072, + "metadata": { + "author": "John Milton", + "title": "Paradise Lost", + "year": 1667, + "genre": "poetry", + "language": "English" + } + } + ] +} +``` + +### Upload Embeddings + +```bash +curl -X POST http://localhost:8880/v1/embeddings/researcher1/literature-analysis \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d @embeddings.json +``` + +### Verify Upload + +List all embeddings: + +```bash +curl -X GET "http://localhost:8880/v1/embeddings/researcher1/literature-analysis?limit=10" \ + -H "Authorization: Bearer $USER_KEY" +``` + +Get a specific embedding: + +```bash +curl -X GET http://localhost:8880/v1/embeddings/researcher1/literature-analysis/hamlet-act1-scene1 \ + -H "Authorization: Bearer $USER_KEY" +``` + +## Step 5: Search Similar Documents + +### Basic Similarity Search + +Find passages similar to Hamlet Act 1: + +```bash +curl -X GET "http://localhost:8880/v1/similars/researcher1/literature-analysis/hamlet-act1-scene1?count=5&threshold=0.7" \ + -H "Authorization: Bearer $USER_KEY" +``` + +**Response:** + +```json +{ + "user_handle": "researcher1", + "project_handle": "literature-analysis", + "results": [ + { + "id": "hamlet-act2-scene1", + "similarity": 0.89 + }, + { + "id": "macbeth-act1-scene3", + "similarity": 0.82 + }, + { + "id": "othello-act3-scene3", + "similarity": 0.76 + } + ] +} +``` + +### Search with Metadata Filtering + +Exclude passages from the same work: + +```bash +curl -X GET "http://localhost:8880/v1/similars/researcher1/literature-analysis/hamlet-act1-scene1?count=5&metadata_path=title&metadata_value=Hamlet" \ + -H "Authorization: Bearer $USER_KEY" +``` + +This excludes all documents where `metadata.title` equals "Hamlet". + +### Search with Raw Embeddings + +Search using a new embedding without storing it: + +```bash +curl -X POST "http://localhost:8880/v1/similars/researcher1/literature-analysis?count=5&threshold=0.7" \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "vector": [0.034, -0.021, 0.092, ...] + }' +``` + +## Step 6: Share Your Project + +### Share with Collaborators + +Grant read-only access to another user: + +```bash +curl -X POST http://localhost:8880/v1/projects/researcher1/literature-analysis/share \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "share_with_handle": "colleague1", + "role": "reader" + }' +``` + +Grant edit access: + +```bash +curl -X POST http://localhost:8880/v1/projects/researcher1/literature-analysis/share \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "share_with_handle": "colleague2", + "role": "editor" + }' +``` + +### Make Project Public + +Enable public read access (no authentication required): + +```bash +curl -X PATCH http://localhost:8880/v1/projects/researcher1/literature-analysis \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "public_read": true + }' +``` + +Now anyone can read embeddings and search without authentication: + +```bash +# No Authorization header needed +curl -X GET http://localhost:8880/v1/embeddings/researcher1/literature-analysis/hamlet-act1-scene1 +``` + +### View Shared Users + +List all users with access to your project: + +```bash +curl -X GET http://localhost:8880/v1/projects/researcher1/literature-analysis/shared-with \ + -H "Authorization: Bearer $USER_KEY" +``` + +## Step 7: Manage Your Project + +### Update Project Description + +```bash +curl -X PATCH http://localhost:8880/v1/projects/researcher1/literature-analysis \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "description": "Updated: Shakespearean and Renaissance literature analysis" + }' +``` + +### Update Metadata Schema + +```bash +curl -X PATCH http://localhost:8880/v1/projects/researcher1/literature-analysis \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "metadataScheme": "{\"type\":\"object\",\"properties\":{\"author\":{\"type\":\"string\"},\"title\":{\"type\":\"string\"},\"year\":{\"type\":\"integer\"},\"genre\":{\"type\":\"string\"},\"language\":{\"type\":\"string\"},\"act\":{\"type\":\"integer\"},\"scene\":{\"type\":\"integer\"}},\"required\":[\"author\",\"title\",\"year\"]}" + }' +``` + +### Delete Specific Embeddings + +```bash +curl -X DELETE http://localhost:8880/v1/embeddings/researcher1/literature-analysis/hamlet-act1-scene1 \ + -H "Authorization: Bearer $USER_KEY" +``` + +### Delete All Embeddings + +```bash +curl -X DELETE http://localhost:8880/v1/embeddings/researcher1/literature-analysis \ + -H "Authorization: Bearer $USER_KEY" +``` + +## Common Patterns + +### Batch Upload Script + +```bash +#!/bin/bash + +USER_KEY="your-vdb-key" +PROJECT="researcher1/literature-analysis" +API_URL="http://localhost:8880" + +# Process multiple files +for file in data/*.json; do + echo "Uploading $file..." + curl -X POST "$API_URL/v1/embeddings/$PROJECT" \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d @"$file" +done +``` + +### Search and Filter Workflow + +```bash +# 1. Find similar documents +SIMILAR=$(curl -s -X GET "$API_URL/v1/similars/$PROJECT/doc1?count=20" \ + -H "Authorization: Bearer $USER_KEY") + +# 2. Extract IDs +IDS=$(echo $SIMILAR | jq -r '.results[].id') + +# 3. Retrieve full embeddings for similar documents +for id in $IDS; do + curl -X GET "$API_URL/v1/embeddings/$PROJECT/$id" \ + -H "Authorization: Bearer $USER_KEY" +done +``` + +## Troubleshooting + +### Validation Errors + +If metadata validation fails: + +```json +{ + "title": "Bad Request", + "status": 400, + "detail": "metadata validation failed for text_id 'doc1': year is required" +} +``` + +Check your metadata schema and ensure all required fields are present. + +### Dimension Mismatches + +If vector dimensions don't match: + +```json +{ + "title": "Bad Request", + "status": 400, + "detail": "dimension validation failed: expected 3072 dimensions, got 1536" +} +``` + +Verify your LLM service configuration and embedding dimensions. + +### Authentication Errors + +If you get 401 Unauthorized: + +- Check your API key is correct +- Ensure `Authorization: Bearer` prefix is included +- Verify the user owns the resource or has been granted access + +## Next Steps + +- [Learn about metadata validation](../guides/metadata-validation/) +- [Explore batch operations](../guides/batch-operations/) +- [Understand similarity search](../concepts/similarity-search/) +- [Review API documentation](../api/) diff --git a/docs/content/getting-started/installation.md b/docs/content/getting-started/installation.md new file mode 100644 index 0000000..ae9bcb1 --- /dev/null +++ b/docs/content/getting-started/installation.md @@ -0,0 +1,129 @@ +--- +title: "Installation" +weight: 1 +--- + +# Installation + +Install dhamps-vdb by compiling from source. + +## Prerequisites + +- **Go 1.21 or later** +- **PostgreSQL 11+** with pgvector extension +- **sqlc** for code generation + +## Quick Install + +```bash +# Clone the repository +git clone https://github.com/mpilhlt/dhamps-vdb.git +cd dhamps-vdb + +# Install dependencies and generate code +go get ./... +sqlc generate --no-remote + +# Build the binary +go build -o build/dhamps-vdb main.go +``` + +## Detailed Steps + +### 1. Install Dependencies + +Download all Go module dependencies: + +```bash +go get ./... +``` + +### 2. Generate Database Code + +Generate type-safe database queries using sqlc: + +```bash +sqlc generate --no-remote +``` + +This creates Go code from SQL queries in `internal/database/queries/`. + +### 3. Build the Application + +Compile the application: + +```bash +go build -o build/dhamps-vdb main.go +``` + +The binary will be created at `build/dhamps-vdb`. + +## Running Without Building + +You can run the application directly without building a binary: + +```bash +go run main.go +``` + +This is useful during development but slower than running a pre-built binary. + +## Verify Installation + +Check that the binary was created successfully: + +```bash +./build/dhamps-vdb --help +``` + +You should see the available command-line options. + +## Next Steps + +After installation, you need to: + +1. [Set up the database](../deployment/database/) +2. [Configure environment variables](configuration/) +3. [Run the service](quick-start/) + +## System Requirements + +- **Memory**: Minimum 512MB RAM (2GB+ recommended for production) +- **Disk**: Minimal (< 50MB for binary, database size varies) +- **CPU**: Any modern CPU (multi-core recommended for concurrent requests) + +## Troubleshooting + +### sqlc Command Not Found + +Install sqlc: + +```bash +go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest +``` + +Make sure `$GOPATH/bin` is in your PATH. + +### Build Errors + +Ensure you're using Go 1.21 or later: + +```bash +go version +``` + +Clean the build cache if you encounter issues: + +```bash +go clean -cache +go build -o build/dhamps-vdb main.go +``` + +### Missing Dependencies + +Force update all dependencies: + +```bash +go mod download +go get -u ./... +``` diff --git a/docs/content/getting-started/quick-start.md b/docs/content/getting-started/quick-start.md new file mode 100644 index 0000000..5237db1 --- /dev/null +++ b/docs/content/getting-started/quick-start.md @@ -0,0 +1,292 @@ +--- +title: "Quick Start" +weight: 3 +--- + +# Quick Start + +Complete walkthrough from installation to searching similar documents using curl. + +## Prerequisites + +- dhamps-vdb installed and running +- Admin API key configured +- PostgreSQL with pgvector ready + +## 1. Create a User + +Create a new user with the admin API key: + +```bash +curl -X POST http://localhost:8880/v1/users \ + -H "Authorization: Bearer YOUR_ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "user_handle": "alice", + "name": "Alice Smith", + "email": "alice@example.com" + }' +``` + +**Response:** + +```json +{ + "user_handle": "alice", + "name": "Alice Smith", + "email": "alice@example.com", + "vdb_key": "024v2013621509245f2e24...", + "created_at": "2024-01-15T10:30:00Z" +} +``` + +**Save the `vdb_key`** - it cannot be recovered later. + +## 2. Create an LLM Service Instance + +Create an LLM service configuration: + +```bash +curl -X PUT http://localhost:8880/v1/llm-services/alice/my-openai \ + -H "Authorization: Bearer alice_vdb_key" \ + -H "Content-Type: application/json" \ + -d '{ + "endpoint": "https://api.openai.com/v1/embeddings", + "api_standard": "openai", + "model": "text-embedding-3-large", + "dimensions": 3072, + "description": "OpenAI large embeddings", + "api_key_encrypted": "sk-proj-your-openai-key" + }' +``` + +**Response:** + +```json +{ + "instance_id": 1, + "instance_handle": "my-openai", + "owner": "alice", + "endpoint": "https://api.openai.com/v1/embeddings", + "model": "text-embedding-3-large", + "dimensions": 3072 +} +``` + +## 3. Create a Project + +Create a project to organize your embeddings: + +```bash +curl -X POST http://localhost:8880/v1/projects/alice \ + -H "Authorization: Bearer alice_vdb_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "research-docs", + "description": "Research document embeddings", + "instance_owner": "alice", + "instance_handle": "my-openai" + }' +``` + +**Response:** + +```json +{ + "project_id": 1, + "project_handle": "research-docs", + "owner": "alice", + "description": "Research document embeddings", + "instance_id": 1, + "created_at": "2024-01-15T10:35:00Z" +} +``` + +## 4. Upload Embeddings + +Upload document embeddings to your project: + +```bash +curl -X POST http://localhost:8880/v1/embeddings/alice/research-docs \ + -H "Authorization: Bearer alice_vdb_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [ + { + "text_id": "doc1", + "instance_handle": "my-openai", + "text": "Introduction to machine learning", + "vector": [0.1, 0.2, 0.3, ..., 0.5], + "vector_dim": 3072, + "metadata": { + "title": "ML Intro", + "author": "Alice", + "year": 2024 + } + }, + { + "text_id": "doc2", + "instance_handle": "my-openai", + "text": "Deep learning fundamentals", + "vector": [0.15, 0.25, 0.35, ..., 0.55], + "vector_dim": 3072, + "metadata": { + "title": "DL Fundamentals", + "author": "Bob", + "year": 2024 + } + } + ] + }' +``` + +**Response:** + +```json +{ + "message": "2 embeddings uploaded successfully" +} +``` + +## 5. Search for Similar Documents + +### Option A: Search Using Stored Document + +Find documents similar to an already-stored document: + +```bash +curl -X GET "http://localhost:8880/v1/similars/alice/research-docs/doc1?count=5&threshold=0.7" \ + -H "Authorization: Bearer alice_vdb_key" +``` + +**Response:** + +```json +{ + "user_handle": "alice", + "project_handle": "research-docs", + "results": [ + { + "id": "doc2", + "similarity": 0.92 + }, + { + "id": "doc5", + "similarity": 0.85 + } + ] +} +``` + +### Option B: Search Using Raw Embeddings + +Search without storing the query embedding: + +```bash +curl -X POST "http://localhost:8880/v1/similars/alice/research-docs?count=5&threshold=0.7" \ + -H "Authorization: Bearer alice_vdb_key" \ + -H "Content-Type: application/json" \ + -d '{ + "vector": [0.12, 0.22, 0.32, ..., 0.52] + }' +``` + +## 6. Filter by Metadata + +Exclude documents from a specific author when searching: + +```bash +curl -X GET "http://localhost:8880/v1/similars/alice/research-docs/doc1?count=5&metadata_path=author&metadata_value=Alice" \ + -H "Authorization: Bearer alice_vdb_key" +``` + +This excludes all documents where `metadata.author` equals "Alice". + +## 7. Retrieve Embeddings + +Get all embeddings in your project: + +```bash +curl -X GET "http://localhost:8880/v1/embeddings/alice/research-docs?limit=10&offset=0" \ + -H "Authorization: Bearer alice_vdb_key" +``` + +Get a specific embedding: + +```bash +curl -X GET http://localhost:8880/v1/embeddings/alice/research-docs/doc1 \ + -H "Authorization: Bearer alice_vdb_key" +``` + +## Complete Workflow Example + +Here's a complete script to get started: + +```bash +#!/bin/bash + +# Configuration +API_URL="http://localhost:8880" +ADMIN_KEY="your-admin-key" + +# 1. Create user +USER_RESPONSE=$(curl -s -X POST "$API_URL/v1/users" \ + -H "Authorization: Bearer $ADMIN_KEY" \ + -H "Content-Type: application/json" \ + -d '{"user_handle":"alice","name":"Alice Smith","email":"alice@example.com"}') + +USER_KEY=$(echo $USER_RESPONSE | jq -r '.vdb_key') +echo "User created with key: $USER_KEY" + +# 2. Create LLM service instance +curl -X PUT "$API_URL/v1/llm-services/alice/my-openai" \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "endpoint": "https://api.openai.com/v1/embeddings", + "api_standard": "openai", + "model": "text-embedding-3-large", + "dimensions": 3072, + "api_key_encrypted": "sk-your-key" + }' + +# 3. Create project +curl -X POST "$API_URL/v1/projects/alice" \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "research-docs", + "description": "Research documents", + "instance_owner": "alice", + "instance_handle": "my-openai" + }' + +# 4. Upload embeddings +curl -X POST "$API_URL/v1/embeddings/alice/research-docs" \ + -H "Authorization: Bearer $USER_KEY" \ + -H "Content-Type: application/json" \ + -d @embeddings.json + +# 5. Search similar +curl -X GET "$API_URL/v1/similars/alice/research-docs/doc1?count=5" \ + -H "Authorization: Bearer $USER_KEY" + +echo "Setup complete!" +``` + +## API Documentation + +For complete API documentation, visit: + +```bash +curl http://localhost:8880/docs +``` + +Or open http://localhost:8880/docs in your browser. + +## Next Steps + +- [Learn about projects](../concepts/projects/) +- [Understand embeddings](../concepts/embeddings/) +- [Explore sharing projects](../guides/project-sharing/) +- [Set up metadata validation](../guides/metadata-validation/) diff --git a/docs/content/guides/_index.md b/docs/content/guides/_index.md new file mode 100644 index 0000000..9b042cf --- /dev/null +++ b/docs/content/guides/_index.md @@ -0,0 +1,23 @@ +--- +title: "Guides" +weight: 3 +--- + +# User Guides + +Step-by-step guides for common tasks and workflows with dhamps-vdb. + +## Available Guides + +This section contains practical guides for using dhamps-vdb in real-world scenarios: + +- **RAG Workflows** - Implement Retrieval Augmented Generation +- **Project Sharing** - Collaborate with other users +- **Public Projects** - Enable unauthenticated access +- **Ownership Transfer** - Move projects between users +- **Metadata Validation** - Ensure data quality with schemas +- **Metadata Filtering** - Exclude documents from similarity search +- **Batch Operations** - Work with multiple embeddings efficiently +- **Instance Management** - Configure LLM service instances + +These guides complement the API reference by providing context and best practices for common use cases. diff --git a/docs/content/guides/batch-operations.md b/docs/content/guides/batch-operations.md new file mode 100644 index 0000000..aaac38c --- /dev/null +++ b/docs/content/guides/batch-operations.md @@ -0,0 +1,718 @@ +--- +title: "Batch Operations Guide" +weight: 7 +--- + +# Batch Operations Guide + +This guide explains how to efficiently upload multiple embeddings, manage large datasets, and implement best practices for batch operations in dhamps-vdb. + +## Overview + +For production workloads and large datasets, efficient batch operations are essential. This guide covers: +- Uploading multiple embeddings in a single request +- Pagination strategies for large result sets +- Best practices for performance and reliability +- Error handling in batch operations + +## Batch Upload Basics + +### Single Request with Multiple Embeddings + +Upload multiple embeddings in one API call using the `embeddings` array: + +```bash +curl -X POST "https://api.example.com/v1/embeddings/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [ + { + "text_id": "doc001", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3, ...], + "vector_dim": 3072, + "text": "First document content", + "metadata": {"category": "science"} + }, + { + "text_id": "doc002", + "instance_handle": "openai-large", + "vector": [0.11, 0.21, 0.31, ...], + "vector_dim": 3072, + "text": "Second document content", + "metadata": {"category": "history"} + }, + { + "text_id": "doc003", + "instance_handle": "openai-large", + "vector": [0.12, 0.22, 0.32, ...], + "vector_dim": 3072, + "text": "Third document content", + "metadata": {"category": "literature"} + } + ] + }' +``` + +**Response:** +```json +{ + "message": "Embeddings uploaded successfully", + "count": 3 +} +``` + +## Optimal Batch Sizes + +### Recommended Batch Sizes + +Based on typical embedding dimensions and network constraints: + +| Embedding Dimensions | Recommended Batch Size | Maximum Batch Size | +|---------------------|------------------------|-------------------| +| 384 (small models) | 500-1000 | 2000 | +| 768 (BERT-base) | 300-500 | 1000 | +| 1536 (OpenAI small) | 100-300 | 500 | +| 3072 (OpenAI large) | 50-100 | 200 | + +**Factors to consider:** +- Network bandwidth and latency +- API gateway timeout limits +- Database transaction size +- Memory constraints +- Client-side serialization limits + +### Finding Your Optimal Batch Size + +Test different batch sizes to find the sweet spot: + +```python +import time +import requests + +def test_batch_size(embeddings, batch_size): + """Test upload performance with given batch size""" + start_time = time.time() + + for i in range(0, len(embeddings), batch_size): + batch = embeddings[i:i+batch_size] + response = requests.post( + "https://api.example.com/v1/embeddings/alice/my-project", + headers={ + "Authorization": "Bearer alice_api_key", + "Content-Type": "application/json" + }, + json={"embeddings": batch} + ) + response.raise_for_status() + + elapsed = time.time() - start_time + throughput = len(embeddings) / elapsed + print(f"Batch size {batch_size}: {throughput:.1f} embeddings/sec") + +# Test different batch sizes +for size in [50, 100, 200, 500]: + test_batch_size(my_embeddings, size) +``` + +## Pagination for Large Datasets + +### Retrieving All Embeddings with Pagination + +Use `limit` and `offset` parameters to paginate through large result sets: + +```bash +# Get first page (embeddings 0-99) +curl -X GET "https://api.example.com/v1/embeddings/alice/my-project?limit=100&offset=0" \ + -H "Authorization: Bearer alice_api_key" + +# Get second page (embeddings 100-199) +curl -X GET "https://api.example.com/v1/embeddings/alice/my-project?limit=100&offset=100" \ + -H "Authorization: Bearer alice_api_key" + +# Get third page (embeddings 200-299) +curl -X GET "https://api.example.com/v1/embeddings/alice/my-project?limit=100&offset=200" \ + -H "Authorization: Bearer alice_api_key" +``` + +### Pagination Best Practices + +**Default Values:** +- `limit`: 10 (if not specified) +- `offset`: 0 (if not specified) +- Maximum `limit`: 200 + +**Example: Download Entire Project** + +```python +import requests + +def download_all_embeddings(owner, project): + """Download all embeddings from a project""" + all_embeddings = [] + offset = 0 + limit = 100 + + while True: + response = requests.get( + f"https://api.example.com/v1/embeddings/{owner}/{project}", + headers={"Authorization": "Bearer api_key"}, + params={"limit": limit, "offset": offset} + ) + response.raise_for_status() + + batch = response.json()['embeddings'] + if not batch: + break # No more results + + all_embeddings.extend(batch) + offset += len(batch) + + print(f"Downloaded {len(all_embeddings)} embeddings...") + + return all_embeddings + +# Usage +embeddings = download_all_embeddings("alice", "my-project") +print(f"Total: {len(embeddings)} embeddings") +``` + +## Efficient Batch Upload Strategies + +### Strategy 1: Simple Sequential Upload + +Good for small to medium datasets (< 10,000 embeddings): + +```python +def upload_sequential(embeddings, batch_size=100): + """Upload embeddings sequentially in batches""" + for i in range(0, len(embeddings), batch_size): + batch = embeddings[i:i+batch_size] + response = requests.post( + "https://api.example.com/v1/embeddings/alice/my-project", + headers={ + "Authorization": "Bearer alice_api_key", + "Content-Type": "application/json" + }, + json={"embeddings": batch} + ) + response.raise_for_status() + print(f"Uploaded batch {i//batch_size + 1}, total: {i+len(batch)}") +``` + +### Strategy 2: Parallel Upload with Threading + +Good for larger datasets with stable network: + +```python +import concurrent.futures +import requests + +def upload_batch(batch, batch_num): + """Upload a single batch""" + try: + response = requests.post( + "https://api.example.com/v1/embeddings/alice/my-project", + headers={ + "Authorization": "Bearer alice_api_key", + "Content-Type": "application/json" + }, + json={"embeddings": batch}, + timeout=60 + ) + response.raise_for_status() + return batch_num, True, None + except Exception as e: + return batch_num, False, str(e) + +def upload_parallel(embeddings, batch_size=100, max_workers=4): + """Upload embeddings in parallel""" + batches = [ + embeddings[i:i+batch_size] + for i in range(0, len(embeddings), batch_size) + ] + + failed = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = { + executor.submit(upload_batch, batch, i): i + for i, batch in enumerate(batches) + } + + for future in concurrent.futures.as_completed(futures): + batch_num, success, error = future.result() + if success: + print(f"✓ Batch {batch_num+1}/{len(batches)} uploaded") + else: + print(f"✗ Batch {batch_num+1} failed: {error}") + failed.append((batch_num, batches[batch_num])) + + return failed + +# Usage +failed_batches = upload_parallel(my_embeddings, batch_size=100, max_workers=4) +if failed_batches: + print(f"Failed batches: {len(failed_batches)}") +``` + +### Strategy 3: Retry with Exponential Backoff + +Robust strategy for unreliable networks: + +```python +import time +import random + +def upload_with_retry(batch, max_retries=3): + """Upload batch with exponential backoff retry""" + for attempt in range(max_retries): + try: + response = requests.post( + "https://api.example.com/v1/embeddings/alice/my-project", + headers={ + "Authorization": "Bearer alice_api_key", + "Content-Type": "application/json" + }, + json={"embeddings": batch}, + timeout=60 + ) + response.raise_for_status() + return True + except requests.exceptions.RequestException as e: + if attempt < max_retries - 1: + wait = (2 ** attempt) + random.uniform(0, 1) + print(f"Retry attempt {attempt+1} after {wait:.1f}s: {e}") + time.sleep(wait) + else: + print(f"Failed after {max_retries} attempts: {e}") + return False + +def upload_robust(embeddings, batch_size=100): + """Upload with robust error handling""" + failed = [] + for i in range(0, len(embeddings), batch_size): + batch = embeddings[i:i+batch_size] + if not upload_with_retry(batch): + failed.append((i, batch)) + else: + print(f"✓ Uploaded {i+batch_size}/{len(embeddings)}") + + return failed +``` + +## Progress Tracking and Resumability + +### Checkpoint-Based Upload + +For very large datasets, implement checkpointing to resume after failures: + +```python +import json +import os + +class CheckpointUploader: + def __init__(self, checkpoint_file="upload_progress.json"): + self.checkpoint_file = checkpoint_file + self.progress = self.load_progress() + + def load_progress(self): + """Load upload progress from checkpoint file""" + if os.path.exists(self.checkpoint_file): + with open(self.checkpoint_file, 'r') as f: + return json.load(f) + return {"uploaded_count": 0, "failed_batches": []} + + def save_progress(self): + """Save current progress""" + with open(self.checkpoint_file, 'w') as f: + json.dump(self.progress, f) + + def upload(self, embeddings, batch_size=100): + """Upload with checkpointing""" + start_idx = self.progress["uploaded_count"] + + for i in range(start_idx, len(embeddings), batch_size): + batch = embeddings[i:i+batch_size] + + try: + response = requests.post( + "https://api.example.com/v1/embeddings/alice/my-project", + headers={ + "Authorization": "Bearer alice_api_key", + "Content-Type": "application/json" + }, + json={"embeddings": batch}, + timeout=60 + ) + response.raise_for_status() + + self.progress["uploaded_count"] = i + len(batch) + self.save_progress() + print(f"✓ Progress: {self.progress['uploaded_count']}/{len(embeddings)}") + + except Exception as e: + print(f"✗ Failed at index {i}: {e}") + self.progress["failed_batches"].append(i) + self.save_progress() + raise + +# Usage +uploader = CheckpointUploader() +try: + uploader.upload(my_embeddings, batch_size=100) + print("Upload complete!") +except: + print("Upload interrupted. Run again to resume.") +``` + +## Error Handling + +### Validation Errors + +If any embedding in a batch fails validation, the entire batch is rejected: + +```bash +curl -X POST "https://api.example.com/v1/embeddings/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [ + { + "text_id": "doc001", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3], + "vector_dim": 3072, + "metadata": {"author": "Alice"} + }, + { + "text_id": "doc002", + "instance_handle": "openai-large", + "vector": [0.1, 0.2], + "vector_dim": 3072, + "metadata": {"author": "Bob"} + } + ] + }' +``` + +**Error Response:** +```json +{ + "title": "Bad Request", + "status": 400, + "detail": "dimension validation failed: vector length mismatch for text_id 'doc002': actual vector has 2 elements but vector_dim declares 3072" +} +``` + +**Solution:** Validate all embeddings before batching, or handle errors and retry failed items. + +### Pre-Upload Validation + +```python +def validate_embeddings(embeddings, expected_dim): + """Validate embeddings before upload""" + errors = [] + + for i, emb in enumerate(embeddings): + # Check vector length + if len(emb['vector']) != expected_dim: + errors.append(f"Index {i} (text_id: {emb['text_id']}): " + f"vector length {len(emb['vector'])} != {expected_dim}") + + # Check declared dimension + if emb.get('vector_dim') != expected_dim: + errors.append(f"Index {i} (text_id: {emb['text_id']}): " + f"vector_dim {emb.get('vector_dim')} != {expected_dim}") + + # Check required fields + if not emb.get('text_id'): + errors.append(f"Index {i}: missing text_id") + + if not emb.get('instance_handle'): + errors.append(f"Index {i}: missing instance_handle") + + return errors + +# Usage +errors = validate_embeddings(my_embeddings, 3072) +if errors: + print("Validation errors:") + for error in errors: + print(f" - {error}") +else: + print("All embeddings valid, proceeding with upload...") +``` + +## Performance Optimization Tips + +### 1. Minimize Payload Size + +Exclude unnecessary fields: + +```python +# Include text only if needed for retrieval +embeddings_with_text = [ + { + "text_id": doc_id, + "instance_handle": "openai-large", + "vector": vector, + "vector_dim": 3072, + "text": text, # Include if needed + "metadata": metadata + } + for doc_id, vector, text, metadata in documents +] + +# Exclude text if not needed (smaller payload) +embeddings_without_text = [ + { + "text_id": doc_id, + "instance_handle": "openai-large", + "vector": vector, + "vector_dim": 3072, + "metadata": metadata + } + for doc_id, vector, metadata in documents +] +``` + +### 2. Compress Requests + +Use gzip compression for large payloads: + +```python +import gzip +import json + +def upload_compressed(embeddings): + """Upload with gzip compression""" + payload = json.dumps({"embeddings": embeddings}) + compressed = gzip.compress(payload.encode('utf-8')) + + response = requests.post( + "https://api.example.com/v1/embeddings/alice/my-project", + headers={ + "Authorization": "Bearer alice_api_key", + "Content-Type": "application/json", + "Content-Encoding": "gzip" + }, + data=compressed + ) + return response +``` + +### 3. Use Connection Pooling + +Reuse HTTP connections for multiple requests: + +```python +session = requests.Session() +session.headers.update({ + "Authorization": "Bearer alice_api_key", + "Content-Type": "application/json" +}) + +for batch in batches: + response = session.post( + "https://api.example.com/v1/embeddings/alice/my-project", + json={"embeddings": batch} + ) + response.raise_for_status() +``` + +### 4. Monitor Upload Rate + +Track and display upload progress: + +```python +import time + +class ProgressTracker: + def __init__(self, total): + self.total = total + self.uploaded = 0 + self.start_time = time.time() + + def update(self, count): + self.uploaded += count + elapsed = time.time() - self.start_time + rate = self.uploaded / elapsed if elapsed > 0 else 0 + percent = (self.uploaded / self.total) * 100 + eta = (self.total - self.uploaded) / rate if rate > 0 else 0 + + print(f"\rProgress: {self.uploaded}/{self.total} ({percent:.1f}%) " + f"Rate: {rate:.1f} emb/s ETA: {eta:.0f}s", end="") + +# Usage +tracker = ProgressTracker(len(all_embeddings)) +for batch in batches: + upload_batch(batch) + tracker.update(len(batch)) +print() # New line after completion +``` + +## Complete Example: Production-Grade Uploader + +```python +import requests +import time +import json +import logging +from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import List, Dict, Tuple + +class ProductionUploader: + def __init__(self, api_base: str, api_key: str, + owner: str, project: str): + self.api_base = api_base + self.api_key = api_key + self.owner = owner + self.project = project + self.session = requests.Session() + self.session.headers.update({ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + }) + + logging.basicConfig(level=logging.INFO) + self.logger = logging.getLogger(__name__) + + def upload_batch(self, batch: List[Dict], batch_num: int, + max_retries: int = 3) -> Tuple[int, bool, str]: + """Upload a single batch with retry logic""" + url = f"{self.api_base}/v1/embeddings/{self.owner}/{self.project}" + + for attempt in range(max_retries): + try: + response = self.session.post( + url, + json={"embeddings": batch}, + timeout=60 + ) + response.raise_for_status() + return batch_num, True, "" + except Exception as e: + if attempt < max_retries - 1: + wait = 2 ** attempt + self.logger.warning( + f"Batch {batch_num} attempt {attempt+1} failed: {e}. " + f"Retrying in {wait}s..." + ) + time.sleep(wait) + else: + return batch_num, False, str(e) + + def upload(self, embeddings: List[Dict], batch_size: int = 100, + max_workers: int = 4) -> Dict: + """Upload embeddings with parallel processing and progress tracking""" + batches = [ + embeddings[i:i+batch_size] + for i in range(0, len(embeddings), batch_size) + ] + + results = { + "total": len(embeddings), + "uploaded": 0, + "failed": [], + "start_time": time.time() + } + + self.logger.info(f"Uploading {len(embeddings)} embeddings in " + f"{len(batches)} batches...") + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = { + executor.submit(self.upload_batch, batch, i): i + for i, batch in enumerate(batches) + } + + for future in as_completed(futures): + batch_num, success, error = future.result() + + if success: + results["uploaded"] += len(batches[batch_num]) + percent = (results["uploaded"] / results["total"]) * 100 + self.logger.info( + f"✓ Batch {batch_num+1}/{len(batches)} " + f"({percent:.1f}% complete)" + ) + else: + results["failed"].append({ + "batch_num": batch_num, + "error": error, + "embeddings": batches[batch_num] + }) + self.logger.error(f"✗ Batch {batch_num+1} failed: {error}") + + results["elapsed"] = time.time() - results["start_time"] + results["rate"] = results["uploaded"] / results["elapsed"] + + self.logger.info( + f"\nUpload complete: {results['uploaded']}/{results['total']} " + f"in {results['elapsed']:.1f}s ({results['rate']:.1f} emb/s)" + ) + + if results["failed"]: + self.logger.warning(f"Failed batches: {len(results['failed'])}") + + return results + +# Usage +uploader = ProductionUploader( + api_base="https://api.example.com", + api_key="alice_api_key", + owner="alice", + project="my-project" +) + +results = uploader.upload( + embeddings=my_embeddings, + batch_size=100, + max_workers=4 +) + +# Save failed batches for retry +if results["failed"]: + with open("failed_batches.json", "w") as f: + json.dump(results["failed"], f) +``` + +## Best Practices Summary + +1. **Batch Size**: Test to find optimal size (typically 50-500 depending on dimensions) +2. **Parallelism**: Use 2-8 parallel workers for large uploads +3. **Retry Logic**: Implement exponential backoff for network errors +4. **Validation**: Pre-validate embeddings before upload +5. **Progress Tracking**: Monitor upload rate and ETA +6. **Checkpointing**: Save progress for resumable uploads +7. **Error Logging**: Log all errors with sufficient context +8. **Connection Reuse**: Use session objects for connection pooling +9. **Compression**: Use gzip for large payloads +10. **Testing**: Test with small batches before full upload + +## Related Documentation + +- [RAG Workflow Guide](./rag-workflow.md) - Complete RAG implementation +- [Metadata Validation Guide](./metadata-validation.md) - Schema validation +- [Instance Management Guide](./instance-management.md) - Managing LLM instances + +## Troubleshooting + +### Timeout Errors + +**Problem:** Requests timing out with large batches + +**Solution:** Reduce batch size or increase timeout value + +### Memory Issues + +**Problem:** Out of memory when processing large datasets + +**Solution:** Process embeddings in streaming fashion, don't load all into memory + +### Rate Limiting + +**Problem:** Getting rate limited by API + +**Solution:** Reduce parallelism (max_workers) or add delays between requests diff --git a/docs/content/guides/instance-management.md b/docs/content/guides/instance-management.md new file mode 100644 index 0000000..e09a871 --- /dev/null +++ b/docs/content/guides/instance-management.md @@ -0,0 +1,634 @@ +--- +title: "Instance Management Guide" +weight: 8 +--- + +# Instance Management Guide + +This guide explains how to create, configure, and share LLM service instances for generating and managing embeddings. + +## Overview + +LLM service instances define the configuration for connecting to embedding services (like OpenAI, Cohere, or Gemini). Each instance includes: +- API endpoint and credentials +- Model name and version +- Vector dimensions +- API standard (protocol) + +Instances can be created from system templates, user-defined templates, or as standalone configurations. They can also be shared with other users for collaborative work. + +## Instance Architecture + +### System Definitions + +The system provides pre-configured templates for common LLM services: + +```bash +# List available system definitions (no auth required) +curl -X GET "https://api.example.com/v1/llm-service-definitions/_system" +``` + +**Default System Definitions:** +- `openai-large`: OpenAI text-embedding-3-large (3072 dimensions) +- `openai-small`: OpenAI text-embedding-3-small (1536 dimensions) +- `cohere-v4`: Cohere Embed v4 (1536 dimensions) +- `gemini-embedding-001`: Google Gemini embedding-001 (3072 dimensions, default size) + +### User Instances + +Users create instances for their own use. Instances contain: +- Configuration (endpoint, model, dimensions) +- Encrypted API keys (write-only, never returned) +- Optional reference to a definition template + +## Creating LLM Service Instances + +### Option 1: Standalone Instance + +Create an instance by specifying all configuration fields: + +```bash +curl -X PUT "https://api.example.com/v1/llm-services/alice/my-openai" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "instance_handle": "my-openai", + "endpoint": "https://api.openai.com/v1/embeddings", + "api_standard": "openai", + "model": "text-embedding-3-large", + "dimensions": 3072, + "description": "OpenAI large embedding model for research", + "api_key_encrypted": "sk-proj-your-openai-api-key-here" + }' +``` + +**Response:** +```json +{ + "instance_id": 123, + "instance_handle": "my-openai", + "owner": "alice", + "endpoint": "https://api.openai.com/v1/embeddings", + "api_standard": "openai", + "model": "text-embedding-3-large", + "dimensions": 3072, + "description": "OpenAI large embedding model for research" +} +``` + +**Note:** The `api_key_encrypted` field is not returned in the response for security reasons. + +### Option 2: From System Definition + +Create an instance based on a system template (only requires API key): + +```bash +curl -X POST "https://api.example.com/v1/llm-services/alice/my-openai-instance" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "instance_handle": "my-openai-instance", + "definition_owner": "_system", + "definition_handle": "openai-large", + "api_key_encrypted": "sk-proj-your-openai-api-key-here" + }' +``` + +This inherits configuration from the `_system/openai-large` definition and only requires you to provide your API key. + +### Option 3: From User Definition + +Users can create their own definitions as templates: + +```bash +# Step 1: Create a custom definition +curl -X PUT "https://api.example.com/v1/llm-service-definitions/alice/my-custom-config" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "definition_handle": "my-custom-config", + "endpoint": "https://custom-api.example.com/embeddings", + "api_standard": "openai", + "model": "custom-model-v2", + "dimensions": 2048, + "description": "Custom embedding service" + }' + +# Step 2: Create instance from that definition +curl -X POST "https://api.example.com/v1/llm-services/alice/custom-instance" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "instance_handle": "custom-instance", + "definition_owner": "alice", + "definition_handle": "my-custom-config", + "api_key_encrypted": "your-api-key-here" + }' +``` + +## Managing Instances + +### List Your Instances + +Get all instances you own or have access to: + +```bash +curl -X GET "https://api.example.com/v1/llm-services/alice" \ + -H "Authorization: Bearer alice_api_key" +``` + +**Response:** +```json +{ + "owned_instances": [ + { + "instance_id": 123, + "instance_handle": "my-openai", + "endpoint": "https://api.openai.com/v1/embeddings", + "model": "text-embedding-3-large", + "dimensions": 3072 + }, + { + "instance_id": 124, + "instance_handle": "my-cohere", + "endpoint": "https://api.cohere.ai/v1/embed", + "model": "embed-english-v4.0", + "dimensions": 1536 + } + ], + "shared_instances": [ + { + "instance_id": 456, + "instance_handle": "team-openai", + "owner": "bob", + "endpoint": "https://api.openai.com/v1/embeddings", + "model": "text-embedding-3-large", + "dimensions": 3072, + "role": "reader" + } + ] +} +``` + +### Get Instance Details + +Retrieve details for a specific instance: + +```bash +curl -X GET "https://api.example.com/v1/llm-services/alice/my-openai" \ + -H "Authorization: Bearer alice_api_key" +``` + +### Update Instance + +Update instance configuration (owner only): + +```bash +curl -X PATCH "https://api.example.com/v1/llm-services/alice/my-openai" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "description": "Updated description", + "api_key_encrypted": "sk-proj-new-api-key-here" + }' +``` + +### Delete Instance + +Delete an instance (owner only): + +```bash +curl -X DELETE "https://api.example.com/v1/llm-services/alice/my-openai" \ + -H "Authorization: Bearer alice_api_key" +``` + +**Note:** Cannot delete instances that are used by existing projects. + +## API Key Encryption + +### How Encryption Works + +API keys are encrypted using AES-256-GCM encryption: + +1. **Encryption Key Source**: `ENCRYPTION_KEY` environment variable +2. **Key Derivation**: SHA256 hash of the environment variable (ensures 32-byte key) +3. **Algorithm**: AES-256-GCM (authenticated encryption) +4. **Storage**: Encrypted bytes stored in `api_key_encrypted` column + +### Setting Up Encryption + +Add to your environment configuration: + +```bash +# .env file +ENCRYPTION_KEY="your-secure-random-32-character-key-or-longer" +``` + +**Important Security Notes:** +- Keep this key secure and backed up +- Losing the key means losing access to encrypted API keys +- Use a strong, random string (32+ characters) +- Never commit the key to version control +- Rotate the key periodically (requires re-encrypting all API keys) + +### API Key Security + +API keys are **write-only** in the API: + +```bash +# Upload API key (works) +curl -X PUT "https://api.example.com/v1/llm-services/alice/my-instance" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"api_key_encrypted": "sk-..."}' + +# Retrieve instance (API key NOT returned) +curl -X GET "https://api.example.com/v1/llm-services/alice/my-instance" \ + -H "Authorization: Bearer alice_api_key" + +# Response does NOT include api_key_encrypted field +{ + "instance_id": 123, + "instance_handle": "my-instance", + "endpoint": "https://api.openai.com/v1/embeddings", + "model": "text-embedding-3-large", + "dimensions": 3072 + // No api_key_encrypted field! +} +``` + +This protects API keys from: +- Accidental exposure in logs +- Unauthorized access via shared instances +- Client-side data breaches + +## Instance Sharing + +### Share an Instance + +Grant another user access to your instance: + +```bash +curl -X POST "https://api.example.com/v1/llm-services/alice/my-openai/share" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "share_with_handle": "bob", + "role": "reader" + }' +``` + +**Roles:** +- `reader`: Can use the instance in projects (read-only) +- `editor`: Can use the instance (currently same as reader) +- `owner`: Full control (only one owner, the creator) + +### List Shared Users + +See who has access to your instance: + +```bash +curl -X GET "https://api.example.com/v1/llm-services/alice/my-openai/shared-with" \ + -H "Authorization: Bearer alice_api_key" +``` + +**Response:** +```json +{ + "instance_handle": "my-openai", + "owner": "alice", + "shared_with": [ + {"user_handle": "bob", "role": "reader"}, + {"user_handle": "charlie", "role": "reader"} + ] +} +``` + +### Unshare an Instance + +Revoke access: + +```bash +curl -X DELETE "https://api.example.com/v1/llm-services/alice/my-openai/share/bob" \ + -H "Authorization: Bearer alice_api_key" +``` + +### Using Shared Instances + +Bob can reference Alice's shared instance in his projects: + +```bash +# Bob creates a project using Alice's instance +curl -X PUT "https://api.example.com/v1/projects/bob/my-project" \ + -H "Authorization: Bearer bob_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "my-project", + "description": "Using shared instance", + "instance_owner": "alice", + "instance_handle": "my-openai" + }' + +# Bob uploads embeddings using the shared instance +curl -X POST "https://api.example.com/v1/embeddings/bob/my-project" \ + -H "Authorization: Bearer bob_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "doc001", + "instance_handle": "alice/my-openai", + "vector": [0.1, 0.2, ...], + "vector_dim": 3072 + }] + }' +``` + +**Important:** Bob can use the instance but cannot see Alice's API key. + +## Instance Sharing Patterns + +### Team Shared Instance + +A team lead creates and shares an instance for the team: + +```bash +# Team lead creates instance +curl -X PUT "https://api.example.com/v1/llm-services/team_lead/team-openai" \ + -H "Authorization: Bearer team_lead_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "instance_handle": "team-openai", + "endpoint": "https://api.openai.com/v1/embeddings", + "api_standard": "openai", + "model": "text-embedding-3-large", + "dimensions": 3072, + "api_key_encrypted": "sk-proj-team-api-key" + }' + +# Share with team members +for member in alice bob charlie; do + curl -X POST "https://api.example.com/v1/llm-services/team_lead/team-openai/share" \ + -H "Authorization: Bearer team_lead_api_key" \ + -H "Content-Type: application/json" \ + -d "{\"share_with_handle\": \"$member\", \"role\": \"reader\"}" +done +``` + +### Organization-Wide Instance + +Create a shared instance for organization-wide use: + +```bash +# Organization admin creates instance +curl -X PUT "https://api.example.com/v1/llm-services/org_admin/org-embeddings" \ + -H "Authorization: Bearer org_admin_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "instance_handle": "org-embeddings", + "endpoint": "https://api.openai.com/v1/embeddings", + "api_standard": "openai", + "model": "text-embedding-3-large", + "dimensions": 3072, + "description": "Organization-wide embedding service", + "api_key_encrypted": "sk-proj-org-api-key" + }' +``` + +### Per-Project Instance + +Each project maintainer creates their own instance: + +```bash +# Alice creates her own instance for her project +curl -X PUT "https://api.example.com/v1/llm-services/alice/research-embeddings" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "instance_handle": "research-embeddings", + "endpoint": "https://api.openai.com/v1/embeddings", + "api_standard": "openai", + "model": "text-embedding-3-large", + "dimensions": 3072, + "api_key_encrypted": "sk-proj-alice-research-key" + }' +``` + +## Common Configurations + +### OpenAI Configuration + +```json +{ + "instance_handle": "openai-large", + "endpoint": "https://api.openai.com/v1/embeddings", + "api_standard": "openai", + "model": "text-embedding-3-large", + "dimensions": 3072, + "api_key_encrypted": "sk-proj-..." +} +``` + +### Cohere Configuration + +```json +{ + "instance_handle": "cohere-english", + "endpoint": "https://api.cohere.ai/v1/embed", + "api_standard": "cohere", + "model": "embed-english-v4.0", + "dimensions": 1536, + "api_key_encrypted": "your-cohere-api-key" +} +``` + +### Google Gemini Configuration + +```json +{ + "instance_handle": "gemini-embedding", + "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/embedding-001:embedContent", + "api_standard": "gemini", + "model": "embedding-001", + "dimensions": 3072, + "api_key_encrypted": "your-gemini-api-key" +} +``` + +### Custom/Self-Hosted Service + +```json +{ + "instance_handle": "custom-service", + "endpoint": "https://custom-api.example.com/v1/embeddings", + "api_standard": "openai", + "model": "custom-model-v2", + "dimensions": 2048, + "description": "Self-hosted embedding service", + "api_key_encrypted": "custom-auth-token" +} +``` + +## Projects and Instances + +### 1:1 Relationship + +Each project must reference exactly one instance: + +```bash +curl -X PUT "https://api.example.com/v1/projects/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "my-project", + "description": "Project with instance", + "instance_id": 123 + }' +``` + +### Changing Instance + +Update a project to use a different instance: + +```bash +curl -X PATCH "https://api.example.com/v1/projects/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"instance_id": 456}' +``` + +**Note:** Only switch to instances with matching dimensions, or you'll get validation errors on future uploads. + +### Finding Instance for Project + +```bash +curl -X GET "https://api.example.com/v1/projects/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" +``` + +The response includes the `instance_id` field. + +## Best Practices + +### 1. Use System Definitions + +Start with system definitions for common services: + +```bash +# Easiest approach +curl -X POST "https://api.example.com/v1/llm-services/alice/my-instance" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "instance_handle": "my-instance", + "definition_owner": "_system", + "definition_handle": "openai-large", + "api_key_encrypted": "sk-..." + }' +``` + +### 2. Descriptive Instance Names + +Use clear, descriptive names: + +```bash +# Good names +"research-openai-large" +"prod-cohere-english" +"test-gemini-embedding" + +# Avoid generic names +"instance1" +"my-instance" +"test" +``` + +### 3. Separate Production and Development + +Create separate instances for different environments: + +```bash +# Development instance +curl -X PUT "https://api.example.com/v1/llm-services/alice/dev-openai" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"instance_handle": "dev-openai", ...}' + +# Production instance +curl -X PUT "https://api.example.com/v1/llm-services/alice/prod-openai" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"instance_handle": "prod-openai", ...}' +``` + +### 4. Document Instance Purpose + +Use the description field: + +```bash +curl -X PUT "https://api.example.com/v1/llm-services/alice/team-openai" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "instance_handle": "team-openai", + "description": "Shared OpenAI instance for research team. Contact alice@example.com for access.", + ... + }' +``` + +### 5. Regular Key Rotation + +Periodically update API keys: + +```bash +curl -X PATCH "https://api.example.com/v1/llm-services/alice/my-openai" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"api_key_encrypted": "sk-proj-new-key-here"}' +``` + +### 6. Monitor Instance Usage + +Track which projects use each instance to avoid deleting in-use instances. + +## Troubleshooting + +### Cannot Delete Instance + +**Error:** "Instance is in use by existing projects" + +**Solution:** Delete or update projects using this instance first. + +### Dimension Mismatch + +**Error:** "vector dimension mismatch" + +**Solution:** Ensure embeddings match the instance's configured dimensions. + +### API Key Not Working + +**Problem:** Embeddings uploads fail with authentication errors + +**Solution:** +1. Verify API key is correct +2. Check API key permissions with the LLM provider +3. Update the API key in the instance + +### Cannot Access Shared Instance + +**Problem:** Getting "Instance not found" errors + +**Solution:** Verify you've been granted access. Contact the instance owner. + +## Related Documentation + +- [RAG Workflow Guide](./rag-workflow.md) - Complete RAG implementation +- [Project Sharing Guide](./project-sharing.md) - Share projects with users +- [Batch Operations Guide](./batch-operations.md) - Upload embeddings efficiently + +## Security Summary + +1. **API keys are encrypted** at rest using AES-256-GCM +2. **API keys are never returned** via GET requests +3. **Shared users cannot see API keys** (write-only field) +4. **Encryption key must be secured** (loss means cannot decrypt keys) +5. **Regular key rotation recommended** for production use diff --git a/docs/content/guides/metadata-filtering.md b/docs/content/guides/metadata-filtering.md new file mode 100644 index 0000000..3a27b48 --- /dev/null +++ b/docs/content/guides/metadata-filtering.md @@ -0,0 +1,468 @@ +--- +title: "Metadata Filtering Guide" +weight: 6 +--- + +# Metadata Filtering Guide + +This guide explains how to use metadata filtering to exclude specific documents from similarity search results. + +## Overview + +When searching for similar documents, you may want to exclude results that share certain metadata values with your query. For example: +- Exclude documents from the same author when finding similar writing styles +- Filter out documents from the same source when finding related content +- Exclude documents with the same category when exploring diversity + +dhamps-vdb provides metadata filtering using query parameters that perform **negative matching** - they exclude documents where the metadata field matches the specified value. + +## Query Parameters + +Both similarity search endpoints support metadata filtering: + +- `metadata_path`: The JSON path to the metadata field (e.g., `author`, `source.id`, `tags[0]`) +- `metadata_value`: The value to exclude from results + +Both parameters must be used together. If you specify one without the other, the API returns an error. + +## Basic Filtering Examples + +### Exclude Documents by Author + +Find similar documents but exclude those from the same author: + +```bash +curl -X GET "https://api.example.com/v1/similars/alice/literary-corpus/hamlet-soliloquy?count=10&metadata_path=author&metadata_value=William%20Shakespeare" \ + -H "Authorization: Bearer alice_api_key" +``` + +This returns similar documents, excluding any with `metadata.author == "William Shakespeare"`. + +### Exclude Documents from Same Source + +Find similar content from different sources: + +```bash +curl -X GET "https://api.example.com/v1/similars/alice/news-articles/article123?count=10&metadata_path=source&metadata_value=NYTimes" \ + -H "Authorization: Bearer alice_api_key" +``` + +This excludes any documents with `metadata.source == "NYTimes"`. + +### Exclude by Category + +Find documents in different categories: + +```bash +curl -X GET "https://api.example.com/v1/similars/alice/products/product456?count=10&metadata_path=category&metadata_value=electronics" \ + -H "Authorization: Bearer alice_api_key" +``` + +This excludes any documents with `metadata.category == "electronics"`. + +## Filtering with Raw Embeddings + +Metadata filtering also works when searching with raw embedding vectors: + +```bash +curl -X POST "https://api.example.com/v1/similars/alice/literary-corpus?count=10&metadata_path=author&metadata_value=William%20Shakespeare" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "vector": [0.032, -0.018, 0.056, ...] + }' +``` + +This searches using the provided vector but excludes documents where `metadata.author == "William Shakespeare"`. + +## Nested Metadata Paths + +For nested metadata objects, use dot notation: + +### Example Metadata Structure + +```json +{ + "author": { + "name": "Jane Doe", + "id": "author123", + "affiliation": "University" + }, + "publication": { + "journal": "Science", + "year": 2023 + } +} +``` + +### Filter by Nested Field + +```bash +# Exclude documents from same author ID +curl -X GET "https://api.example.com/v1/similars/alice/papers/paper001?count=10&metadata_path=author.id&metadata_value=author123" \ + -H "Authorization: Bearer alice_api_key" + +# Exclude documents from same journal +curl -X GET "https://api.example.com/v1/similars/alice/papers/paper001?count=10&metadata_path=publication.journal&metadata_value=Science" \ + -H "Authorization: Bearer alice_api_key" +``` + +## Combining with Other Parameters + +Metadata filtering works seamlessly with other search parameters: + +```bash +curl -X GET "https://api.example.com/v1/similars/alice/documents/doc123?count=20&threshold=0.8&limit=10&offset=0&metadata_path=source_id&metadata_value=src_456" \ + -H "Authorization: Bearer alice_api_key" +``` + +Parameters: +- `count=20`: Consider top 20 similar documents +- `threshold=0.8`: Only include documents with similarity ≥ 0.8 +- `limit=10`: Return at most 10 results +- `offset=0`: Start from first result (for pagination) +- `metadata_path=source_id`: Filter on this metadata field +- `metadata_value=src_456`: Exclude documents with this value + +## Use Cases + +### 1. Finding Similar Writing Styles Across Authors + +When analyzing writing styles, you want similar texts from different authors: + +```bash +# Upload documents with author metadata +curl -X POST "https://api.example.com/v1/embeddings/alice/writing-styles" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "tolstoy-passage-1", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, ...], + "vector_dim": 3072, + "text": "Happy families are all alike...", + "metadata": { + "author": "Leo Tolstoy", + "work": "Anna Karenina", + "language": "Russian" + } + }] + }' + +# Find similar writing styles from other authors +curl -X GET "https://api.example.com/v1/similars/alice/writing-styles/tolstoy-passage-1?count=10&metadata_path=author&metadata_value=Leo%20Tolstoy" \ + -H "Authorization: Bearer alice_api_key" +``` + +### 2. Cross-Source Content Discovery + +Find related news articles from different sources: + +```bash +# Search for similar content, excluding same source +curl -X GET "https://api.example.com/v1/similars/alice/news-corpus/nyt-article-456?count=15&metadata_path=source&metadata_value=New%20York%20Times" \ + -H "Authorization: Bearer alice_api_key" +``` + +This helps discover how different outlets cover similar topics. + +### 3. Product Recommendations Across Categories + +Find similar products in different categories: + +```bash +# User is viewing a laptop +curl -X GET "https://api.example.com/v1/similars/alice/product-catalog/laptop-001?count=10&threshold=0.7&metadata_path=category&metadata_value=electronics" \ + -H "Authorization: Bearer alice_api_key" +``` + +This could recommend accessories, furniture (for home office), or other complementary items. + +### 4. Research Paper Discovery + +Find related papers from different research groups: + +```bash +curl -X GET "https://api.example.com/v1/similars/alice/research-papers/paper123?count=20&metadata_path=lab_id&metadata_value=lab_abc_001" \ + -H "Authorization: Bearer alice_api_key" +``` + +Helps researchers discover related work from other institutions. + +### 5. Avoiding Duplicate Content + +When building a diverse content feed, exclude items from the same collection: + +```bash +curl -X GET "https://api.example.com/v1/similars/alice/blog-posts/post789?count=5&metadata_path=collection_id&metadata_value=series_xyz" \ + -H "Authorization: Bearer alice_api_key" +``` + +### 6. Cross-Language Document Discovery + +Find similar documents in other languages: + +```bash +curl -X GET "https://api.example.com/v1/similars/alice/multilingual-docs/doc_en_123?count=10&metadata_path=language&metadata_value=en" \ + -H "Authorization: Bearer alice_api_key" +``` + +This finds semantically similar documents in languages other than English. + +## Working with Multiple Values + +Currently, you can only filter by one metadata field at a time. To exclude multiple values, you need to: + +1. **Make multiple requests** and merge results in your application +2. **Use more specific metadata fields** that combine multiple attributes +3. **Post-process results** on the client side + +### Example: Excluding Multiple Authors + +```python +import requests + +def find_similar_excluding_authors(doc_id, exclude_authors): + """Find similar docs excluding multiple authors""" + all_results = [] + + for author in exclude_authors: + response = requests.get( + f"https://api.example.com/v1/similars/alice/corpus/{doc_id}", + headers={"Authorization": "Bearer alice_api_key"}, + params={ + "count": 20, + "metadata_path": "author", + "metadata_value": author + } + ) + results = response.json()['results'] + all_results.extend(results) + + # Deduplicate and sort by similarity + seen = set() + unique_results = [] + for r in sorted(all_results, key=lambda x: x['similarity'], reverse=True): + if r['id'] not in seen: + seen.add(r['id']) + unique_results.append(r) + + return unique_results[:10] + +# Usage +similar = find_similar_excluding_authors( + "doc123", + ["Author A", "Author B", "Author C"] +) +``` + +## Combining with Metadata Validation + +For reliable filtering, combine with metadata schema validation: + +```bash +# Step 1: Create project with metadata schema +curl -X POST "https://api.example.com/v1/projects/alice/validated-corpus" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "validated-corpus", + "description": "Corpus with validated metadata", + "instance_id": 123, + "metadataScheme": "{\"type\":\"object\",\"properties\":{\"author\":{\"type\":\"string\"},\"source_id\":{\"type\":\"string\"}},\"required\":[\"author\",\"source_id\"]}" + }' + +# Step 2: Upload embeddings with metadata +curl -X POST "https://api.example.com/v1/embeddings/alice/validated-corpus" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "doc001", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, ...], + "vector_dim": 3072, + "metadata": { + "author": "John Doe", + "source_id": "source_123" + } + }] + }' + +# Step 3: Search with guaranteed metadata field existence +curl -X GET "https://api.example.com/v1/similars/alice/validated-corpus/doc001?count=10&metadata_path=author&metadata_value=John%20Doe" \ + -H "Authorization: Bearer alice_api_key" +``` + +See the [Metadata Validation Guide](./metadata-validation.md) for more details. + +## Understanding the Filter Logic + +The metadata filter uses **negative matching**: + +``` +INCLUDE document IF: + - document.similarity >= threshold + AND + - document.metadata[metadata_path] != metadata_value +``` + +**Important:** Documents without the specified metadata field are included (not filtered out). + +### Example + +Given this query: +```bash +?metadata_path=author&metadata_value=Alice +``` + +**Included:** +- Documents where `metadata.author == "Bob"` +- Documents where `metadata.author == "Charlie"` +- Documents without an `author` field in metadata + +**Excluded:** +- Documents where `metadata.author == "Alice"` + +## Performance Considerations + +Metadata filtering is performed at the database level using efficient indexing: + +1. **Vector similarity** is computed first +2. **Metadata filter** is applied to the similarity results +3. Results are sorted and limited + +For best performance: +- Use indexed metadata fields when possible +- Keep metadata values relatively small (under 1KB per document) +- Consider using IDs instead of full names for filtering + +## Error Handling + +### Missing One Parameter + +```bash +# Missing metadata_value +curl -X GET "https://api.example.com/v1/similars/alice/corpus/doc123?metadata_path=author" \ + -H "Authorization: Bearer alice_api_key" +``` + +**Error:** +```json +{ + "title": "Bad Request", + "status": 400, + "detail": "metadata_path and metadata_value must be used together" +} +``` + +### Non-Existent Metadata Field + +```bash +# Filtering on field that doesn't exist in documents +curl -X GET "https://api.example.com/v1/similars/alice/corpus/doc123?count=10&metadata_path=nonexistent_field&metadata_value=some_value" \ + -H "Authorization: Bearer alice_api_key" +``` + +**Result:** Returns all matching documents (since none have the field, none are excluded). + +### URL Encoding + +Remember to URL-encode metadata values with special characters: + +```bash +# Correct: URL-encoded value +curl -X GET "https://api.example.com/v1/similars/alice/corpus/doc123?metadata_path=author&metadata_value=John%20Doe%20%26%20Jane%20Smith" \ + -H "Authorization: Bearer alice_api_key" + +# Incorrect: Unencoded special characters +curl -X GET "https://api.example.com/v1/similars/alice/corpus/doc123?metadata_path=author&metadata_value=John Doe & Jane Smith" \ + -H "Authorization: Bearer alice_api_key" +``` + +## Complete Example + +Here's a complete workflow demonstrating metadata filtering: + +```bash +# 1. Create project +curl -X POST "https://api.example.com/v1/projects/alice/literature" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "literature", + "instance_id": 123 + }' + +# 2. Upload documents with metadata +curl -X POST "https://api.example.com/v1/embeddings/alice/literature" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [ + { + "text_id": "tolstoy_1", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, ...], + "vector_dim": 3072, + "text": "All happy families...", + "metadata": {"author": "Tolstoy", "work": "Anna Karenina"} + }, + { + "text_id": "tolstoy_2", + "instance_handle": "openai-large", + "vector": [0.11, 0.21, ...], + "vector_dim": 3072, + "text": "It was the best of times...", + "metadata": {"author": "Tolstoy", "work": "War and Peace"} + }, + { + "text_id": "dickens_1", + "instance_handle": "openai-large", + "vector": [0.12, 0.19, ...], + "vector_dim": 3072, + "text": "It was the age of wisdom...", + "metadata": {"author": "Dickens", "work": "Tale of Two Cities"} + } + ] + }' + +# 3. Find similar to tolstoy_1, excluding Tolstoy's works +curl -X GET "https://api.example.com/v1/similars/alice/literature/tolstoy_1?count=10&metadata_path=author&metadata_value=Tolstoy" \ + -H "Authorization: Bearer alice_api_key" + +# Result: Returns dickens_1, excludes tolstoy_2 +``` + +## Related Documentation + +- [RAG Workflow Guide](./rag-workflow.md) - Complete RAG implementation +- [Metadata Validation Guide](./metadata-validation.md) - Schema validation +- [Batch Operations Guide](./batch-operations.md) - Upload large datasets + +## Troubleshooting + +### No Results Returned + +**Problem:** Filter excludes all results + +**Solution:** +- Verify the metadata field exists in your documents +- Check that the metadata value matches exactly (case-sensitive) +- Try without the filter to ensure there are similar documents + +### Filter Not Working + +**Problem:** Still seeing documents you want to exclude + +**Solution:** +- Check URL encoding of the metadata value +- Verify the metadata path is correct (use dot notation for nested fields) +- Ensure both `metadata_path` and `metadata_value` are specified + +### Want Positive Matching + +**Problem:** Want to include only specific values, not exclude them + +**Solution:** Currently, only negative matching (exclusion) is supported. For positive matching, retrieve all results and filter on the client side, or use multiple negative filters to exclude everything except your target values. diff --git a/docs/content/guides/metadata-validation.md b/docs/content/guides/metadata-validation.md new file mode 100644 index 0000000..c345fd1 --- /dev/null +++ b/docs/content/guides/metadata-validation.md @@ -0,0 +1,665 @@ +--- +title: "Metadata Validation Guide" +weight: 5 +--- + +# Metadata Validation Guide + +This guide explains how to use JSON Schema validation to ensure consistent metadata structure across your embeddings. + +## Overview + +dhamps-vdb supports optional metadata validation using JSON Schema. When you define a metadata schema for a project, the API automatically validates all embedding metadata against that schema, ensuring data quality and consistency. + +Benefits: +- Enforce consistent metadata structure across all embeddings +- Catch data entry errors early +- Document expected metadata fields +- Enable reliable metadata-based filtering + +## Defining a Metadata Schema + +### When Creating a Project + +Include a `metadataScheme` field with a valid JSON Schema when creating a project: + +```bash +curl -X POST "https://api.example.com/v1/projects/alice/validated-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "validated-project", + "description": "Project with metadata validation", + "instance_id": 123, + "metadataScheme": "{\"type\":\"object\",\"properties\":{\"author\":{\"type\":\"string\"},\"year\":{\"type\":\"integer\"}},\"required\":[\"author\"]}" + }' +``` + +### Updating an Existing Project + +Add or update a metadata schema using PATCH: + +```bash +curl -X PATCH "https://api.example.com/v1/projects/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "metadataScheme": "{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\"},\"author\":{\"type\":\"string\"},\"year\":{\"type\":\"integer\"}},\"required\":[\"title\",\"author\"]}" + }' +``` + +**Note:** Schema updates only affect new or updated embeddings. Existing embeddings are not retroactively validated. + +## Common Schema Patterns + +### Simple Required Fields + +Require specific fields with basic types: + +```json +{ + "type": "object", + "properties": { + "author": {"type": "string"}, + "year": {"type": "integer"} + }, + "required": ["author"] +} +``` + +Example usage: + +```bash +curl -X POST "https://api.example.com/v1/projects/alice/literary-texts" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "literary-texts", + "description": "Literary texts with structured metadata", + "instance_id": 123, + "metadataScheme": "{\"type\":\"object\",\"properties\":{\"author\":{\"type\":\"string\"},\"year\":{\"type\":\"integer\"}},\"required\":[\"author\"]}" + }' +``` + +### Using Enums for Controlled Values + +Restrict fields to specific allowed values: + +```json +{ + "type": "object", + "properties": { + "genre": { + "type": "string", + "enum": ["poetry", "prose", "drama", "essay"] + }, + "language": { + "type": "string", + "enum": ["en", "de", "fr", "es", "la"] + } + }, + "required": ["genre"] +} +``` + +Example: + +```bash +curl -X POST "https://api.example.com/v1/embeddings/alice/literary-texts" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "hamlet-soliloquy", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3, ...], + "vector_dim": 3072, + "metadata": { + "author": "William Shakespeare", + "year": 1603, + "genre": "drama" + } + }] + }' +``` + +### Nested Objects + +Define structured metadata with nested objects: + +```json +{ + "type": "object", + "properties": { + "author": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "birth_year": {"type": "integer"}, + "nationality": {"type": "string"} + }, + "required": ["name"] + }, + "publication": { + "type": "object", + "properties": { + "year": {"type": "integer"}, + "publisher": {"type": "string"}, + "city": {"type": "string"} + } + } + }, + "required": ["author"] +} +``` + +Example: + +```bash +curl -X POST "https://api.example.com/v1/embeddings/alice/academic-papers" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "paper001", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3, ...], + "vector_dim": 3072, + "metadata": { + "author": { + "name": "Jane Smith", + "birth_year": 1975, + "nationality": "USA" + }, + "publication": { + "year": 2023, + "publisher": "Academic Press", + "city": "Boston" + } + } + }] + }' +``` + +### Arrays and Lists + +Define arrays of values: + +```json +{ + "type": "object", + "properties": { + "keywords": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1, + "maxItems": 10 + }, + "categories": { + "type": "array", + "items": { + "type": "string", + "enum": ["philosophy", "literature", "science", "history"] + } + } + } +} +``` + +Example: + +```bash +curl -X POST "https://api.example.com/v1/embeddings/alice/research-docs" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "doc001", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3, ...], + "vector_dim": 3072, + "metadata": { + "keywords": ["machine learning", "embeddings", "NLP"], + "categories": ["science", "literature"] + } + }] + }' +``` + +### Numeric Constraints + +Apply minimum, maximum, and range constraints: + +```json +{ + "type": "object", + "properties": { + "rating": { + "type": "number", + "minimum": 0, + "maximum": 5 + }, + "page_count": { + "type": "integer", + "minimum": 1 + }, + "confidence": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + } + } +} +``` + +### String Constraints + +Apply length and pattern constraints: + +```json +{ + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "isbn": { + "type": "string", + "pattern": "^[0-9]{13}$" + }, + "doi": { + "type": "string", + "pattern": "^10\\.\\d{4,}/[\\w\\-\\.]+$" + } + } +} +``` + +## Validation Examples + +### Valid Upload + +When metadata conforms to the schema: + +```bash +curl -X POST "https://api.example.com/v1/embeddings/alice/literary-texts" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "kant-critique", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3, ...], + "vector_dim": 3072, + "metadata": { + "author": "Immanuel Kant", + "year": 1781, + "genre": "prose" + } + }] + }' +``` + +**Response:** +```json +{ + "message": "Embeddings uploaded successfully", + "count": 1 +} +``` + +### Validation Error: Missing Required Field + +```bash +curl -X POST "https://api.example.com/v1/embeddings/alice/literary-texts" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "some-text", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3, ...], + "vector_dim": 3072, + "metadata": { + "year": 1781 + } + }] + }' +``` + +**Error Response:** +```json +{ + "$schema": "http://localhost:8080/schemas/ErrorModel.json", + "title": "Bad Request", + "status": 400, + "detail": "metadata validation failed for text_id 'some-text': metadata validation failed:\n - author is required" +} +``` + +### Validation Error: Wrong Type + +```bash +curl -X POST "https://api.example.com/v1/embeddings/alice/literary-texts" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "some-text", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3, ...], + "vector_dim": 3072, + "metadata": { + "author": "John Doe", + "year": "1781" + } + }] + }' +``` + +**Error Response:** +```json +{ + "$schema": "http://localhost:8080/schemas/ErrorModel.json", + "title": "Bad Request", + "status": 400, + "detail": "metadata validation failed for text_id 'some-text': metadata validation failed:\n - year: expected integer, got string" +} +``` + +### Validation Error: Invalid Enum Value + +```bash +curl -X POST "https://api.example.com/v1/embeddings/alice/literary-texts" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "some-text", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3, ...], + "vector_dim": 3072, + "metadata": { + "author": "John Doe", + "year": 1781, + "genre": "novel" + } + }] + }' +``` + +**Error Response:** +```json +{ + "$schema": "http://localhost:8080/schemas/ErrorModel.json", + "title": "Bad Request", + "status": 400, + "detail": "metadata validation failed for text_id 'some-text': metadata validation failed:\n - genre: value must be one of: poetry, prose, drama, essay" +} +``` + +### Validation Error: Value Out of Range + +```bash +curl -X POST "https://api.example.com/v1/embeddings/alice/rated-content" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "review001", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3, ...], + "vector_dim": 3072, + "metadata": { + "rating": 7.5 + } + }] + }' +``` + +**Error Response:** +```json +{ + "$schema": "http://localhost:8080/schemas/ErrorModel.json", + "title": "Bad Request", + "status": 400, + "detail": "metadata validation failed for text_id 'review001': metadata validation failed:\n - rating: must be <= 5" +} +``` + +## Real-World Schema Examples + +### Academic Publications + +```json +{ + "type": "object", + "properties": { + "doi": { + "type": "string", + "pattern": "^10\\.\\d{4,}/[\\w\\-\\.]+$" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "authors": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1 + }, + "year": { + "type": "integer", + "minimum": 1900, + "maximum": 2100 + }, + "journal": {"type": "string"}, + "volume": {"type": "integer"}, + "pages": {"type": "string"}, + "keywords": { + "type": "array", + "items": {"type": "string"} + } + }, + "required": ["doi", "title", "authors", "year"] +} +``` + +### Legal Documents + +```json +{ + "type": "object", + "properties": { + "case_number": {"type": "string"}, + "court": {"type": "string"}, + "date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + }, + "jurisdiction": { + "type": "string", + "enum": ["federal", "state", "local"] + }, + "category": { + "type": "string", + "enum": ["civil", "criminal", "administrative"] + }, + "parties": { + "type": "array", + "items": {"type": "string"} + } + }, + "required": ["case_number", "court", "date"] +} +``` + +### Product Catalog + +```json +{ + "type": "object", + "properties": { + "sku": { + "type": "string", + "pattern": "^[A-Z]{3}-\\d{6}$" + }, + "name": {"type": "string"}, + "category": { + "type": "string", + "enum": ["electronics", "clothing", "books", "home", "toys"] + }, + "price": { + "type": "number", + "minimum": 0 + }, + "in_stock": {"type": "boolean"}, + "tags": { + "type": "array", + "items": {"type": "string"} + } + }, + "required": ["sku", "name", "category", "price"] +} +``` + +## Admin Sanity Check + +Administrators can verify database integrity using the `/v1/admin/sanity-check` endpoint: + +```bash +curl -X GET "https://api.example.com/v1/admin/sanity-check" \ + -H "Authorization: Bearer admin_api_key" +``` + +**Response:** +```json +{ + "status": "PASSED", + "total_projects": 5, + "issues_count": 0, + "warnings_count": 1, + "warnings": [ + "Project alice/project1 has 100 embeddings but no metadata schema defined" + ] +} +``` + +**Status Values:** +- `PASSED`: No issues or warnings found +- `WARNING`: No critical issues, but warnings exist +- `FAILED`: Validation issues found that need attention + +The sanity check: +- Validates all embeddings have dimensions matching their LLM service +- Validates all metadata against project schemas (if defined) +- Reports projects without schemas as warnings + +## Best Practices + +### 1. Start Simple, Add Complexity Later + +Begin with basic required fields: + +```json +{ + "type": "object", + "properties": { + "source": {"type": "string"} + }, + "required": ["source"] +} +``` + +Add more constraints as your needs evolve. + +### 2. Test Schemas Before Deployment + +Use online JSON Schema validators like [jsonschemavalidator.net](https://www.jsonschemavalidator.net/) to test your schemas before deploying them. + +### 3. Document Your Schema + +Include a description in your project: + +```bash +curl -X PATCH "https://api.example.com/v1/projects/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "description": "Project with metadata schema: author (required string), year (integer), genre (enum)" + }' +``` + +### 4. Version Your Schemas + +If you need to change a schema significantly, consider creating a new project rather than updating the existing one. + +### 5. Optional vs Required + +Be judicious with `required` fields. Too many required fields can make uploads cumbersome. + +### 6. Escape JSON Properly + +When passing JSON schemas in curl commands, escape quotes properly or use single quotes for the outer JSON. + +## Projects Without Schemas + +If you don't provide a `metadataScheme` when creating a project: +- Metadata validation is skipped +- You can upload any valid JSON metadata +- This is useful for exploratory work or heterogeneous data + +## Schema Updates and Existing Data + +When you update a project's metadata schema: +- Existing embeddings are **not** revalidated +- The new schema only applies to new or updated embeddings +- Use the admin sanity check to find existing embeddings that don't conform + +## Removing a Schema + +To remove metadata validation from a project: + +```bash +curl -X PATCH "https://api.example.com/v1/projects/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"metadataScheme": null}' +``` + +After this, new embeddings can have any metadata structure. + +## Related Documentation + +- [RAG Workflow Guide](./rag-workflow.md) - Complete RAG implementation +- [Metadata Filtering Guide](./metadata-filtering.md) - Filter search results by metadata +- [Batch Operations Guide](./batch-operations.md) - Upload large datasets efficiently + +## Troubleshooting + +### Schema Syntax Errors + +**Error:** "Invalid JSON Schema" + +**Solution:** Validate your schema syntax using an online validator. Common issues: +- Missing commas between properties +- Unescaped quotes +- Invalid JSON structure + +### Uploads Failing After Schema Change + +**Problem:** Uploads worked before, now failing with validation errors + +**Solution:** Check that your metadata matches the new schema requirements. Review the error message for specific validation failures. + +### Want to Fix Non-Conforming Data + +**Problem:** Sanity check shows validation errors in existing data + +**Solution:** Either: +1. Update the schema to accept existing data +2. Re-upload conforming data to replace non-conforming embeddings +3. Delete and re-upload the project with correct metadata diff --git a/docs/content/guides/ownership-transfer.md b/docs/content/guides/ownership-transfer.md new file mode 100644 index 0000000..8c47b2b --- /dev/null +++ b/docs/content/guides/ownership-transfer.md @@ -0,0 +1,406 @@ +--- +title: "Ownership Transfer Guide" +weight: 4 +--- + +# Ownership Transfer Guide + +This guide explains how to transfer project ownership between users in dhamps-vdb. + +## Overview + +Project ownership transfer allows you to reassign full control of a project from one user to another. This is useful when: +- A project maintainer is leaving and wants to hand over control +- Organizational changes require reassigning project ownership +- Consolidating projects under a different user account +- Transferring stewardship of research data to a new PI + +## Important Constraints + +Before transferring ownership, understand these constraints: + +1. **Only the current owner can transfer** - Editors and readers cannot initiate transfers +2. **New owner must exist** - The target user must already be registered in the system +3. **No handle conflicts** - The new owner cannot already have a project with the same handle +4. **Old owner loses access** - After transfer, the original owner has no access to the project +5. **Data remains intact** - All embeddings and metadata are preserved during transfer +6. **Shared users remain** - Existing sharing relationships are maintained + +## Transferring Ownership + +### Basic Transfer + +Transfer a project to another user: + +```bash +curl -X POST "https://api.example.com/v1/projects/alice/research-data/transfer-ownership" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "new_owner_handle": "bob" + }' +``` + +**Response:** +```json +{ + "message": "Project ownership transferred successfully", + "project_handle": "research-data", + "old_owner": "alice", + "new_owner": "bob" +} +``` + +After this operation: +- The project is now accessible at `/v1/projects/bob/research-data` +- Bob has full owner privileges +- Alice no longer has any access to the project +- All embeddings remain unchanged + +### Complete Transfer Example + +Here's a complete workflow showing before and after transfer: + +```bash +# Before transfer - Alice is the owner +curl -X GET "https://api.example.com/v1/projects/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" + +# Response +{ + "project_handle": "my-project", + "owner": "alice", + "description": "Research project", + "instance_id": 123 +} + +# Alice transfers to Bob +curl -X POST "https://api.example.com/v1/projects/alice/my-project/transfer-ownership" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "bob"}' + +# After transfer - Bob is now the owner +curl -X GET "https://api.example.com/v1/projects/bob/my-project" \ + -H "Authorization: Bearer bob_api_key" + +# Response +{ + "project_handle": "my-project", + "owner": "bob", + "description": "Research project", + "instance_id": 123 +} + +# Alice can no longer access it +curl -X GET "https://api.example.com/v1/projects/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" +# Returns: 404 Not Found +``` + +## Effects of Transfer + +### Project Access Path Changes + +The project URL changes to reflect the new owner: + +**Before:** +``` +/v1/projects/alice/research-data +/v1/embeddings/alice/research-data +/v1/similars/alice/research-data/doc123 +``` + +**After:** +``` +/v1/projects/bob/research-data +/v1/embeddings/bob/research-data +/v1/similars/bob/research-data/doc123 +``` + +**Important:** Update all client code and bookmarks to use the new owner's handle. + +### New Owner Gains Full Control + +Bob (new owner) can now: +- View and modify all embeddings +- Update project settings (description, instance, metadata schema) +- Manage sharing (add/remove shared users) +- Transfer ownership again to someone else +- Delete the project + +### Old Owner Loses All Access + +Alice (old owner) can no longer: +- Access the project in any way +- View or modify embeddings +- See project metadata +- Manage sharing +- Transfer ownership back + +**Note:** If Alice needs continued access, Bob should share the project with her after the transfer. + +### Shared Users Remain + +If the project was shared with other users, those sharing relationships are preserved: + +```bash +# Before transfer - Alice shares with Charlie (editor) +curl -X POST "https://api.example.com/v1/projects/alice/research-data/share" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"share_with_handle": "charlie", "role": "editor"}' + +# Transfer to Bob +curl -X POST "https://api.example.com/v1/projects/alice/research-data/transfer-ownership" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "bob"}' + +# After transfer - Charlie still has editor access +curl -X GET "https://api.example.com/v1/projects/bob/research-data" \ + -H "Authorization: Bearer charlie_api_key" +# Works! Charlie can still access as editor +``` + +### Upgrading Shared User to Owner + +If the new owner was previously a shared user, their role is automatically upgraded: + +```bash +# Alice shares project with Bob (editor) +curl -X POST "https://api.example.com/v1/projects/alice/my-project/share" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"share_with_handle": "bob", "role": "editor"}' + +# Alice transfers ownership to Bob +curl -X POST "https://api.example.com/v1/projects/alice/my-project/transfer-ownership" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "bob"}' + +# Bob's previous "editor" sharing role is removed +# Bob now has full owner privileges instead +``` + +## Use Cases + +### PI Leaving Institution + +A principal investigator leaving an institution transfers project ownership to a colleague: + +```bash +curl -X POST "https://api.example.com/v1/projects/prof_jones/lab_data/transfer-ownership" \ + -H "Authorization: Bearer prof_jones_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "prof_smith"}' +``` + +### Account Consolidation + +Consolidate multiple projects under a single organizational account: + +```bash +# Transfer Alice's personal projects to organization account +curl -X POST "https://api.example.com/v1/projects/alice/project1/transfer-ownership" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "org_datascience"}' + +curl -X POST "https://api.example.com/v1/projects/alice/project2/transfer-ownership" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "org_datascience"}' +``` + +### Graduated Student Handoff + +A graduating student transfers their research project to their advisor: + +```bash +curl -X POST "https://api.example.com/v1/projects/student_bob/thesis_embeddings/transfer-ownership" \ + -H "Authorization: Bearer student_bob_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "advisor_carol"}' + +# Advisor can then share it back with the student if needed +curl -X POST "https://api.example.com/v1/projects/advisor_carol/thesis_embeddings/share" \ + -H "Authorization: Bearer advisor_carol_api_key" \ + -H "Content-Type: application/json" \ + -d '{"share_with_handle": "student_bob", "role": "reader"}' +``` + +### Department Reorganization + +Projects move to a new department owner: + +```bash +curl -X POST "https://api.example.com/v1/projects/old_dept/resource_library/transfer-ownership" \ + -H "Authorization: Bearer old_dept_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "new_dept"}' +``` + +## Error Conditions + +### New Owner Doesn't Exist + +```bash +curl -X POST "https://api.example.com/v1/projects/alice/my-project/transfer-ownership" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "nonexistent_user"}' +``` + +**Error:** +```json +{ + "title": "Bad Request", + "status": 400, + "detail": "User 'nonexistent_user' does not exist" +} +``` + +**Solution:** Ensure the target user is registered first. Contact admin to create the user. + +### Handle Conflict + +```bash +# Bob already has a project called "research-data" +curl -X POST "https://api.example.com/v1/projects/alice/research-data/transfer-ownership" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "bob"}' +``` + +**Error:** +```json +{ + "title": "Conflict", + "status": 409, + "detail": "User 'bob' already has a project with handle 'research-data'" +} +``` + +**Solution:** Either: +1. Rename Alice's project before transferring: + ```bash + curl -X PATCH "https://api.example.com/v1/projects/alice/research-data" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"project_handle": "research-data-alice"}' + ``` +2. Ask Bob to rename or delete their existing project +3. Choose a different target user + +### Not the Owner + +```bash +# Charlie tries to transfer Alice's project +curl -X POST "https://api.example.com/v1/projects/alice/research-data/transfer-ownership" \ + -H "Authorization: Bearer charlie_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "bob"}' +``` + +**Error:** +```json +{ + "title": "Forbidden", + "status": 403, + "detail": "Only the project owner can transfer ownership" +} +``` + +**Solution:** Only the current owner (Alice) can initiate the transfer. + +## Best Practices + +### Before Transferring + +1. **Communicate with New Owner**: Ensure they're willing to accept ownership +2. **Document Current State**: Export or document current embeddings and metadata +3. **Review Shared Users**: Check who has access and whether sharing should continue +4. **Update Client Code**: Identify all systems accessing the project that need updating +5. **Backup Data**: Consider exporting important data before transfer + +### During Transfer + +1. **Transfer at Low-Activity Time**: Minimize disruption by transferring during quiet periods +2. **Test Access First**: Verify new owner can access their other projects +3. **Use Correct Handle**: Double-check the new owner's handle before submitting + +### After Transferring + +1. **Verify New Ownership**: Confirm the transfer succeeded +2. **Update Client Applications**: Change all API calls to use new owner handle +3. **Grant Back Access if Needed**: New owner can share project back to old owner +4. **Update Documentation**: Update any documentation referencing the project path +5. **Notify Shared Users**: Inform shared users about the path change + +## Maintaining Access After Transfer + +If the original owner needs continued access, the new owner should share the project: + +```bash +# Step 1: Alice transfers to Bob +curl -X POST "https://api.example.com/v1/projects/alice/research-data/transfer-ownership" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "bob"}' + +# Step 2: Bob shares back with Alice as editor +curl -X POST "https://api.example.com/v1/projects/bob/research-data/share" \ + -H "Authorization: Bearer bob_api_key" \ + -H "Content-Type: application/json" \ + -d '{"share_with_handle": "alice", "role": "editor"}' + +# Now Alice can still access (but as editor, not owner) +curl -X GET "https://api.example.com/v1/embeddings/bob/research-data" \ + -H "Authorization: Bearer alice_api_key" +``` + +## Checking Current Owner + +To verify current project ownership: + +```bash +curl -X GET "https://api.example.com/v1/projects/{owner}/{project}" \ + -H "Authorization: Bearer your_api_key" +``` + +The `owner` field in the response shows the current owner. + +## Related Documentation + +- [Project Sharing Guide](./project-sharing.md) - Share projects with specific users +- [Public Projects Guide](./public-projects.md) - Make projects publicly accessible + +## Troubleshooting + +### Cannot Find Project After Transfer + +**Problem:** Getting 404 after transfer + +**Solution:** Update the owner in your API calls: +- Old: `/v1/projects/alice/my-project` +- New: `/v1/projects/bob/my-project` + +### Need to Reverse Transfer + +**Problem:** Transferred by mistake, need to reverse + +**Solution:** New owner must transfer back: +```bash +curl -X POST "https://api.example.com/v1/projects/bob/my-project/transfer-ownership" \ + -H "Authorization: Bearer bob_api_key" \ + -H "Content-Type: application/json" \ + -d '{"new_owner_handle": "alice"}' +``` + +### Client Applications Failing + +**Problem:** Applications can't access project after transfer + +**Solution:** Update all hardcoded owner references in your code to use the new owner's handle. diff --git a/docs/content/guides/project-sharing.md b/docs/content/guides/project-sharing.md new file mode 100644 index 0000000..b9276e1 --- /dev/null +++ b/docs/content/guides/project-sharing.md @@ -0,0 +1,333 @@ +--- +title: "Project Sharing Guide" +weight: 2 +--- + +# Project Sharing Guide + +This guide explains how to share projects with specific users for collaborative work in dhamps-vdb. + +## Overview + +Project sharing allows you to grant other users access to your projects with different permission levels: +- **reader**: Read-only access to embeddings and similar documents +- **editor**: Read and write access to embeddings (can add/modify/delete embeddings) + +Only the project owner can manage sharing settings and delete the project. + +## Sharing During Project Creation + +You can specify users to share with when creating a new project using the `shared_with` field: + +```bash +curl -X PUT "https://api.example.com/v1/projects/alice/collaborative-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "collaborative-project", + "description": "A project shared with team members", + "instance_id": 123, + "shared_with": [ + { + "user_handle": "bob", + "role": "reader" + }, + { + "user_handle": "charlie", + "role": "editor" + } + ] + }' +``` + +In this example: +- `bob` can read embeddings and search for similar documents +- `charlie` can read embeddings, search, and also add/modify/delete embeddings +- `alice` (the owner) has full control including managing sharing and deleting the project + +## Managing Sharing After Creation + +### Share a Project with a User + +Add a user to an existing project: + +```bash +curl -X POST "https://api.example.com/v1/projects/alice/collaborative-project/share" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "share_with_handle": "david", + "role": "reader" + }' +``` + +**Response:** +```json +{ + "message": "Project shared successfully", + "user": "david", + "role": "reader" +} +``` + +### Update User's Role + +To change a user's role, simply share again with the new role: + +```bash +curl -X POST "https://api.example.com/v1/projects/alice/collaborative-project/share" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "share_with_handle": "david", + "role": "editor" + }' +``` + +This updates David's access from reader to editor. + +### Unshare a Project from a User + +Remove a user's access to a project: + +```bash +curl -X DELETE "https://api.example.com/v1/projects/alice/collaborative-project/share/david" \ + -H "Authorization: Bearer alice_api_key" +``` + +**Response:** +```json +{ + "message": "Project unshared successfully", + "user": "david" +} +``` + +### List Shared Users + +View all users a project is shared with: + +```bash +curl -X GET "https://api.example.com/v1/projects/alice/collaborative-project/shared-with" \ + -H "Authorization: Bearer alice_api_key" +``` + +**Response:** +```json +{ + "project_handle": "collaborative-project", + "owner": "alice", + "shared_with": [ + { + "user_handle": "bob", + "role": "reader" + }, + { + "user_handle": "charlie", + "role": "editor" + } + ] +} +``` + +**Note:** Only the project owner can view the list of shared users. Users who have been granted access cannot see which other users also have access. + +## What Shared Users Can Do + +### As a Reader + +Bob (with reader access) can: + +```bash +# View project metadata +curl -X GET "https://api.example.com/v1/projects/alice/collaborative-project" \ + -H "Authorization: Bearer bob_api_key" + +# Retrieve embeddings +curl -X GET "https://api.example.com/v1/embeddings/alice/collaborative-project" \ + -H "Authorization: Bearer bob_api_key" + +# Get a specific embedding +curl -X GET "https://api.example.com/v1/embeddings/alice/collaborative-project/doc123" \ + -H "Authorization: Bearer bob_api_key" + +# Search for similar documents +curl -X GET "https://api.example.com/v1/similars/alice/collaborative-project/doc123?count=5" \ + -H "Authorization: Bearer bob_api_key" +``` + +### As an Editor + +Charlie (with editor access) can do everything a reader can, plus: + +```bash +# Upload new embeddings +curl -X POST "https://api.example.com/v1/embeddings/alice/collaborative-project" \ + -H "Authorization: Bearer charlie_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "doc456", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3, ...], + "vector_dim": 3072, + "metadata": {"author": "Charlie"} + }] + }' + +# Delete specific embeddings +curl -X DELETE "https://api.example.com/v1/embeddings/alice/collaborative-project/doc456" \ + -H "Authorization: Bearer charlie_api_key" + +# Delete all embeddings +curl -X DELETE "https://api.example.com/v1/embeddings/alice/collaborative-project" \ + -H "Authorization: Bearer charlie_api_key" +``` + +## What Shared Users Cannot Do + +Even with editor access, shared users **cannot**: + +- Delete the project +- Change project settings (description, instance, metadata schema) +- Manage sharing (add/remove other users) +- View the list of other shared users +- Transfer project ownership + +These operations require owner privileges. + +## Permission Summary Table + +| Operation | Owner | Editor | Reader | +|-----------|-------|--------|--------| +| View project metadata | ✅ | ✅ | ✅ | +| Retrieve embeddings | ✅ | ✅ | ✅ | +| Search similar documents | ✅ | ✅ | ✅ | +| Add embeddings | ✅ | ✅ | ❌ | +| Modify embeddings | ✅ | ✅ | ❌ | +| Delete embeddings | ✅ | ✅ | ❌ | +| Update project settings | ✅ | ❌ | ❌ | +| Delete project | ✅ | ❌ | ❌ | +| Manage sharing | ✅ | ❌ | ❌ | +| View shared users list | ✅ | ❌ | ❌ | +| Transfer ownership | ✅ | ❌ | ❌ | + +## Use Cases + +### Research Team Collaboration + +A research team can share a project where: +- The principal investigator (PI) is the owner +- Research assistants have editor access to upload new data +- External collaborators have reader access to query the data + +```bash +# PI creates and shares the project +curl -X PUT "https://api.example.com/v1/projects/pi/research-corpus" \ + -H "Authorization: Bearer pi_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "research-corpus", + "description": "Research team corpus", + "instance_id": 123, + "shared_with": [ + {"user_handle": "assistant1", "role": "editor"}, + {"user_handle": "assistant2", "role": "editor"}, + {"user_handle": "external_collab", "role": "reader"} + ] + }' +``` + +### Data Pipeline with Read-Only Access + +Share processed embeddings with downstream consumers: + +```bash +# Data processor creates project +curl -X PUT "https://api.example.com/v1/projects/processor/processed-data" \ + -H "Authorization: Bearer processor_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "processed-data", + "description": "Processed embeddings for consumption", + "instance_id": 456, + "shared_with": [ + {"user_handle": "app_backend", "role": "reader"}, + {"user_handle": "analytics_team", "role": "reader"} + ] + }' +``` + +### Temporary Access + +Grant temporary access to a consultant and revoke it later: + +```bash +# Grant access +curl -X POST "https://api.example.com/v1/projects/alice/sensitive-project/share" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"share_with_handle": "consultant", "role": "reader"}' + +# Revoke access when consultation is complete +curl -X DELETE "https://api.example.com/v1/projects/alice/sensitive-project/share/consultant" \ + -H "Authorization: Bearer alice_api_key" +``` + +## Combining with Public Access + +You can combine user-specific sharing with public access (see [Public Projects Guide](./public-projects.md)): + +```bash +curl -X PUT "https://api.example.com/v1/projects/alice/mixed-access-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "mixed-access-project", + "description": "Public read, specific editors", + "instance_id": 123, + "public_read": true, + "shared_with": [ + {"user_handle": "bob", "role": "editor"} + ] + }' +``` + +In this case: +- Anyone can read the project (no authentication required) +- `bob` can also edit (add/modify/delete embeddings) +- `alice` retains full owner privileges + +## Security Considerations + +1. **Choose Roles Carefully**: Only grant editor access to trusted users who need to modify data +2. **Audit Access**: Regularly review the shared users list to ensure appropriate access levels +3. **Revoke Promptly**: Remove access immediately when users no longer need it +4. **Use Reader by Default**: Start with reader access and upgrade to editor only when necessary +5. **Consider Public Access**: For truly open data, use `public_read: true` instead of sharing with many users + +## Related Documentation + +- [Ownership Transfer Guide](./ownership-transfer.md) - Transfer project ownership +- [Public Projects Guide](./public-projects.md) - Make projects publicly accessible +- [Instance Management Guide](./instance-management.md) - Share LLM service instances + +## Troubleshooting + +### Cannot Share Project + +**Error:** "Only the owner can share projects" + +**Solution:** Only the project owner can manage sharing. If you need to share the project, ask the owner to add you, or consider [transferring ownership](./ownership-transfer.md). + +### User Not Found + +**Error:** "User not found" + +**Solution:** The user must exist in the system before you can share a project with them. Ask the admin to create the user first. + +### Cannot View Shared Users List + +**Error:** "Forbidden" + +**Solution:** Only the project owner can view the list of shared users. Shared users cannot see other shared users for privacy reasons. diff --git a/docs/content/guides/public-projects.md b/docs/content/guides/public-projects.md new file mode 100644 index 0000000..c2854fe --- /dev/null +++ b/docs/content/guides/public-projects.md @@ -0,0 +1,420 @@ +--- +title: "Public Projects Guide" +weight: 3 +--- + +# Public Projects Guide + +This guide explains how to make projects publicly accessible, allowing anyone to read embeddings and search for similar documents without authentication. + +## Overview + +Projects can be configured to allow unauthenticated (public) read access by setting the `public_read` field to `true`. This is useful for: +- Open datasets and research data +- Public APIs and services +- Shared knowledge bases +- Educational resources + +**Important:** Public access only applies to read operations. Write operations (creating, updating, or deleting embeddings) always require authentication. + +## Creating a Public Project + +Set `public_read` to `true` when creating a project: + +```bash +curl -X PUT "https://api.example.com/v1/projects/alice/public-knowledge" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "public-knowledge", + "description": "Publicly accessible knowledge base", + "instance_id": 123, + "public_read": true + }' +``` + +**Response:** +```json +{ + "project_handle": "public-knowledge", + "owner": "alice", + "description": "Publicly accessible knowledge base", + "instance_id": 123, + "public_read": true +} +``` + +## Making an Existing Project Public + +Update an existing project using PATCH: + +```bash +curl -X PATCH "https://api.example.com/v1/projects/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"public_read": true}' +``` + +## Making a Public Project Private + +To disable public access: + +```bash +curl -X PATCH "https://api.example.com/v1/projects/alice/public-knowledge" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"public_read": false}' +``` + +## Accessing Public Projects Without Authentication + +Once a project has `public_read` enabled, anyone can access it without providing an API key. + +### Get Project Metadata + +```bash +curl -X GET "https://api.example.com/v1/projects/alice/public-knowledge" +``` + +**Response:** +```json +{ + "project_handle": "public-knowledge", + "owner": "alice", + "description": "Publicly accessible knowledge base", + "instance_id": 123, + "public_read": true +} +``` + +### Retrieve All Embeddings + +```bash +curl -X GET "https://api.example.com/v1/embeddings/alice/public-knowledge?limit=100" +``` + +**Response:** +```json +{ + "embeddings": [ + { + "text_id": "doc001", + "text": "Public document content", + "metadata": {"category": "science"}, + "vector_dim": 3072 + }, + { + "text_id": "doc002", + "text": "Another public document", + "metadata": {"category": "history"}, + "vector_dim": 3072 + } + ] +} +``` + +### Get a Specific Embedding + +```bash +curl -X GET "https://api.example.com/v1/embeddings/alice/public-knowledge/doc001" +``` + +**Response:** +```json +{ + "text_id": "doc001", + "text": "Public document content", + "metadata": {"category": "science"}, + "vector_dim": 3072, + "vector": [0.021, -0.015, 0.043, ...] +} +``` + +### Search for Similar Documents + +```bash +# Search by existing document ID +curl -X GET "https://api.example.com/v1/similars/alice/public-knowledge/doc001?count=5&threshold=0.7" +``` + +**Response:** +```json +{ + "user_handle": "alice", + "project_handle": "public-knowledge", + "results": [ + {"id": "doc002", "similarity": 0.92}, + {"id": "doc003", "similarity": 0.85}, + {"id": "doc004", "similarity": 0.78} + ] +} +``` + +### Search with Raw Embeddings + +```bash +curl -X POST "https://api.example.com/v1/similars/alice/public-knowledge?count=5" \ + -H "Content-Type: application/json" \ + -d '{ + "vector": [0.032, -0.018, 0.056, ...] + }' +``` + +## Operations Still Requiring Authentication + +Even for public projects, these operations require authentication: + +### Creating Embeddings (Requires Auth) + +```bash +# This will fail with 401 Unauthorized +curl -X POST "https://api.example.com/v1/embeddings/alice/public-knowledge" \ + -H "Content-Type: application/json" \ + -d '{"embeddings": [...]}' + +# This succeeds with valid API key +curl -X POST "https://api.example.com/v1/embeddings/alice/public-knowledge" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "doc123", + "instance_handle": "openai-large", + "vector": [0.1, 0.2, 0.3, ...], + "vector_dim": 3072 + }] + }' +``` + +### Deleting Embeddings (Requires Auth) + +```bash +# Delete specific embedding (requires auth) +curl -X DELETE "https://api.example.com/v1/embeddings/alice/public-knowledge/doc001" \ + -H "Authorization: Bearer alice_api_key" + +# Delete all embeddings (requires auth) +curl -X DELETE "https://api.example.com/v1/embeddings/alice/public-knowledge" \ + -H "Authorization: Bearer alice_api_key" +``` + +### Modifying Project Settings (Requires Auth) + +```bash +# Update project description (requires auth) +curl -X PATCH "https://api.example.com/v1/projects/alice/public-knowledge" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"description": "Updated description"}' +``` + +### Deleting Project (Requires Auth) + +```bash +curl -X DELETE "https://api.example.com/v1/projects/alice/public-knowledge" \ + -H "Authorization: Bearer alice_api_key" +``` + +## Combining Public Access with User Sharing + +You can combine public read access with user-specific editor permissions: + +```bash +curl -X PUT "https://api.example.com/v1/projects/alice/collaborative-public" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "collaborative-public", + "description": "Public read, restricted write", + "instance_id": 123, + "public_read": true, + "shared_with": [ + { + "user_handle": "bob", + "role": "editor" + }, + { + "user_handle": "charlie", + "role": "editor" + } + ] + }' +``` + +In this configuration: +- **Anyone** can read embeddings and search (no auth required) +- **bob** and **charlie** can add/modify/delete embeddings (with auth) +- **alice** (owner) has full control (with auth) + +## Use Cases + +### Open Research Dataset + +Share research data publicly while maintaining write control: + +```bash +curl -X PUT "https://api.example.com/v1/projects/university/research-corpus" \ + -H "Authorization: Bearer university_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "research-corpus", + "description": "Open research corpus for academic use", + "instance_id": 456, + "public_read": true, + "metadataScheme": "{\"type\":\"object\",\"properties\":{\"doi\":{\"type\":\"string\"},\"year\":{\"type\":\"integer\"}},\"required\":[\"doi\"]}" + }' +``` + +Researchers worldwide can access the data without credentials, but only authorized users can add new data. + +### Public API Backend + +Build a public search API on top of dhamps-vdb: + +```python +import requests + +def public_search_api(query_vector, count=10): + """Public search function requiring no authentication""" + response = requests.post( + "https://api.example.com/v1/similars/company/product-docs", + json={"vector": query_vector}, + params={"count": count, "threshold": 0.6} + ) + return response.json() + +# No API key needed for public projects! +results = public_search_api(query_embedding) +``` + +### Educational Resources + +Share educational content publicly: + +```bash +curl -X PUT "https://api.example.com/v1/projects/edu/learning-materials" \ + -H "Authorization: Bearer edu_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "learning-materials", + "description": "Free educational content embeddings", + "instance_id": 789, + "public_read": true + }' +``` + +Students and educators can access the materials without creating accounts. + +### Community-Driven Knowledge Base + +Open knowledge base with restricted editors: + +```bash +curl -X PUT "https://api.example.com/v1/projects/community/wiki-embeddings" \ + -H "Authorization: Bearer community_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "wiki-embeddings", + "description": "Community wiki embeddings", + "instance_id": 321, + "public_read": true, + "shared_with": [ + {"user_handle": "moderator1", "role": "editor"}, + {"user_handle": "moderator2", "role": "editor"} + ] + }' +``` + +## Security Considerations + +### What is Publicly Visible + +When `public_read` is enabled: +- ✅ Project metadata (name, description, owner) +- ✅ All embedding vectors and text content +- ✅ All embedding metadata +- ✅ Vector dimensions and instance references +- ❌ API keys (never exposed) +- ❌ User passwords or credentials + +### Best Practices + +1. **Review Content First**: Ensure no sensitive information is in embeddings or metadata before enabling public access +2. **Use Metadata Schemas**: Enforce consistent metadata structure with validation +3. **Monitor Usage**: Track access patterns to your public projects +4. **Set Clear Descriptions**: Provide clear project descriptions explaining the data's purpose and licensing +5. **Consider Rate Limiting**: For high-traffic public APIs, implement rate limiting at the application level + +### What to Avoid + +❌ **Don't** make projects public that contain: +- Personal identifiable information (PII) +- Proprietary or confidential data +- Sensitive research data not yet published +- Internal company information + +✅ **Do** make projects public that contain: +- Already-published research data +- Open educational resources +- Public domain content +- Creative Commons licensed materials + +## Disabling Public Access + +If you need to make a public project private again: + +```bash +curl -X PATCH "https://api.example.com/v1/projects/alice/public-knowledge" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"public_read": false}' +``` + +After this change: +- All read operations require authentication +- Existing anonymous access is immediately revoked +- No data is deleted, just access is restricted + +## Checking if a Project is Public + +View project metadata to check the `public_read` flag: + +```bash +curl -X GET "https://api.example.com/v1/projects/alice/public-knowledge" +``` + +Look for `"public_read": true` in the response. + +## Related Documentation + +- [Project Sharing Guide](./project-sharing.md) - Share with specific users +- [RAG Workflow Guide](./rag-workflow.md) - Complete RAG implementation +- [Metadata Validation Guide](./metadata-validation.md) - Enforce data quality + +## Troubleshooting + +### Public Access Not Working + +**Symptom:** Still getting 401 Unauthorized for public project + +**Solutions:** +1. Verify `public_read: true` is set: + ```bash + curl -X GET "https://api.example.com/v1/projects/alice/my-project" + ``` +2. Check you're using GET/POST for similars (not other methods) +3. Ensure the project exists and handle is correct + +### Accidentally Made Project Public + +**Solution:** Immediately disable public access: +```bash +curl -X PATCH "https://api.example.com/v1/projects/alice/my-project" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{"public_read": false}' +``` + +### Want to Track Public Usage + +**Solution:** Anonymous requests are logged with user set to "public". Review server logs to monitor public access patterns. diff --git a/docs/content/guides/rag-workflow.md b/docs/content/guides/rag-workflow.md new file mode 100644 index 0000000..30c5138 --- /dev/null +++ b/docs/content/guides/rag-workflow.md @@ -0,0 +1,393 @@ +--- +title: "RAG Workflow Guide" +weight: 1 +--- + +# Complete RAG Workflow Guide + +This guide demonstrates a complete Retrieval Augmented Generation (RAG) workflow using dhamps-vdb as your vector database. + +## Overview + +A typical RAG workflow involves: +1. Generate embeddings from your text content (using an external LLM service) +2. Upload embeddings to dhamps-vdb +3. Search for similar documents based on a query +4. Retrieve the relevant context +5. Use the context with an LLM to generate responses + +## Prerequisites + +- Access to dhamps-vdb API with a valid API key +- An external LLM service for generating embeddings (e.g., OpenAI, Cohere) +- Text content you want to process + +## Step 1: Generate Embeddings Externally + +First, use your chosen LLM service to generate embeddings for your text content. Here's an example using OpenAI's API: + +```python +import openai + +# Initialize OpenAI client +client = openai.OpenAI(api_key="your-openai-key") + +# Generate embeddings for your text +text = "The quick brown fox jumps over the lazy dog" +response = client.embeddings.create( + model="text-embedding-3-large", + input=text, + dimensions=3072 +) + +embedding_vector = response.data[0].embedding +``` + +## Step 2: Create LLM Service Instance + +Before uploading embeddings, create an LLM service instance in dhamps-vdb that matches your embedding configuration: + +```bash +curl -X PUT "https://api.example.com/v1/llm-services/alice/my-openai" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "endpoint": "https://api.openai.com/v1/embeddings", + "api_standard": "openai", + "model": "text-embedding-3-large", + "dimensions": 3072, + "description": "OpenAI large embedding model", + "api_key_encrypted": "sk-proj-your-openai-key" + }' +``` + +**Response:** +```json +{ + "instance_id": 123, + "instance_handle": "my-openai", + "owner": "alice", + "endpoint": "https://api.openai.com/v1/embeddings", + "model": "text-embedding-3-large", + "dimensions": 3072 +} +``` + +## Step 3: Create a Project + +Create a project to organize your embeddings: + +```bash +curl -X PUT "https://api.example.com/v1/projects/alice/my-documents" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "project_handle": "my-documents", + "description": "Document embeddings for RAG", + "instance_id": 123 + }' +``` + +## Step 4: Upload Embeddings to dhamps-vdb + +Upload your pre-generated embeddings along with metadata and optional text content: + +```bash +curl -X POST "https://api.example.com/v1/embeddings/alice/my-documents" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "embeddings": [{ + "text_id": "doc001", + "instance_handle": "my-openai", + "vector": [0.021, -0.015, 0.043, ...], + "vector_dim": 3072, + "text": "The quick brown fox jumps over the lazy dog", + "metadata": { + "source": "example.txt", + "author": "Alice", + "category": "animals" + } + }] + }' +``` + +**Tip:** Upload multiple embeddings in batches for efficiency (see [Batch Operations Guide](./batch-operations.md)). + +## Step 5: Search for Similar Documents + +When you need to retrieve relevant context for a query: + +### Option A: Search by Stored Document ID + +If you already have a document in your database that represents your query: + +```bash +curl -X GET "https://api.example.com/v1/similars/alice/my-documents/doc001?count=5&threshold=0.7" \ + -H "Authorization: Bearer alice_api_key" +``` + +### Option B: Search with Raw Query Embedding + +Generate an embedding for your query and search without storing it: + +```python +# Generate query embedding +query = "What animals are mentioned?" +query_response = client.embeddings.create( + model="text-embedding-3-large", + input=query, + dimensions=3072 +) +query_vector = query_response.data[0].embedding +``` + +```bash +curl -X POST "https://api.example.com/v1/similars/alice/my-documents?count=5&threshold=0.7" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "vector": [0.032, -0.018, 0.056, ...] + }' +``` + +**Response:** +```json +{ + "user_handle": "alice", + "project_handle": "my-documents", + "results": [ + { + "id": "doc001", + "similarity": 0.95 + }, + { + "id": "doc042", + "similarity": 0.87 + }, + { + "id": "doc103", + "similarity": 0.82 + } + ] +} +``` + +## Step 6: Retrieve Context Documents + +Retrieve the full content and metadata for the most similar documents: + +```bash +curl -X GET "https://api.example.com/v1/embeddings/alice/my-documents/doc001" \ + -H "Authorization: Bearer alice_api_key" +``` + +**Response:** +```json +{ + "text_id": "doc001", + "text": "The quick brown fox jumps over the lazy dog", + "metadata": { + "source": "example.txt", + "author": "Alice", + "category": "animals" + }, + "vector_dim": 3072 +} +``` + +## Step 7: Use Context with LLM + +Combine the retrieved context with your original query to generate an informed response: + +```python +# Collect context from similar documents +context_docs = [] +for result in similarity_results['results'][:3]: + doc = get_document(result['id']) # Your function to fetch document + context_docs.append(doc['text']) + +# Build context string +context = "\n\n".join(context_docs) + +# Generate response with context +response = client.chat.completions.create( + model="gpt-4", + messages=[ + {"role": "system", "content": "Answer based on the provided context."}, + {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {query}"} + ] +) + +answer = response.choices[0].message.content +``` + +## Complete Python Example + +Here's a complete example combining all steps: + +```python +import openai +import requests + +# Configuration +DHAMPS_API = "https://api.example.com" +DHAMPS_KEY = "your-dhamps-api-key" +OPENAI_KEY = "your-openai-key" + +# Initialize OpenAI +client = openai.OpenAI(api_key=OPENAI_KEY) + +def embed_and_store(text_id, text, metadata=None): + """Generate embedding and store in dhamps-vdb""" + # Generate embedding + response = client.embeddings.create( + model="text-embedding-3-large", + input=text, + dimensions=3072 + ) + vector = response.data[0].embedding + + # Upload to dhamps-vdb + requests.post( + f"{DHAMPS_API}/v1/embeddings/alice/my-documents", + headers={ + "Authorization": f"Bearer {DHAMPS_KEY}", + "Content-Type": "application/json" + }, + json={ + "embeddings": [{ + "text_id": text_id, + "instance_handle": "my-openai", + "vector": vector, + "vector_dim": 3072, + "text": text, + "metadata": metadata or {} + }] + } + ) + +def search_similar(query, count=5): + """Search for similar documents using query text""" + # Generate query embedding + response = client.embeddings.create( + model="text-embedding-3-large", + input=query, + dimensions=3072 + ) + query_vector = response.data[0].embedding + + # Search in dhamps-vdb + result = requests.post( + f"{DHAMPS_API}/v1/similars/alice/my-documents?count={count}", + headers={ + "Authorization": f"Bearer {DHAMPS_KEY}", + "Content-Type": "application/json" + }, + json={"vector": query_vector} + ) + return result.json()['results'] + +def retrieve_context(doc_ids): + """Retrieve full document content""" + docs = [] + for doc_id in doc_ids: + response = requests.get( + f"{DHAMPS_API}/v1/embeddings/alice/my-documents/{doc_id}", + headers={"Authorization": f"Bearer {DHAMPS_KEY}"} + ) + docs.append(response.json()) + return docs + +def rag_query(query): + """Complete RAG workflow""" + # Search for similar documents + similar = search_similar(query, count=3) + + # Retrieve context + context_docs = retrieve_context([r['id'] for r in similar]) + context = "\n\n".join([doc['text'] for doc in context_docs]) + + # Generate answer with LLM + response = client.chat.completions.create( + model="gpt-4", + messages=[ + {"role": "system", "content": "Answer based on the provided context."}, + {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {query}"} + ] + ) + + return response.choices[0].message.content + +# Usage +embed_and_store("doc001", "The quick brown fox jumps over the lazy dog", + {"category": "animals"}) +answer = rag_query("What animals are mentioned?") +print(answer) +``` + +## Best Practices + +1. **Batch Upload**: Upload embeddings in batches of 100-1000 for better performance +2. **Use Metadata**: Include rich metadata for better filtering and organization +3. **Set Thresholds**: Use similarity thresholds (e.g., 0.7) to filter low-quality matches +4. **Cache Embeddings**: Cache generated embeddings to avoid redundant API calls +5. **Monitor Dimensions**: Ensure all embeddings use consistent dimensions (3072 for text-embedding-3-large) + +## Advanced Features + +### Metadata Filtering + +Exclude certain documents from search results using metadata filters: + +```bash +# Exclude documents from the same author as the query +curl -X GET "https://api.example.com/v1/similars/alice/my-documents/doc001?metadata_path=author&metadata_value=Alice" \ + -H "Authorization: Bearer alice_api_key" +``` + +See the [Metadata Filtering Guide](./metadata-filtering.md) for more details. + +### Metadata Validation + +Enforce consistent metadata structure using JSON Schema validation: + +```bash +curl -X PATCH "https://api.example.com/v1/projects/alice/my-documents" \ + -H "Authorization: Bearer alice_api_key" \ + -H "Content-Type: application/json" \ + -d '{ + "metadataScheme": "{\"type\":\"object\",\"properties\":{\"author\":{\"type\":\"string\"},\"category\":{\"type\":\"string\"}},\"required\":[\"author\"]}" + }' +``` + +See the [Metadata Validation Guide](./metadata-validation.md) for more details. + +## Related Documentation + +- [Batch Operations Guide](./batch-operations.md) - Efficiently upload large datasets +- [Metadata Filtering Guide](./metadata-filtering.md) - Advanced search filtering +- [Metadata Validation Guide](./metadata-validation.md) - Schema validation +- [Instance Management Guide](./instance-management.md) - Managing LLM service instances + +## Troubleshooting + +### Dimension Mismatch Error + +```json +{ + "title": "Bad Request", + "status": 400, + "detail": "dimension validation failed: vector dimension mismatch" +} +``` + +**Solution**: Ensure the `vector_dim` field matches the dimensions configured in your LLM service instance. + +### No Similar Results + +If searches return no results, try: +- Lowering the similarity threshold (e.g., from 0.8 to 0.5) +- Increasing the count parameter +- Verifying embeddings are uploaded correctly +- Checking that query embeddings use the same model and dimensions diff --git a/docs/content/reference/_index.md b/docs/content/reference/_index.md new file mode 100644 index 0000000..99baa22 --- /dev/null +++ b/docs/content/reference/_index.md @@ -0,0 +1,20 @@ +--- +title: "Reference" +weight: 7 +--- + +# Reference Documentation + +Technical reference materials and specifications. + +## Contents + +- [Configuration Reference](configuration/) - Complete configuration options +- [Database Schema](database-schema/) - Database structure +- [Roadmap](roadmap/) - Planned features and improvements + +## Additional Resources + +- **OpenAPI Specification**: Available at `/openapi.yaml` on any running instance +- **Go Package Documentation**: Coming soon +- **Source Code**: [github.com/mpilhlt/dhamps-vdb](https://github.com/mpilhlt/dhamps-vdb) diff --git a/docs/public/404.html b/docs/public/404.html new file mode 100644 index 0000000..fc52734 --- /dev/null +++ b/docs/public/404.html @@ -0,0 +1,4 @@ +
o)o=a(e+=1),I.debug("jitter increased, raising bufferTime",{latency:A,maxJitter:s,jitterRange:n,bufferTime:o});else if(e>1&&Ci&&(r=performance.now(),o=a(e-=1),I.debug("jitter decreased, lowering bufferTime",{latency:A,maxJitter:s,jitterRange:n,bufferTime:o})),o;return r=null,o}}({logger:i},A)),function(A,g,I,B,Q){let C=arguments.length>5&&void 0!==arguments[5]?arguments[5]:1/60,E=performance.now()-1e3*Q,V=A(0);const i=new x;C*=1e3;let e=-C,o=!1;function t(){return performance.now()-E}return setTimeout((async()=>{for(;!o;){const A=await i.popAll();if(o)return;for(const B of A){const A=1e3*B[0]+V;if(A-e0&&(await W(Q),o))return;I(B[0]),g(B[1],B[2]),e=A}}}),0),{pushEvent(g){let I=t()-1e3*g[0];I<0&&(B.debug(`correcting epoch by ${I} ms`),E+=I,I=0),V=A(I),i.push(g)},pushText(A){i.push([t()/1e3,"o",A])},stop(){o=!0,i.push(void 0)}}}(g,e,C,i,E??0,V)}}function W(A){return new Promise((g=>{setTimeout(g,A)}))}const j=1e6;function Z(A){const g=new TextDecoder,I=new TextDecoder;let B,Q=function(A){const g=(new TextDecoder).decode(A);if("ALiS"!==g)throw"not an ALiS v1 live stream";Q=E},C=0;function E(A){const g=new _(new DataView(A)),I=g.getUint8();if(1!==I)throw`expected reset (0x01) frame, got ${I}`;return V(g,A)}function V(A,I){A.decodeVarUint();let E=A.decodeVarUint();B=E,E/=j,C=0;const V=A.decodeVarUint(),e=A.decodeVarUint(),o=A.getUint8();let t;if(8===o){const g=30;t=X(new Uint8Array(I,A.offset,g)),A.forward(g)}else if(16===o){const g=54;t=X(new Uint8Array(I,A.offset,g)),A.forward(g)}else if(0!==o)throw`alis: invalid theme format (${o})`;const s=A.decodeVarUint();let n;return s>0&&(n=g.decode(new Uint8Array(I,A.offset,s))),Q=i,{time:E,term:{size:{cols:V,rows:e},theme:t,init:n}}}function i(i){const e=new _(new DataView(i)),o=e.getUint8();return 1===o?V(e,i):111===o?function(A,I){A.decodeVarUint();const Q=A.decodeVarUint();B+=Q;const C=A.decodeVarUint(),E=g.decode(new Uint8Array(I,A.offset,C));return[B/j,"o",E]}(e,i):105===o?function(A,g){A.decodeVarUint();const Q=A.decodeVarUint();B+=Q;const C=A.decodeVarUint(),E=I.decode(new Uint8Array(g,A.offset,C));return[B/j,"i",E]}(e,i):114===o?function(A){A.decodeVarUint();const g=A.decodeVarUint();B+=g;const I=A.decodeVarUint(),Q=A.decodeVarUint();return[B/j,"r",{cols:I,rows:Q}]}(e):109===o?function(A,g){A.decodeVarUint();const I=A.decodeVarUint();B+=I;const Q=A.decodeVarUint(),E=new TextDecoder,V=C++,i=B/j,e=E.decode(new Uint8Array(g,A.offset,Q));return[i,"m",{index:V,time:i,label:e}]}(e,i):4===o?(Q=E,!1):void A.debug(`alis: unknown frame type: ${o}`)}return function(A){return Q(A)}}function X(A){const g=A.length/3,I=P(A[0],A[1],A[2]),B=P(A[3],A[4],A[5]),Q=[];for(let I=2;I 1&&void 0!==arguments[1]?arguments[1]:0;this.inner=A,this.offset=g}forward(A){this.offset+=A}getUint8(){const A=this.inner.getUint8(this.offset);return this.offset+=1,A}decodeVarUint(){let A=BigInt(0),g=BigInt(0),I=this.getUint8();for(;I>127;)I&=127,A+=BigInt(I)< (await R(u),J))();class QA{constructor(A){this.core=A,this.driver=A.driver}onEnter(A){}init(){}play(){}pause(){}togglePlay(){}seek(A){return!1}step(A){}stop(){this.driver.stop()}}class CA extends QA{async init(){try{return await this.core._initializeDriver(),this.core._setState("idle")}catch(A){throw this.core._setState("errored"),A}}async play(){this.core._dispatchEvent("play");const A=await this.init();await A.doPlay()}async togglePlay(){await this.play()}async seek(A){const g=await this.init();return await g.seek(A)}async step(A){const g=await this.init();await g.step(A)}stop(){}}class EA extends QA{onEnter(A){let{reason:g,message:I}=A;this.core._dispatchEvent("idle",{message:I}),"paused"===g&&this.core._dispatchEvent("pause")}async play(){this.core._dispatchEvent("play"),await this.doPlay()}async doPlay(){const A=await this.driver.play();!0===A?this.core._setState("playing"):"function"==typeof A&&(this.core._setState("playing"),this.driver.stop=A)}async togglePlay(){await this.play()}seek(A){return this.driver.seek(A)}step(A){this.driver.step(A)}}class VA extends QA{onEnter(){this.core._dispatchEvent("playing")}pause(){!0===this.driver.pause()&&this.core._setState("idle",{reason:"paused"})}togglePlay(){this.pause()}seek(A){return this.driver.seek(A)}}class iA extends QA{onEnter(){this.core._dispatchEvent("loading")}}class eA extends QA{onEnter(A){let{message:g}=A;this.core._dispatchEvent("offline",{message:g})}}class oA extends QA{onEnter(A){let{message:g}=A;this.core._dispatchEvent("ended",{message:g})}async play(){this.core._dispatchEvent("play"),await this.driver.restart()&&this.core._setState("playing")}async togglePlay(){await this.play()}seek(A){return!0===this.driver.seek(A)&&(this.core._setState("idle"),!0)}}class tA extends QA{onEnter(){this.core._dispatchEvent("errored")}}class sA{constructor(A,I){this.logger=I.logger,this.state=new CA(this),this.stateName="uninitialized",this.driver=function(A){if("function"==typeof A)return A;"string"==typeof A&&(A="ws://"==A.substring(0,5)||"wss://"==A.substring(0,6)?{driver:"websocket",url:A}:"clock:"==A.substring(0,6)?{driver:"clock"}:"random:"==A.substring(0,7)?{driver:"random"}:"benchmark:"==A.substring(0,10)?{driver:"benchmark",url:A.substring(10)}:{driver:"recording",url:A});void 0===A.driver&&(A.driver="recording");if("recording"==A.driver&&(void 0===A.parser&&(A.parser="asciicast"),"string"==typeof A.parser)){if(!rA.has(A.parser))throw`unknown parser: ${A.parser}`;A.parser=rA.get(A.parser)}if(nA.has(A.driver)){const g=nA.get(A.driver);return(I,B)=>g(A,I,B)}throw`unsupported driver: ${JSON.stringify(A)}`}(A),this.changedLines=new Set,this.cursor=void 0,this.duration=void 0,this.cols=I.cols,this.rows=I.rows,this.speed=I.speed,this.loop=I.loop,this.autoPlay=I.autoPlay,this.idleTimeLimit=I.idleTimeLimit,this.preload=I.preload,this.startAt=g(I.startAt),this.poster=this._parsePoster(I.poster),this.markers=this._normalizeMarkers(I.markers),this.pauseOnMarkers=I.pauseOnMarkers,this.commandQueue=Promise.resolve(),this.eventHandlers=new Map([["ended",[]],["errored",[]],["idle",[]],["input",[]],["loading",[]],["marker",[]],["metadata",[]],["offline",[]],["pause",[]],["play",[]],["playing",[]],["ready",[]],["reset",[]],["resize",[]],["seeked",[]],["terminalUpdate",[]]])}async init(){this.wasm=await BA;const A=this._feed.bind(this),g=this._now.bind(this),I=this._resetVt.bind(this),B=this._resizeVt.bind(this),Q=this._setState.bind(this),C="npt"===this.poster.type?this.poster.value:void 0;this.driver=this.driver({feed:A,onInput:A=>{this._dispatchEvent("input",{data:A})},onMarker:A=>{let{index:g,time:I,label:B}=A;this._dispatchEvent("marker",{index:g,time:I,label:B})},reset:I,resize:B,now:g,setTimeout:(A,g)=>setTimeout(A,g/this.speed),setInterval:(A,g)=>setInterval(A,g/this.speed),setState:Q,logger:this.logger},{cols:this.cols,rows:this.rows,idleTimeLimit:this.idleTimeLimit,startAt:this.startAt,loop:this.loop,posterTime:C,markers:this.markers,pauseOnMarkers:this.pauseOnMarkers}),"function"==typeof this.driver&&(this.driver={play:this.driver}),(this.preload||void 0!==C)&&this._withState((A=>A.init()));const E="text"===this.poster.type?this._renderPoster(this.poster.value):null,V={isPausable:!!this.driver.pause,isSeekable:!!this.driver.seek,poster:E};if(void 0===this.driver.init&&(this.driver.init=()=>({})),void 0===this.driver.pause&&(this.driver.pause=()=>{}),void 0===this.driver.seek&&(this.driver.seek=A=>!1),void 0===this.driver.step&&(this.driver.step=A=>{}),void 0===this.driver.stop&&(this.driver.stop=()=>{}),void 0===this.driver.restart&&(this.driver.restart=()=>{}),void 0===this.driver.getCurrentTime){const A=this.driver.play;let g=new U;this.driver.play=()=>(g=new f(this.speed),A()),this.driver.getCurrentTime=()=>g.getTime()}this._dispatchEvent("ready",V),this.autoPlay&&this.play()}play(){return this._withState((A=>A.play()))}pause(){return this._withState((A=>A.pause()))}togglePlay(){return this._withState((A=>A.togglePlay()))}seek(A){return this._withState((async g=>{await g.seek(A)&&this._dispatchEvent("seeked")}))}step(A){return this._withState((g=>g.step(A)))}stop(){return this._withState((A=>A.stop()))}getChanges(){const A={};if(this.changedLines.size>0){const g=new Map,I=this.vt.rows;for(const A of this.changedLines)A1&&void 0!==arguments[1]?arguments[1]:{};for(const I of this.eventHandlers.get(A))I(g)}_withState(A){return this._enqueueCommand((()=>A(this.state)))}_enqueueCommand(A){return this.commandQueue=this.commandQueue.then(A),this.commandQueue}_setState(A){let g=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(this.stateName===A)return this.state;if(this.stateName=A,"playing"===A)this.state=new VA(this);else if("idle"===A)this.state=new EA(this);else if("loading"===A)this.state=new iA(this);else if("ended"===A)this.state=new oA(this);else if("offline"===A)this.state=new eA(this);else{if("errored"!==A)throw`invalid state: ${A}`;this.state=new tA(this)}return this.state.onEnter(g),this.state}_feed(A){this._doFeed(A),this._dispatchEvent("terminalUpdate")}_doFeed(A){this.vt.feed(A).forEach((A=>this.changedLines.add(A))),this.cursor=void 0}_now(){return performance.now()*this.speed}async _initializeDriver(){const A=await this.driver.init();this.cols=this.cols??A.cols??80,this.rows=this.rows??A.rows??24,this.duration=this.duration??A.duration,this.markers=this._normalizeMarkers(A.markers)??this.markers??[],0===this.cols&&(this.cols=80),0===this.rows&&(this.rows=24),this._initializeVt(this.cols,this.rows);const g=void 0!==A.poster?this._renderPoster(A.poster):null;this._dispatchEvent("metadata",{cols:this.cols,rows:this.rows,duration:this.duration,markers:this.markers,theme:A.theme,poster:g})}_resetVt(A,g){let I=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0,B=arguments.length>3&&void 0!==arguments[3]?arguments[3]:void 0;this.logger.debug(`core: vt reset (${A}x${g})`),this.cols=A,this.rows=g,this.cursor=void 0,this._initializeVt(A,g),void 0!==I&&""!==I&&this._doFeed(I),this._dispatchEvent("reset",{cols:A,rows:g,theme:B})}_resizeVt(A,g){if(A===this.vt.cols&&g===this.vt.rows)return;this.vt.resize(A,g).forEach((A=>this.changedLines.add(A))),this.cursor=void 0,this.vt.cols=A,this.vt.rows=g,this.logger.debug(`core: vt resize (${A}x${g})`),this._dispatchEvent("resize",{cols:A,rows:g})}_initializeVt(A,g){this.vt=this.wasm.create(A,g,!0,100),this.vt.cols=A,this.vt.rows=g,this.changedLines.clear();for(let A=0;A B.feed(A)));const Q=B.getCursor()??!1,C=[];for(let A=0;A"number"==typeof A?[A,""]:A))}}const nA=new Map([["benchmark",function(A,g){let I,{url:B,iterations:Q=10}=A,{feed:C,setState:E,now:V}=g,i=0;return{async init(){const A=await p(await fetch(B)),{cols:g,rows:Q,events:C}=A;I=Array.from(C).filter((A=>{let[g,I,B]=A;return"o"===I})).map((A=>{let[g,I,B]=A;return[g,B]}));const E=I[I.length-1][0];for(const[A,g]of I)i+=new Blob([g]).size;return{cols:g,rows:Q,duration:E}},play(){const A=V();for(let A=0;A {E("stopped",{reason:"ended"})}),0),!0}}}],["clock",function(A,g,I){let{hourColor:B=3,minuteColor:Q=4,separatorColor:C=9}=A,{feed:E}=g,{cols:V=5,rows:i=1}=I;const e=Math.floor(i/2),o=Math.floor(V/2)-2,t=`[?25l[1m[${e}B`;let s;const n=()=>{const A=new Date,g=A.getHours(),I=A.getMinutes(),E=[];E.push("\r");for(let A=0;A{n().forEach(E)};return{init:()=>{const A=[t].concat(n());return{cols:V,rows:i,duration:1440,poster:A}},play:()=>(E(t),r(),s=setInterval(r,1e3),!0),stop:()=>{clearInterval(s)},getCurrentTime:()=>{const A=new Date;return 60*A.getHours()+A.getMinutes()}}}],["eventsource",function(A,g){let I,Q,{url:C,bufferTime:E,minFrameTime:V}=A,{feed:i,reset:e,resize:o,onInput:t,onMarker:s,setState:n,logger:r}=g;r=new B(r,"eventsource: ");let a=new U;function c(A){void 0!==Q&&Q.stop(),Q=O(E,i,o,t,s,(A=>a.setTime(A)),A,V,r)}return{play:()=>{I=new EventSource(C),I.addEventListener("open",(()=>{r.info("opened"),c()})),I.addEventListener("error",(A=>{r.info("errored"),r.debug({e:A}),n("loading")})),I.addEventListener("message",(A=>{const g=JSON.parse(A.data);if(Array.isArray(g))Q.pushEvent(g);else if(void 0!==g.cols||void 0!==g.width){const A=g.cols??g.width,I=g.rows??g.height;r.debug(`vt reset (${A}x${I})`),n("playing"),c(g.time),e(A,I,g.init??void 0),a=new f,"number"==typeof g.time&&a.setTime(g.time)}else"offline"===g.state&&(r.info("stream offline"),n("offline",{message:"Stream offline"}),a=new U)})),I.addEventListener("done",(()=>{r.info("closed"),I.close(),n("ended",{message:"Stream ended"})}))},stop:()=>{void 0!==Q&&Q.stop(),void 0!==I&&I.close()},getCurrentTime:()=>a.getTime()}}],["random",function(A,g){let{feed:I,setTimeout:B}=g;const Q=" ".charCodeAt(0),C="~".charCodeAt(0)-Q;let E;const V=()=>{const A=Math.pow(5,4*Math.random());E=B(i,A)},i=()=>{V();const A=String.fromCharCode(Q+Math.floor(Math.random()*C));I(A)};return()=>(V(),()=>clearInterval(E))}],["recording",function(A,g,I){let B,Q,C,E,V,i,e,o,t,{feed:s,resize:n,onInput:r,onMarker:a,now:c,setTimeout:D,setState:w,logger:l}=g,{idleTimeLimit:h,startAt:y,loop:G,posterTime:k,markers:F,pauseOnMarkers:N,cols:q,rows:R}=I,J=0,d=0,M=0;async function u(A,g){const I=await fetch(A,g);if(!I.ok)throw`failed fetching recording from ${A}: ${I.status} ${I.statusText}`;return I}function f(){const A=C[J];A?e=D(U,function(A){let g=1e3*A-(c()-o);return g<0&&(g=0),g}(A[0])):p()}function U(){let A,g=C[J];do{d=g[0],J++;if(L(g))return;g=C[J],A=c()-o}while(g&&A>1e3*g[0]);f()}function S(){clearTimeout(e),e=null}function L(A){const[g,I,B]=A;if("o"===I)s(B);else if("i"===I)r(B);else if("r"===I){const[A,g]=B.split("x");n(A,g)}else if("m"===I&&(a(B),N))return K(),t=1e3*g,w("idle",{reason:"paused"}),!0;return!1}function p(){S(),M++,!0===G||"number"==typeof G&&M >"===A?A=I+5:"<<<"===A?A=I-.1*V:">>>"===A?A=I+.1*V:"%"===A[A.length-1]&&(A=parseFloat(A.substring(0,A.length-1))/100*V);else if("object"==typeof A)if("prev"===A.marker)A=b(I)??0,g&&I-A<1&&(A=b(A)??0);else if("next"===A.marker)A=function(A){if(0==E.length)return;let g,I=E.length-1,B=E[I];for(;B&&B[0]>A;)g=B[0],B=E[--I];return g}(I)??V;else if("number"==typeof A.marker){const g=E[A.marker];if(void 0===g)throw`invalid marker index: ${A.marker}`;A=g[0]}const B=Math.min(Math.max(A,0),V);B 1&&void 0!==arguments[1]?arguments[1]:1/60;return B=>{let Q=0,C=0;return{step:A=>{Q++,void 0!==g?"o"===A[1]&&"o"===g[1]&&A[0]-g[0]{void 0!==g&&(B(g),C++),A.debug(`batched ${Q} frames to ${C} frames`)}}}}(g,C)).map(function(A,g,I){let B=0,Q=0;return function(C){const E=C[0]-B-A;return B=C[0],E>0&&(Q+=E,C[0] "m"!==A[1])).multiplex(V,((A,g)=>A[0] "i"===A[1]?[A[0]+E,A[1],A[2]]:A)),i.sort(((A,g)=>A[0]-g[0])));const o=i[i.length-1][0],t=B-e.offset;return{...A,events:i,duration:o,effectiveStartAt:t}}(await g(await function(A){let{url:g,data:I,fetchOpts:B={}}=A;if("string"==typeof g)return u(g,B);if(Array.isArray(g))return Promise.all(g.map((A=>u(A,B))));if(void 0!==I)return"function"==typeof I&&(I=I()),I instanceof Promise||(I=Promise.resolve(I)),I.then((A=>"string"==typeof A||A instanceof ArrayBuffer?new Response(A):A));throw"failed fetching recording file: url/data missing in src"}(A),{encoding:t}),l,{idleTimeLimit:h,startAt:y,minFrameTime:I,inputOffset:e,markers_:F});if(({cols:B,rows:Q,events:C,duration:V,effectiveStartAt:i}=s),q=q??B,R=R??Q,0===C.length)throw"recording is missing events";void 0!==o&&function(A,g){const I=document.createElement("a"),B=A.events.map((A=>"m"===A[1]?[A[0],A[1],A[2].label]:A)),Q=function(A){return`${JSON.stringify({version:2,width:A.cols,height:A.rows})}\n${A.events.map(JSON.stringify).join("\n")}\n`}({...A,events:B});I.href=URL.createObjectURL(new Blob([Q],{type:"text/plain"})),I.download=g,I.click()}(s,o);const n=void 0!==k?(r=k,C.filter((A=>A[0] A[2]))):void 0;var r;return E=C.filter((A=>"m"===A[1])).map((A=>[A[0],A[2].label])),{cols:B,rows:Q,duration:V,theme:s.theme,poster:n,markers:E}},play:function(){if(e)throw"already playing";if(void 0===C[J])throw"already ended";return null!==i&&m(i),H(),!0},pause:K,seek:m,step:function(A){let g,I;if(void 0===A&&(A=1),A>0){let B=J;g=C[B];for(let Q=0;Q{const A=I.protocol||"raw";a.info("opened"),a.info(`activating ${A} protocol handler`),"v1.alis"===A?I.onmessage=k(Z(a)):"v2.asciicast"===A?I.onmessage=k(function(){let A=function(I){const B=JSON.parse(I);if(2!==B.version)throw"not an asciicast v2 stream";return A=g,{time:0,term:{size:{cols:B.width,rows:B.height}}}};function g(A){const g=JSON.parse(A);if("r"===g[1]){const[A,I]=g[2].split("x");return[g[0],"r",{cols:A,rows:I}]}return g}return function(g){return A(g)}}()):"raw"===A&&(I.onmessage=k($())),c=setTimeout((()=>{l=0}),1e3)},I.onclose=A=>{if(clearTimeout(D),q(),h||1e3===A.code||1005===A.code)a.info("closed"),r("ended",{message:"Stream ended"});else if(1002===A.code)a.debug(`close reason: ${A.reason}`),r("ended",{message:"Err: Player not compatible with the server"});else{clearTimeout(c);const A=V(l++);a.info(`unclean close, reconnecting in ${A}...`),r("loading"),setTimeout(G,A)}},y=!1}function k(A){return D=setTimeout(N,5e3),function(g){try{const I=A(g.data);if(Q)if(Array.isArray(I))Q.pushEvent(I);else if("string"==typeof I)Q.pushText(I);else if("object"!=typeof I||Array.isArray(I)){if(!1===I)N();else if(void 0!==I)throw`unexpected value from protocol handler: ${I}`}else F(I);else if("object"!=typeof I||Array.isArray(I)){if(void 0!==I)throw clearTimeout(D),`unexpected value from protocol handler: ${I}`;clearTimeout(D),D=setTimeout(N,1e3)}else F(I),clearTimeout(D)}catch(A){throw I.close(),A}}}function F(A){let{time:g,term:I}=A;const{size:B,init:C,theme:V}=I,{cols:c,rows:D}=B;a.info(`stream reset (${c}x${D} @${g})`),r("playing"),q(),Q=O(E,e,t,s,n,(A=>w.setTime(A)),g,i,a),o(c,D,C,V),w=new f,y=!0,"number"==typeof g&&w.setTime(g)}function N(){q(),y?(a.info("stream ended"),r("offline",{message:"Stream ended"})):(a.info("stream offline"),r("offline",{message:"Stream offline"})),w=new U}function q(){Q&&Q.stop(),Q=null}return{play:()=>{G()},stop:()=>{h=!0,q(),void 0!==I&&I.close()},getCurrentTime:()=>w.getTime()}}]]),rA=new Map([["asciicast",p],["typescript",async function(A,g){let{encoding:I}=g;const B=new TextDecoder(I);let Q,C,E=(await A[0].text()).split("\n").filter((A=>A.length>0)).map((A=>A.split(" ")));E[0].length<3&&(E=E.map((A=>["O",A[0],A[1]])));const V=await A[1].arrayBuffer(),i=new Uint8Array(V),e=i.findIndex((A=>10==A))+1,o=B.decode(i.subarray(0,e)).match(/COLUMNS="(\d+)" LINES="(\d+)"/);null!==o&&(Q=parseInt(o[1],10),C=parseInt(o[2],10));const t={array:i,cursor:e};let s=t;if(void 0!==A[2]){const g=await A[2].arrayBuffer();s={array:new Uint8Array(g),cursor:e}}const n=[];let r=0;for(const A of E)if(r+=parseFloat(A[1]),"O"===A[0]){const g=parseInt(A[2],10),I=t.array.subarray(t.cursor,t.cursor+g),Q=B.decode(I);n.push([r,"o",Q]),t.cursor+=g}else if("I"===A[0]){const g=parseInt(A[2],10),I=s.array.subarray(s.cursor,s.cursor+g),Q=B.decode(I);n.push([r,"i",Q]),s.cursor+=g}else if("S"===A[0]&&"SIGWINCH"===A[2]){const g=parseInt(A[4].slice(5),10),I=parseInt(A[3].slice(5),10);n.push([r,"r",`${g}x${I}`])}else"H"===A[0]&&"COLUMNS"===A[2]?Q=parseInt(A[3],10):"H"===A[0]&&"LINES"===A[2]&&(C=parseInt(A[3],10));return Q=Q??80,C=C??24,{cols:Q,rows:C,events:n}}],["ttyrec",async function(A,g){let{encoding:I}=g;const B=new TextDecoder(I),Q=await A.arrayBuffer(),C=new Uint8Array(Q),E=gA(C),V=E.time,i=B.decode(E.data).match(/\x1b\[8;(\d+);(\d+)t/),e=[];let o=80,t=24;null!==i&&(o=parseInt(i[2],10),t=parseInt(i[1],10));let s=0,n=gA(C);for(;void 0!==n;){const A=n.time-V,g=B.decode(n.data);e.push([A,"o",g]),s+=n.len,n=gA(C.subarray(s))}return{cols:o,rows:t,events:e}}]]);const aA=Symbol("solid-proxy"),cA="function"==typeof Proxy,DA=Symbol("solid-track"),wA={equals:(A,g)=>A===g};let lA=OA;const hA=1,yA=2,GA={owned:null,cleanups:null,context:null,owner:null};var kA=null;let FA=null,NA=null,qA=null,RA=null,JA=0;function dA(A,g){const I=NA,B=kA,Q=0===A.length,C=void 0===g?B:g,E=Q?GA:{owned:null,cleanups:null,context:C?C.context:null,owner:C},V=Q?A:()=>A((()=>YA((()=>XA(E)))));kA=E,NA=null;try{return xA(V,!0)}finally{NA=I,kA=B}}function MA(A,g){const I={value:A,observers:null,observerSlots:null,comparator:(g=g?Object.assign({},wA,g):wA).equals||void 0};return[HA.bind(I),A=>("function"==typeof A&&(A=A(I.value)),mA(I,A))]}function uA(A,g,I){bA(vA(A,g,!1,hA))}function fA(A,g,I){I=I?Object.assign({},wA,I):wA;const B=vA(A,g,!0,0);return B.observers=null,B.observerSlots=null,B.comparator=I.equals||void 0,bA(B),HA.bind(B)}function UA(A){return xA(A,!1)}function YA(A){if(null===NA)return A();const g=NA;NA=null;try{return A()}finally{NA=g}}function SA(A){!function(A,g,I){lA=WA;const B=vA(A,g,!1,hA);I&&I.render||(B.user=!0),RA?RA.push(B):bA(B)}((()=>YA(A)))}function LA(A){return null===kA||(null===kA.cleanups?kA.cleanups=[A]:kA.cleanups.push(A)),A}function pA(){return NA}function KA(A){const g=fA(A),I=fA((()=>zA(g())));return I.toArray=()=>{const A=I();return Array.isArray(A)?A:null!=A?[A]:[]},I}function HA(){if(this.sources&&this.state)if(this.state===hA)bA(this);else{const A=qA;qA=null,xA((()=>jA(this)),!1),qA=A}if(NA){const A=this.observers?this.observers.length:0;NA.sources?(NA.sources.push(this),NA.sourceSlots.push(A)):(NA.sources=[this],NA.sourceSlots=[A]),this.observers?(this.observers.push(NA),this.observerSlots.push(NA.sources.length-1)):(this.observers=[NA],this.observerSlots=[NA.sources.length-1])}return this.value}function mA(A,g,I){let B=A.value;return A.comparator&&A.comparator(B,g)||(A.value=g,A.observers&&A.observers.length&&xA((()=>{for(let g=0;g 1e6)throw qA=[],new Error}),!1)),g}function bA(A){if(!A.fn)return;XA(A);const g=JA;!function(A,g,I){let B;const Q=kA,C=NA;NA=kA=A;try{B=A.fn(g)}catch(g){return A.pure&&(A.state=hA,A.owned&&A.owned.forEach(XA),A.owned=null),A.updatedAt=I+1,PA(g)}finally{NA=C,kA=Q}(!A.updatedAt||A.updatedAt<=I)&&(null!=A.updatedAt&&"observers"in A?mA(A,B):A.value=B,A.updatedAt=I)}(A,A.value,g)}function vA(A,g,I,B=hA,Q){const C={fn:A,state:B,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:g,owner:kA,context:kA?kA.context:null,pure:I};return null===kA||kA!==GA&&(kA.owned?kA.owned.push(C):kA.owned=[C]),C}function TA(A){if(0===A.state)return;if(A.state===yA)return jA(A);if(A.suspense&&YA(A.suspense.inFallback))return A.suspense.effects.push(A);const g=[A];for(;(A=A.owner)&&(!A.updatedAt||A.updatedAt =0;I--)if((A=g[I]).state===hA)bA(A);else if(A.state===yA){const I=qA;qA=null,xA((()=>jA(A,g[0])),!1),qA=I}}function xA(A,g){if(qA)return A();let I=!1;g||(qA=[]),RA?I=!0:RA=[],JA++;try{const g=A();return function(A){qA&&(OA(qA),qA=null);if(A)return;const g=RA;RA=null,g.length&&xA((()=>lA(g)),!1)}(I),g}catch(A){I||(RA=null),qA=null,PA(A)}}function OA(A){for(let g=0;g =0;g--)XA(A.tOwned[g]);delete A.tOwned}if(A.owned){for(g=A.owned.length-1;g>=0;g--)XA(A.owned[g]);A.owned=null}if(A.cleanups){for(g=A.cleanups.length-1;g>=0;g--)A.cleanups[g]();A.cleanups=null}A.state=0}function PA(A,g=kA){const I=function(A){return A instanceof Error?A:new Error("string"==typeof A?A:"Unknown error",{cause:A})}(A);throw I}function zA(A){if("function"==typeof A&&!A.length)return zA(A());if(Array.isArray(A)){const g=[];for(let I=0;I A(g||{})))}function gg(){return!0}const Ig={get:(A,g,I)=>g===aA?I:A.get(g),has:(A,g)=>g===aA||A.has(g),set:gg,deleteProperty:gg,getOwnPropertyDescriptor:(A,g)=>({configurable:!0,enumerable:!0,get:()=>A.get(g),set:gg,deleteProperty:gg}),ownKeys:A=>A.keys()};function Bg(A){return(A="function"==typeof A?A():A)?A:{}}function Qg(){for(let A=0,g=this.length;A `Stale read from <${A}>.`;function Eg(A){const g="fallback"in A&&{fallback:()=>A.fallback};return fA(function(A,g,I={}){let B=[],Q=[],C=[],E=0,V=g.length>1?[]:null;return LA((()=>$A(C))),()=>{let i,e,o=A()||[],t=o.length;return o[DA],YA((()=>{let A,g,n,r,a,c,D,w,l;if(0===t)0!==E&&($A(C),C=[],B=[],Q=[],E=0,V&&(V=[])),I.fallback&&(B=[_A],Q[0]=dA((A=>(C[0]=A,I.fallback()))),E=1);else if(0===E){for(Q=new Array(t),e=0;e =c&&w>=c&&B[D]===o[w];D--,w--)n[w]=Q[D],r[w]=C[D],V&&(a[w]=V[D]);for(A=new Map,g=new Array(w+1),e=w;e>=c;e--)l=o[e],i=A.get(l),g[e]=void 0===i?-1:i,A.set(l,e);for(i=c;i<=D;i++)l=B[i],e=A.get(l),void 0!==e&&-1!==e?(n[e]=Q[i],r[e]=C[i],V&&(a[e]=V[i]),e=g[e],A.set(l,e)):C[i]();for(e=c;e A.each),A.children,g||void 0))}function Vg(A){const g="fallback"in A&&{fallback:()=>A.fallback};return fA(function(A,g,I={}){let B,Q=[],C=[],E=[],V=[],i=0;return LA((()=>$A(E))),()=>{const e=A()||[],o=e.length;return e[DA],YA((()=>{if(0===o)return 0!==i&&($A(E),E=[],Q=[],C=[],i=0,V=[]),I.fallback&&(Q=[_A],C[0]=dA((A=>(E[0]=A,I.fallback()))),i=1),C;for(Q[0]===_A&&(E[0](),E=[],Q=[],C=[],i=0),B=0;B e[B])):B>=Q.length&&(C[B]=dA(t));for(;B A.each),A.children,g||void 0))}function ig(A){const g=A.keyed,I=fA((()=>A.when),void 0,void 0),B=g?I:fA(I,void 0,{equals:(A,g)=>!A==!g});return fA((()=>{const Q=B();if(Q){const C=A.children;return"function"==typeof C&&C.length>0?YA((()=>C(g?Q:()=>{if(!YA(B))throw Cg("Show");return I()}))):C}return A.fallback}),void 0,void 0)}function eg(A){const g=KA((()=>A.children)),I=fA((()=>{const A=g(),I=Array.isArray(A)?A:[A];let B=()=>{};for(let A=0;A C()?void 0:Q.when),void 0,void 0),V=Q.keyed?E:fA(E,void 0,{equals:(A,g)=>!A==!g});B=()=>C()||(V()?[g,E,Q]:void 0)}return B}));return fA((()=>{const g=I()();if(!g)return A.fallback;const[B,Q,C]=g,E=C.children;return"function"==typeof E&&E.length>0?YA((()=>E(C.keyed?Q():()=>{if(YA(I)()?.[0]!==B)throw Cg("Match");return Q()}))):E}),void 0,void 0)}function og(A){return A}const tg="_$DX_DELEGATE";function sg(A,g,I,B={}){let Q;return dA((B=>{Q=B,g===document?A():lg(g,A(),g.firstChild?null:void 0,I)}),B.owner),()=>{Q(),g.textContent=""}}function ng(A,g,I,B){let Q;const C=()=>{const g=B?document.createElementNS("http://www.w3.org/1998/Math/MathML","template"):document.createElement("template");return g.innerHTML=A,I?g.content.firstChild.firstChild:B?g.firstChild:g.content.firstChild},E=g?()=>YA((()=>document.importNode(Q||(Q=C()),!0))):()=>(Q||(Q=C())).cloneNode(!0);return E.cloneNode=E,E}function rg(A,g=window.document){const I=g[tg]||(g[tg]=new Set);for(let B=0,Q=A.length;B B.call(A,I[1],g))}else A.addEventListener(g,I,"function"!=typeof I&&I)}function Dg(A,g,I){if(!g)return I?function(A,g,I){null==I?A.removeAttribute(g):A.setAttribute(g,I)}(A,"style"):g;const B=A.style;if("string"==typeof g)return B.cssText=g;let Q,C;for(C in"string"==typeof I&&(B.cssText=I=void 0),I||(I={}),g||(g={}),I)null==g[C]&&B.removeProperty(C),delete I[C];for(C in g)Q=g[C],Q!==I[C]&&(B.setProperty(C,Q),I[C]=Q);return I}function wg(A,g,I){return YA((()=>A(g,I)))}function lg(A,g,I,B){if(void 0===I||B||(B=[]),"function"!=typeof g)return yg(A,g,B,I);uA((B=>yg(A,g(),B,I)),B)}function hg(A){let g=A.target;const I=`$$${A.type}`,B=A.target,Q=A.currentTarget,C=g=>Object.defineProperty(A,"target",{configurable:!0,value:g}),E=()=>{const B=g[I];if(B&&!g.disabled){const Q=g[`${I}Data`];if(void 0!==Q?B.call(g,Q,A):B.call(g,A),A.cancelBubble)return}return g.host&&"string"!=typeof g.host&&!g.host._$host&&g.contains(A.target)&&C(g.host),!0},V=()=>{for(;E()&&(g=g._$host||g.parentNode||g.host););};if(Object.defineProperty(A,"currentTarget",{configurable:!0,get:()=>g||document}),A.composedPath){const I=A.composedPath();C(I[0]);for(let A=0;A{let Q=g();for(;"function"==typeof Q;)Q=Q();I=yg(A,Q,I,B)})),()=>I;if(Array.isArray(g)){const C=[],V=I&&Array.isArray(I);if(Gg(C,g,I,Q))return uA((()=>I=yg(A,C,I,B,!0))),()=>I;if(0===C.length){if(I=Fg(A,I,B),E)return I}else V?0===I.length?kg(A,C,B):function(A,g,I){let B=I.length,Q=g.length,C=B,E=0,V=0,i=g[Q-1].nextSibling,e=null;for(;E B-V){const Q=g[E];for(;V=0;C--){const E=g[C];if(Q!==E){const g=E.parentNode===A;B||C?g&&E.remove():g?A.replaceChild(Q,E):A.insertBefore(Q,I)}else B=!0}}else A.insertBefore(Q,I);return[Q]}const Ng=Symbol("store-raw"),qg=Symbol("store-node"),Rg=Symbol("store-has"),Jg=Symbol("store-self");function dg(A){let g=A[aA];if(!g&&(Object.defineProperty(A,aA,{value:g=new Proxy(A,Sg)}),!Array.isArray(A))){const I=Object.keys(A),B=Object.getOwnPropertyDescriptors(A);for(let Q=0,C=I.length;Qg===Ng||g===aA||g===DA||g===qg||g===Rg||"__proto__"===g||(pA()&&Ug(fg(A,Rg),g)(),g in A),set:()=>!0,deleteProperty:()=>!0,ownKeys:function(A){return Yg(A),Reflect.ownKeys(A)},getOwnPropertyDescriptor:function(A,g){const I=Reflect.getOwnPropertyDescriptor(A,g);return I&&!I.get&&I.configurable&&g!==aA&&g!==qg?(delete I.value,delete I.writable,I.get=()=>A[aA][g],I):I}};function Lg(A,g,I,B=!1){if(!B&&A[g]===I)return;const Q=A[g],C=A.length;void 0===I?(delete A[g],A[Rg]&&A[Rg][g]&&void 0!==Q&&A[Rg][g].$()):(A[g]=I,A[Rg]&&A[Rg][g]&&void 0===Q&&A[Rg][g].$());let E,V=fg(A,qg);if((E=Ug(V,g,Q))&&E.$((()=>I)),Array.isArray(A)&&A.length!==C){for(let g=A.length;g 1){B=g.shift();const C=typeof B,E=Array.isArray(A);if(Array.isArray(B)){for(let Q=0;Q 1)return void Kg(A[B],g,[B].concat(I));Q=A[B],I=[B].concat(I)}let C=g[0];"function"==typeof C&&(C=C(Q,I),C===Q)||void 0===B&&null==C||(C=ug(C),void 0===B||Mg(Q)&&Mg(C)&&!Array.isArray(C)?pg(Q,C):Lg(A,B,C))}function Hg(...[A,g]){const I=ug(A||{}),B=Array.isArray(I);return[dg(I),function(...A){UA((()=>{B&&1===A.length?function(A,g){if("function"==typeof g&&(g=g(A)),g=ug(g),Array.isArray(g)){if(A===g)return;let I=0,B=g.length;for(;I=E&&i>=E&&(C[V]===A[i]||Q&&C[V]&&A[i]&&C[V][Q]===A[i][Q]);V--,i--)s[i]=C[V];if(E>i||E>V){for(I=E;I<=i;I++)Lg(C,I,A[I]);for(;I A.length&&Lg(C,"length",A.length))}for(o=new Array(i+1),I=i;I>=E;I--)e=A[I],t=Q&&e?e[Q]:e,g=n.get(t),o[I]=void 0===g?-1:g,n.set(t,I);for(g=E;g<=V;g++)e=C[g],t=Q&&e?e[Q]:e,I=n.get(t),void 0!==I&&-1!==I&&(s[I]=C[g],I=o[I],n.set(t,I));for(I=E;I A.length&&Lg(C,"length",A.length))}const V=Object.keys(A);for(let g=0,I=V.length;g{if(!Mg(A)||!Mg(Q))return Q;const g=bg(Q,{[mg]:A},mg,I,B);return void 0===g?A:g}}const Tg=ng("",2);var xg=A=>{const g=fA((()=>{if(1==A.text.length){const g=A.text.codePointAt(0);if(g>=9600&&g<=9631||57520==g||57522==g)return g}})),I=fA((()=>g()?" ":A.text)),B=fA((()=>function(A,g,I){const B=A.get("fg"),Q=A.get("bg");let C={"--offset":g,width:`${I+.01}ch`};"string"==typeof B&&(C["--fg"]=B);"string"==typeof Q&&(C["--bg"]=Q);return C}(A.pen,A.offset,A.cellCount))),Q=fA((()=>function(A,g,I){const B=Og(A.get("fg"),A.get("bold"),"fg-"),Q=Og(A.get("bg"),!1,"bg-");let C=I??"";void 0!==g&&(C+=` cp-${g.toString(16)}`);B&&(C+=" "+B);Q&&(C+=" "+Q);A.has("bold")&&(C+=" ap-bright");A.has("faint")&&(C+=" ap-faint");A.has("italic")&&(C+=" ap-italic");A.has("underline")&&(C+=" ap-underline");A.has("blink")&&(C+=" ap-blink");A.get("inverse")&&(C+=" ap-inverse");return C}(A.pen,g(),A.extraClass)));return(()=>{const A=Tg.cloneNode(!0);return lg(A,I),uA((g=>{const I=Q(),C=B();return I!==g._v$&&ag(A,g._v$=I),g._v$2=Dg(A,C,g._v$2),g}),{_v$:void 0,_v$2:void 0}),A})()};function Og(A,g,I){if("number"==typeof A)return g&&A<8&&(A+=8),`${I}${A}`}const Wg=ng('',2);var jg=A=>(()=>{const g=Wg.cloneNode(!0);return lg(g,Ag(Vg,{get each(){return(()=>{if("number"==typeof A.cursor){const g=[];let I=0,B=0;for(;B 0&&g.push({...Q,text:i.slice(0,V).join("")}),g.push({...Q,text:i[V],offset:I+E,cellCount:C,extraClass:"ap-cursor"}),V Ag(xg,function(...A){let g=!1;for(let I=0;I =0;I--){const B=Bg(A[I])[g];if(void 0!==B)return B}},has(g){for(let I=A.length-1;I>=0;I--)if(g in Bg(A[I]))return!0;return!1},keys(){const g=[];for(let I=0;I =0;g--){const Q=A[g];if(!Q)continue;const C=Object.getOwnPropertyNames(Q);for(let A=C.length-1;A>=0;A--){const g=C[A];if("__proto__"===g||"constructor"===g)continue;const E=Object.getOwnPropertyDescriptor(Q,g);if(B[g]){const A=I[g];A&&(E.get?A.push(E.get.bind(Q)):void 0!==E.value&&A.push((()=>E.value)))}else B[g]=E.get?{enumerable:!0,configurable:!0,get:Qg.bind(I[g]=[E.get.bind(Q)])}:void 0!==E.value?E:void 0}}const Q={},C=Object.keys(B);for(let A=C.length-1;A>=0;A--){const g=C[A],I=B[g];I&&I.get?Object.defineProperty(Q,g,I):Q[g]=I?I.value:void 0}return Q}(A))})),g})();const Zg=ng('',2);var Xg=A=>{const g=()=>A.lineHeight??1.3333333333,I=fA((()=>({width:`${A.cols}ch`,height:g()*A.rows+"em","font-size":100*(A.scale||1)+"%","font-family":A.fontFamily,"--term-line-height":`${g()}em`,"--term-cols":A.cols}))),B=fA((()=>A.cursor?.[0])),Q=fA((()=>A.cursor?.[1]));return(()=>{const g=Zg.cloneNode(!0),C=A.ref;return"function"==typeof C?wg(C,g):A.ref=g,lg(g,Ag(Eg,{get each(){return A.lines},children:(A,g)=>Ag(jg,{get segments(){return A.segments},get cursor(){return(A=()=>g()===Q(),fA((()=>A())))()?B():null;var A}})})),uA((B=>{const Q=!(!A.blink&&!A.cursorHold),C=!!A.blink,E=I();return Q!==B._v$&&g.classList.toggle("ap-cursor-on",B._v$=Q),C!==B._v$2&&g.classList.toggle("ap-blink",B._v$2=C),B._v$3=Dg(g,E,B._v$3),B}),{_v$:void 0,_v$2:void 0,_v$3:void 0}),g})()};const Pg=ng('',6),zg=ng('',4),_g=ng('',2),$g=ng('',6),AI=ng('',34),gI=ng('',6);function II(A){let g=Math.floor(A);const I=Math.floor(g/86400);g%=86400;const B=Math.floor(g/3600);g%=3600;const Q=Math.floor(g/60);return g%=60,I>0?`${BI(I)}:${BI(B)}:${BI(Q)}:${BI(g)}`:B>0?`${BI(B)}:${BI(Q)}:${BI(g)}`:`${BI(Q)}:${BI(g)}`}function BI(A){return A<10?`0${A}`:A.toString()}var QI=A=>{const g=A=>g=>{g.preventDefault(),A(g)},I=()=>"number"==typeof A.currentTime?II(A.currentTime):"--:--",B=()=>"number"==typeof A.remainingTime?"-"+II(A.remainingTime):I(),Q=fA((()=>"number"==typeof A.duration?A.markers.filter((g=>g[0] {const g=A.currentTarget.offsetWidth,I=A.currentTarget.getBoundingClientRect(),B=A.clientX-I.left;return 100*Math.max(0,B/g)+"%"},[E,V]=MA(!1),i=function(A,g){let I=!0;return function(){if(I){I=!1;for(var B=arguments.length,Q=new Array(B),C=0;CI=!0),g)}}}(A.onSeekClick,50),e=g=>{g._marker||g.altKey||g.shiftKey||g.metaKey||g.ctrlKey||0!==g.button||(V(!0),A.onSeekClick(C(g)))},o=A=>{A.altKey||A.shiftKey||A.metaKey||A.ctrlKey||E()&&i(C(A))},t=()=>{V(!1)};return document.addEventListener("mouseup",t),LA((()=>{document.removeEventListener("mouseup",t)})),(()=>{const C=AI.cloneNode(!0),E=C.firstChild,V=E.firstChild,i=V.nextSibling,t=E.nextSibling,s=t.nextSibling,n=s.nextSibling,r=A.ref;return"function"==typeof r?wg(r,C):A.ref=C,lg(C,Ag(ig,{get when(){return A.isPausable},get children(){const I=_g.cloneNode(!0);return cg(I,"click",g(A.onPlayClick),!0),lg(I,Ag(eg,{get children(){return[Ag(og,{get when(){return A.isPlaying},get children(){return Pg.cloneNode(!0)}}),Ag(og,{get when(){return!A.isPlaying},get children(){return zg.cloneNode(!0)}})]}})),I}}),E),lg(V,I),lg(i,B),lg(t,Ag(ig,{get when(){return"number"==typeof A.progress||A.isSeekable},get children(){const I=$g.cloneNode(!0),B=I.firstChild.nextSibling;return I.$$mousemove=o,I.$$mousedown=e,lg(I,Ag(Eg,{get each(){return Q()},children:(I,B)=>(()=>{const Q=gI.cloneNode(!0),C=Q.firstChild,E=C.nextSibling;var V;return Q.$$mousedown=A=>{A._marker=!0},cg(Q,"click",(V=B(),g((()=>{A.onSeekClick({marker:V})}))),!0),lg(E,(()=>(A=>""===A[1]?II(A[0]):`${II(A[0])} - ${A[1]}`)(I))),uA((g=>{const B=(g=>g[0]/A.duration*100+"%")(I),E=!!(g=>"number"==typeof A.currentTime&&g[0]<=A.currentTime)(I);return B!==g._v$&&Q.style.setProperty("left",g._v$=B),E!==g._v$2&&C.classList.toggle("ap-marker-past",g._v$2=E),g}),{_v$:void 0,_v$2:void 0}),Q})()}),null),uA((g=>Dg(B,{transform:`scaleX(${A.progress||0}`},g))),I}})),cg(s,"click",g(A.onHelpClick),!0),cg(n,"click",g(A.onFullscreenClick),!0),uA((()=>C.classList.toggle("ap-seekable",!!A.isSeekable))),C})()};rg(["click","mousedown","mousemove"]);const CI=ng('',4);var EI=A=>CI.cloneNode(!0);const VI=ng('',4);var iI=A=>VI.cloneNode(!0);const eI=ng('',4);var oI=A=>(()=>{const g=eI.cloneNode(!0),I=g.firstChild;return lg(I,(()=>A.message)),uA((g=>Dg(I,{"font-family":A.fontFamily},g))),g})();const tI=ng('',22);var sI=A=>(()=>{const g=tI.cloneNode(!0);var I;return cg(g,"click",(I=A.onClick,A=>{A.preventDefault(),I(A)}),!0),g})();rg(["click"]);const nI=ng(" space - pause / resume ",4),rI=ng("← / → - rewind / fast-forward by 5 seconds ",6),aI=ng("Shift + ← / → - rewind / fast-forward by 10% ",8),cI=ng("[ / ] - jump to the previous / next marker ",6),DI=ng("0, 1, 2 ... 9 - jump to 0%, 10%, 20% ... 90% ",10),wI=ng(", / . - step back / forward, a frame at a time (when paused) ",6),lI=ng('',18);var hI=A=>(()=>{const g=lI.cloneNode(!0),I=g.firstChild,B=I.firstChild.firstChild.nextSibling,Q=B.firstChild;var C;return cg(g,"click",(C=A.onClose,A=>{A.preventDefault(),C(A)}),!0),I.$$click=A=>{A.stopPropagation()},lg(B,Ag(ig,{get when(){return A.isPausable},get children(){return nI.cloneNode(!0)}}),Q),lg(B,Ag(ig,{get when(){return A.isSeekable},get children(){return[rI.cloneNode(!0),aI.cloneNode(!0),cI.cloneNode(!0),DI.cloneNode(!0),wI.cloneNode(!0)]}}),Q),uA((I=>Dg(g,{"font-family":A.fontFamily},I))),g})();rg(["click"]);const yI=ng('',4);var GI=A=>{const g=A.logger,I=A.core,B=A.autoPlay,[Q,C]=Hg({lines:[],cursor:void 0,charW:A.charW,charH:A.charH,bordersW:A.bordersW,bordersH:A.bordersH,containerW:0,containerH:0,isPausable:!0,isSeekable:!0,isFullscreen:!1,currentTime:null,remainingTime:null,progress:null,blink:!0,cursorHold:!1}),[E,V]=MA(!1),[i,e]=MA(B?null:"start"),[o,t]=MA(null),[s,n]=MA({cols:A.cols,rows:A.rows},{equals:(A,g)=>A.cols===g.cols&&A.rows===g.rows}),[r,a]=MA(void 0),[c,D]=Hg([]),[w,l]=MA(!1),[h,y]=MA(!1),[G,k]=MA(void 0),F=fA((()=>s().cols||80)),N=fA((()=>s().rows||24)),q=()=>!1===A.controls?0:32;let R,J,d,M,u,f,U,Y,S,L;function p(){gA(),_(),$()}function K(A){UA((()=>{A.rows{L=A}));I.addEventListener("ready",(A=>{let{isPausable:g,isSeekable:I,poster:B}=A;C({isPausable:g,isSeekable:I}),H(B),L()})),I.addEventListener("metadata",(A=>{let{cols:g,rows:I,duration:B,theme:Q,poster:C,markers:E}=A;UA((()=>{K({cols:g,rows:I}),a(B),k(Q),D(E),H(C)}))})),I.addEventListener("play",(()=>{e(null)})),I.addEventListener("playing",(()=>{UA((()=>{V(!0),e(null),T(),AA(),z()}))})),I.addEventListener("idle",(()=>{UA((()=>{V(!1),p()}))})),I.addEventListener("loading",(()=>{UA((()=>{V(!1),p(),e("loader")}))})),I.addEventListener("offline",(A=>{let{message:g}=A;UA((()=>{V(!1),p(),void 0!==g&&(t(g),e("info"))}))}));let b=0;I.addEventListener("ended",(A=>{let{message:I}=A;UA((()=>{V(!1),p(),void 0!==I&&(t(I),e("info"))})),g.debug(`view: render count: ${b}`)})),I.addEventListener("errored",(()=>{e("error")})),I.addEventListener("resize",K),I.addEventListener("reset",(A=>{let{cols:g,rows:I,theme:B}=A;UA((()=>{K({cols:g,rows:I}),k(B),T()}))})),I.addEventListener("seeked",(()=>{$()})),I.addEventListener("terminalUpdate",(()=>{void 0===R&&(R=requestAnimationFrame(T))}));const v=()=>{S=new ResizeObserver(function(A,g){let I;return function(){for(var B=arguments.length,Q=new Array(B),C=0;CA.apply(this,Q)),g)}}((A=>{C({containerW:u.offsetWidth,containerH:u.offsetHeight}),u.dispatchEvent(new CustomEvent("resize",{detail:{el:f}}))}),10)),S.observe(u)};SA((async()=>{g.info("view: mounted"),g.debug("view: font measurements",{charW:Q.charW,charH:Q.charH}),v(),C({containerW:u.offsetWidth,containerH:u.offsetHeight})})),LA((()=>{I.stop(),gA(),_(),S.disconnect()}));const T=async()=>{const A=await I.getChanges();UA((()=>{void 0!==A.lines&&A.lines.forEach(((A,g)=>{C("lines",g,vg(A))})),void 0!==A.cursor&&C("cursor",vg(A.cursor)),C("cursorHold",!0)})),R=void 0,b+=1},x=fA((()=>{const g=Q.charW*F()+Q.bordersW,I=Q.charH*N()+Q.bordersH;let B=A.fit??"width";if("both"===B||Q.isFullscreen){B=Q.containerW/(Q.containerH-q())>g/I?"height":"width"}if(!1===B||"none"===B)return{};if("width"===B){const A=Q.containerW/g;return{scale:A,width:Q.containerW,height:I*A+q()}}if("height"===B){const A=(Q.containerH-q())/I;return{scale:A,width:g*A,height:Q.containerH}}throw`unsupported fit mode: ${B}`})),O=()=>{C("isFullscreen",document.fullscreenElement??document.webkitFullscreenElement)},W=()=>{Q.isFullscreen?(document.exitFullscreen??document.webkitExitFullscreen??(()=>{})).apply(document):(u.requestFullscreen??u.webkitRequestFullscreen??(()=>{})).apply(u)},j=()=>{h()?y(!1):(I.pause(),y(!0))},Z=A=>{if(!(A.altKey||A.metaKey||A.ctrlKey)){if(" "==A.key)I.togglePlay();else if(","==A.key)I.step(-1),$();else if("."==A.key)I.step(),$();else if("f"==A.key)W();else if("["==A.key)I.seek({marker:"prev"});else if("]"==A.key)I.seek({marker:"next"});else if(A.key.charCodeAt(0)>=48&&A.key.charCodeAt(0)<=57){const g=(A.key.charCodeAt(0)-48)/10;I.seek(100*g+"%")}else if("?"==A.key)j();else if("ArrowLeft"==A.key)A.shiftKey?I.seek("<<<"):I.seek("<<");else if("ArrowRight"==A.key)A.shiftKey?I.seek(">>>"):I.seek(">>");else{if("Escape"!=A.key)return;y(!1)}A.stopPropagation(),A.preventDefault()}},X=()=>{Q.isFullscreen&&IA(!0)},P=()=>{Q.isFullscreen||IA(!1)},z=()=>{d=setInterval($,100)},_=()=>{clearInterval(d)},$=async()=>{const A=await I.getCurrentTime(),g=await I.getRemainingTime(),B=await I.getProgress();C({currentTime:A,remainingTime:g,progress:B})},AA=()=>{M=setInterval((()=>{C((A=>{const g={blink:!A.blink};return g.blink&&(g.cursorHold=!1),g}))}),600)},gA=()=>{clearInterval(M),C("blink",!0)},IA=A=>{clearTimeout(J),A&&(J=setTimeout((()=>IA(!1)),2e3)),l(A)},BA=fA((()=>{const g=A.theme||"auto/asciinema";return"auto/"===g.slice(0,5)?{name:g.slice(5),colors:G()}:{name:g}})),QA=()=>{m.then((()=>I.play()))},CA=()=>{m.then((()=>I.togglePlay()))},EA=A=>{m.then((()=>I.seek(A)))},VA=(()=>{const g=yI.cloneNode(!0),I=g.firstChild;"function"==typeof u?wg(u,g):u=g,g.addEventListener("webkitfullscreenchange",O),g.addEventListener("fullscreenchange",O),g.$$mousemove=X,g.$$keydown=Z;return"function"==typeof f?wg(f,I):f=I,I.$$mousemove=()=>IA(!0),I.addEventListener("mouseleave",P),lg(I,Ag(Xg,{get cols(){return F()},get rows(){return N()},get scale(){return x()?.scale},get blink(){return Q.blink},get lines(){return Q.lines},get cursor(){return Q.cursor},get cursorHold(){return Q.cursorHold},get fontFamily(){return A.terminalFontFamily},get lineHeight(){return A.terminalLineHeight},ref(A){"function"==typeof U?U(A):U=A}}),null),lg(I,Ag(ig,{get when(){return!1!==A.controls},get children(){return Ag(QI,{get duration(){return r()},get currentTime(){return Q.currentTime},get remainingTime(){return Q.remainingTime},get progress(){return Q.progress},markers:c,get isPlaying(){return E()},get isPausable(){return Q.isPausable},get isSeekable(){return Q.isSeekable},onPlayClick:CA,onFullscreenClick:W,onHelpClick:j,onSeekClick:EA,ref(A){"function"==typeof Y?Y(A):Y=A}})}}),null),lg(I,Ag(eg,{get children(){return[Ag(og,{get when(){return"start"==i()},get children(){return Ag(sI,{onClick:QA})}}),Ag(og,{get when(){return"loader"==i()},get children(){return Ag(iI,{})}}),Ag(og,{get when(){return"info"==i()},get children(){return Ag(oI,{get message(){return o()},get fontFamily(){return A.terminalFontFamily}})}}),Ag(og,{get when(){return"error"==i()},get children(){return Ag(EI,{})}})]}}),null),lg(I,Ag(ig,{get when(){return h()},get children(){return Ag(hI,{get fontFamily(){return A.terminalFontFamily},onClose:()=>y(!1),get isPausable(){return Q.isPausable},get isSeekable(){return Q.isSeekable}})}}),null),uA((B=>{const Q=!!(!0===A.controls||"auto"===A.controls&&w()),C=`ap-player asciinema-player-theme-${BA().name}`,E=(()=>{const g={};!1!==A.fit&&"none"!==A.fit||void 0===A.terminalFontSize||("small"===A.terminalFontSize?g["font-size"]="12px":"medium"===A.terminalFontSize?g["font-size"]="18px":"big"===A.terminalFontSize?g["font-size"]="24px":g["font-size"]=A.terminalFontSize);const I=x();void 0!==I.width&&(g.width=`${I.width}px`,g.height=`${I.height}px`);const B=BA().colors;return B&&(g["--term-color-foreground"]=B.foreground,g["--term-color-background"]=B.background,B.palette.forEach(((A,I)=>{g[`--term-color-${I}`]=A}))),g})();return Q!==B._v$&&g.classList.toggle("ap-hud",B._v$=Q),C!==B._v$2&&ag(I,B._v$2=C),B._v$3=Dg(I,E,B._v$3),B}),{_v$:void 0,_v$2:void 0,_v$3:void 0}),g})();return VA};function kI(A,g){let I=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const B=function(A,g){const I=80,B=24,Q=document.createElement("div");let C;Q.style.height="0px",Q.style.overflow="hidden",Q.style.fontSize="15px",document.body.appendChild(Q);const E=sg((()=>(C=Ag(Xg,{cols:I,rows:B,lineHeight:g,fontFamily:A,lines:[]}),C)),Q),V={charW:C.clientWidth/I,charH:C.clientHeight/B,bordersW:C.offsetWidth-C.clientWidth,bordersH:C.offsetHeight-C.clientHeight};return E(),document.body.removeChild(Q),V}(I.terminalFontFamily,I.terminalLineHeight),Q={core:A,logger:I.logger,cols:I.cols,rows:I.rows,fit:I.fit,controls:I.controls,autoPlay:I.autoPlay,terminalFontSize:I.terminalFontSize,terminalFontFamily:I.terminalFontFamily,terminalLineHeight:I.terminalLineHeight,theme:I.theme,...B};let C;const E=sg((()=>(C=Ag(GI,Q),C)),g);return{el:C,dispose:E}}rg(["keydown","mousemove"]);const FI=["autoPlay","autoplay","cols","idleTimeLimit","loop","markers","pauseOnMarkers","poster","preload","rows","speed","startAt"],NI=["autoPlay","autoplay","cols","controls","fit","rows","terminalFontFamily","terminalFontSize","terminalLineHeight","theme"];return A.create=function(A,g){let B=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const Q=B.logger??new I,C=new sA(A,function(A){let g=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const I=Object.fromEntries(Object.entries(A).filter((A=>{let[g]=A;return FI.includes(g)})));return I.autoPlay??=I.autoplay,I.speed??=1,{...I,...g}}(B,{logger:Q})),{el:E,dispose:V}=kI(C,g,function(A){let g=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const I=Object.fromEntries(Object.entries(A).filter((A=>{let[g]=A;return NI.includes(g)})));return I.autoPlay??=I.autoplay,I.controls??="auto",{...I,...g}}(B,{logger:Q})),i=C.init(),e={el:E,dispose:V,getCurrentTime:()=>i.then(C.getCurrentTime.bind(C)),getDuration:()=>i.then(C.getDuration.bind(C)),play:()=>i.then(C.play.bind(C)),pause:()=>i.then(C.pause.bind(C)),seek:A=>i.then((()=>C.seek(A))),addEventListener:(A,g)=>C.addEventListener(A,g.bind(e))};return e},A}({}); diff --git a/docs/public/book.min.cc2c524ed250aac81b23d1f4af87344917b325208841feca0968fe450f570575.css b/docs/public/book.min.cc2c524ed250aac81b23d1f4af87344917b325208841feca0968fe450f570575.css new file mode 100644 index 0000000..db845ba --- /dev/null +++ b/docs/public/book.min.cc2c524ed250aac81b23d1f4af87344917b325208841feca0968fe450f570575.css @@ -0,0 +1 @@ +@charset "UTF-8";:root{--font-size:16px;--font-size-smaller:0.875rem;--font-size-smallest:0.75rem;--body-font-weight:400;--body-background:white;--body-background-tint:transparent;--body-font-color:black;--border-radius:0.25rem}/*!modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize*/*,::before,::after{box-sizing:border-box}html{font-family:system-ui,segoe ui,Roboto,Helvetica,Arial,sans-serif,apple color emoji,segoe ui emoji;line-height:1.15;-webkit-text-size-adjust:100%;tab-size:4}body{margin:0}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,liberation mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-color:initial}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}.flex{display:flex}.flex.gap{gap:1rem}.flex-auto{flex:auto}.flex-even{flex:1 1}.flex-wrap{flex-wrap:wrap}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.align-center{align-items:center}.mx-auto{margin:0 auto}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-small,small{font-size:.875em}.hidden{display:none}input.toggle{height:0;width:0;overflow:hidden;opacity:0;position:absolute}html{font-size:var(--font-size);scroll-behavior:smooth;touch-action:manipulation;scrollbar-gutter:stable}body{min-width:20rem;color:var(--body-font-color);background:var(--body-background)var(--body-background-tint);font-weight:var(--body-font-weight);text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1,h2,h3,h4,h5,h6{font-weight:inherit}a{flex:auto;align-items:center;gap:.5em;text-decoration:none;cursor:default}a[href],a[role=button]{color:var(--color-link);cursor:pointer}:focus-visible,input.toggle:focus-visible+label{outline-style:auto;outline-color:var(--color-link)}nav ul{padding:0;margin:0;list-style:none}nav ul li{position:relative}nav ul a{padding:.5em 0;display:flex;transition:opacity .1s ease-in-out}nav ul a[href]:hover,nav ul a[role=button]:hover{opacity:.5}nav ul ul{padding-inline-start:1.5em}ul.pagination{display:flex;justify-content:center;list-style-type:none;padding-inline-start:0}ul.pagination .page-item a{padding:1rem}.container{max-width:80rem;margin:0 auto}.book-icon{filter:var(--icon-filter)}a .book-icon{height:1em;width:1em}.book-brand{margin-top:0;margin-bottom:1rem}.book-brand img{height:1.5em;width:1.5em}.book-menu{flex:0 0 16rem;font-size:var(--font-size-smaller)}.book-menu .book-menu-content{width:16rem;padding:1rem;position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-menu a,.book-menu label{color:inherit;word-wrap:break-word;display:flex}.book-menu a.active{color:var(--color-link)}.book-menu label>img:last-child{height:1em;width:1em;cursor:pointer;align-self:center;transition:transform .1s ease-in-out}.book-menu input.toggle+label+ul{display:none}.book-menu input.toggle:checked+label>img:last-child{transform:rotate(90deg)}.book-menu input.toggle:checked+label+ul{display:block}body[dir=rtl] .book-menu input.toggle+label>img:last-child{transform:rotate(180deg)}body[dir=rtl] .book-menu input.toggle:checked+label>img:last-child{transform:rotate(90deg)}.book-section-flat{margin:1rem 0}.book-section-flat>a,.book-section-flat>span,.book-section-flat>label{font-weight:bolder}.book-section-flat>ul{padding-inline-start:0}.book-page{min-width:20rem;flex-grow:1;padding:1rem}.book-post{margin-bottom:4rem}.book-post .book-post-date img{height:1em;width:1em;margin-inline-end:.5em}.book-post .book-post-content{margin-top:1rem}.book-post .book-post-thumbnail{flex:0 0 34%}.book-post .book-post-thumbnail img{width:100%;aspect-ratio:4/3;object-fit:cover}.book-header{margin-bottom:1rem}.book-header label{line-height:0}.book-header h3{overflow:hidden;text-overflow:ellipsis;margin:0 1rem}.book-layout-landing .book-header{display:block;position:relative;z-index:1}.book-layout-landing .book-header nav>ul{display:flex;gap:1rem;justify-content:end}.book-layout-landing .book-header nav>ul>li{display:block;white-space:nowrap}.book-layout-landing .book-header nav>ul>li>ul{display:none;position:absolute;padding:0}.book-layout-landing .book-header nav>ul>li:hover>ul,.book-layout-landing .book-header nav>ul>li:focus-within>ul{display:block}.book-search{position:relative;margin:.5rem 0}.book-search input{width:100%;padding:.5rem;border:1px solid var(--gray-200);border-radius:var(--border-radius);background:var(--gray-100);color:var(--body-font-color)}.book-search input:required+.book-search-spinner{display:block}.book-search .book-search-spinner{position:absolute;top:0;margin:.5rem;margin-inline-start:calc(100% - 1.5rem);width:1rem;height:1rem;border:1px solid transparent;border-top-color:var(--body-font-color);border-radius:50%;animation:spin 1s ease infinite}@keyframes spin{100%{transform:rotate(360deg)}}.book-search ul a{padding-bottom:0}.book-search small{opacity:.5}.book-toc{flex:0 0 16rem;font-size:var(--font-size-smallest)}.book-toc .book-toc-content{width:16rem;padding:1rem;position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-toc a{display:block}.book-toc img{height:1em;width:1em}.book-toc nav>ul>li:first-child{margin-top:0}.book-footer{padding-top:1rem;font-size:var(--font-size-smaller)}.book-footer a{margin:.25rem 0;padding:.25rem 0}.book-comments{margin-top:1rem}.book-copyright{margin-top:1rem}.book-languages{margin-bottom:1rem}.book-languages span{padding:0}.book-languages ul{padding-inline-start:1.5em}.book-menu-content,.book-toc-content{transition:.2s ease-in-out;transition-property:transform,margin,opacity,visibility;will-change:transform,margin,opacity}@media screen and (max-width:56rem){.book-menu{visibility:hidden;margin-inline-start:-16rem;z-index:1}.book-menu .book-menu-content{background:var(--body-background)}.book-toc{display:none}.book-header{display:block}.book-post-container{flex-direction:column-reverse}#menu-control,#toc-control{display:inline}#menu-control:checked~main .book-menu{visibility:initial}#menu-control:checked~main .book-menu .book-menu-content{transform:translateX(16rem);box-shadow:0 0 .5rem rgba(0,0,0,.1)}#menu-control:checked~main .book-page{opacity:.25}#menu-control:checked~main .book-menu-overlay{display:block;position:fixed;top:0;bottom:0;left:0;right:0}#toc-control:checked~main .book-header aside{display:block}body[dir=rtl] #menu-control:checked~main .book-menu .book-menu-content{transform:translateX(-16rem)}}@media screen and (min-width:80rem){.book-page,.book-menu .book-menu-content,.book-toc .book-toc-content{padding:2rem 1rem}}@media print{.book-menu,.book-footer,.book-toc{display:none}.book-header,.book-header aside{display:block}main{display:block!important}}.markdown{line-height:1.6}.markdown>:first-child{margin-top:0}.markdown h1,.markdown h2,.markdown h3,.markdown h4,.markdown h5,.markdown h6{font-weight:inherit;line-height:1;margin-top:1.5em;margin-bottom:1rem}.markdown h1 a.anchor,.markdown h2 a.anchor,.markdown h3 a.anchor,.markdown h4 a.anchor,.markdown h5 a.anchor,.markdown h6 a.anchor{opacity:0;font-size:.75em;margin-inline-start:.25em}.markdown h1:hover a.anchor,.markdown h1 a.anchor:focus-visible,.markdown h2:hover a.anchor,.markdown h2 a.anchor:focus-visible,.markdown h3:hover a.anchor,.markdown h3 a.anchor:focus-visible,.markdown h4:hover a.anchor,.markdown h4 a.anchor:focus-visible,.markdown h5:hover a.anchor,.markdown h5 a.anchor:focus-visible,.markdown h6:hover a.anchor,.markdown h6 a.anchor:focus-visible{opacity:initial;text-decoration:none}.markdown h1{font-size:2rem}.markdown h2{font-size:1.5rem}.markdown h3{font-size:1.25rem}.markdown h4{font-size:1.125rem}.markdown h5{font-size:1rem}.markdown h6{font-size:.875rem}.markdown b,.markdown optgroup,.markdown strong{font-weight:bolder}.markdown a{text-decoration:none}.markdown a[href]:hover{text-decoration:underline}.markdown a[href]:visited{color:var(--color-visited-link)}.markdown img{max-width:100%;height:auto}.markdown code{direction:ltr;unicode-bidi:embed;padding:.125em .25em;background:var(--gray-100);border:1px solid var(--gray-200);border-radius:var(--border-radius);font-size:.875em}.markdown pre{padding:1rem;background:var(--gray-100);border:1px solid var(--gray-200);border-radius:var(--border-radius);overflow-x:auto}.markdown pre:focus{outline-style:auto;outline-color:var(--color-link)}.markdown pre code{padding:0;border:0;background:0 0}.markdown p{word-wrap:break-word}.markdown blockquote{margin:1rem 0;padding:.5rem 1rem .5rem .75rem;border-inline-start:.25rem solid var(--gray-200);border-radius:var(--border-radius)}.markdown blockquote :first-child{margin-top:0}.markdown blockquote :last-child{margin-bottom:0}.markdown table{overflow:auto;display:block;border-spacing:0;border-collapse:collapse;margin-top:1rem;margin-bottom:1rem}.markdown table tr th,.markdown table tr td{padding:.5rem 1rem;border:1px solid var(--gray-200);text-align:start}.markdown table tr:nth-child(2n){background:var(--gray-100)}.markdown hr{height:1px;border:none;background:var(--gray-200)}.markdown ul,.markdown ol{padding-inline-start:2rem;word-wrap:break-word}.markdown dl dt{font-weight:bolder;margin-top:1rem}.markdown dl dd{margin-inline-start:0;margin-bottom:1rem}.markdown .highlight{direction:ltr;unicode-bidi:embed;border-radius:var(--border-radius)}.markdown .highlight table tbody{border:1px solid var(--gray-200)}.markdown .highlight table tr pre{border:0}.markdown .highlight table tr td pre code>span{display:flex}.markdown .highlight table tr td:nth-child(1) pre{margin:0;padding-inline-end:0}.markdown .highlight table tr td:nth-child(2) pre{margin:0;padding-inline-start:0}.markdown details{padding:1rem;margin:1rem 0;border:1px solid var(--gray-200);border-radius:var(--border-radius)}.markdown details summary{line-height:1;padding:1rem;margin:-1rem;cursor:pointer;list-style:none}.markdown details summary::before{content:"›";display:inline-block;margin-inline-end:.5rem;transition:transform .1s ease-in-out}.markdown details[open] summary{margin-bottom:0}.markdown details[open] summary::before{transform:rotate(90deg)}.markdown figure{margin:1rem 0}.markdown figure figcaption{margin-top:1rem}.markdown-inner>:first-child,.markdown .book-steps>ol>li>:first-child,.markdown figure figcaption>:first-child{margin-top:0}.markdown-inner>:last-child,.markdown .book-steps>ol>li>:last-child,.markdown figure figcaption>:last-child{margin-bottom:0}.markdown .book-tabs{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:var(--border-radius);display:flex;flex-wrap:wrap}.markdown .book-tabs label{display:inline-block;padding:.5rem 1rem;border-bottom:1px transparent;cursor:pointer}.markdown .book-tabs .book-tabs-content{order:999;width:100%;border-top:1px solid var(--gray-100);padding:1rem;display:none}.markdown .book-tabs input[type=radio]:checked+label{border-bottom:1px solid var(--color-link)}.markdown .book-tabs input[type=radio]:checked+label+.book-tabs-content{display:block}.markdown .book-columns{gap:1rem}.markdown .book-columns>div{margin:1rem 0;min-width:13.2rem}.markdown .book-columns>ul{list-style:none;display:flex;padding:0;flex-wrap:wrap;gap:1rem}.markdown .book-columns>ul>li{flex:1 1;min-width:13.2rem}.markdown a.book-btn[href]{display:inline-block;font-size:var(--font-size-smaller);color:var(--color-link);line-height:2rem;padding:0 1rem;border:1px solid var(--color-link);border-radius:var(--border-radius);cursor:pointer}.markdown a.book-btn[href]:hover{text-decoration:none}.markdown .book-hint.note{border-color:var(--color-accent-note);background-color:var(--color-accent-note-tint)}.markdown .book-hint.tip{border-color:var(--color-accent-tip);background-color:var(--color-accent-tip-tint)}.markdown .book-hint.important{border-color:var(--color-accent-important);background-color:var(--color-accent-important-tint)}.markdown .book-hint.warning{border-color:var(--color-accent-warning);background-color:var(--color-accent-warning-tint)}.markdown .book-hint.caution{border-color:var(--color-accent-caution);background-color:var(--color-accent-caution-tint)}.markdown .book-hint.default{border-color:var(--color-accent-default);background-color:var(--color-accent-default-tint)}.markdown .book-hint.info{border-color:var(--color-accent-info);background-color:var(--color-accent-info-tint)}.markdown .book-hint.success{border-color:var(--color-accent-success);background-color:var(--color-accent-success-tint)}.markdown .book-hint.danger{border-color:var(--color-accent-danger);background-color:var(--color-accent-danger-tint)}.markdown .book-badge{display:inline-block;font-size:var(--font-size-smaller);font-weight:var(--body-font-weight);vertical-align:middle;border-radius:var(--border-radius);border:1px solid var(--accent-color);overflow:hidden;text-wrap:nowrap;color:var(--body-font-color)}.markdown .book-badge.note{--accent-color:var(--color-accent-note)}.markdown .book-badge.tip{--accent-color:var(--color-accent-tip)}.markdown .book-badge.important{--accent-color:var(--color-accent-important)}.markdown .book-badge.warning{--accent-color:var(--color-accent-warning)}.markdown .book-badge.caution{--accent-color:var(--color-accent-caution)}.markdown .book-badge.default{--accent-color:var(--color-accent-default)}.markdown .book-badge.info{--accent-color:var(--color-accent-info)}.markdown .book-badge.success{--accent-color:var(--color-accent-success)}.markdown .book-badge.danger{--accent-color:var(--color-accent-danger)}.markdown .book-badge span{display:inline-block;padding:0 .5rem}.markdown .book-badge span.book-badge-value{color:var(--body-background);background-color:var(--accent-color)}.markdown .book-steps{position:relative}.markdown .book-steps>ol{counter-reset:steps;list-style:none;padding-inline-start:1.25rem;margin-top:2rem}.markdown .book-steps>ol>li::before{content:counter(steps);counter-increment:steps;position:absolute;display:flex;justify-content:center;left:.5rem;height:1.5rem;width:1.5rem;padding:.25rem;border-radius:.5rem;white-space:nowrap;line-height:1rem;color:var(--body-background);background:var(--gray-500);outline:.25rem solid var(--body-background)}.markdown .book-steps>ol>li{border-inline-start:1px solid var(--gray-500);padding-inline-start:3rem;padding-bottom:2rem}.markdown .book-steps>ol>li:last-child{border:0}.markdown .book-card{display:block;overflow:hidden;height:100%;border-radius:var(--border-radius);border:1px solid var(--gray-200)}.markdown .book-card>a{display:block;height:100%}.markdown .book-card>a[href],.markdown .book-card>a[href]:visited{color:var(--body-font-color)}.markdown .book-card>a[href]:hover{text-decoration:none;background:var(--gray-100)}.markdown .book-card>a>img,.markdown .book-card>img{width:100%;display:block;aspect-ratio:4/3;object-fit:cover}.markdown .book-card .markdown-inner,.markdown .book-card figure figcaption,.markdown figure .book-card figcaption,.markdown .book-card .book-steps>ol>li{padding:1rem}.markdown .book-image input+img{cursor:zoom-in;transition:transform .2s ease-in-out}.markdown .book-image input:checked+img{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--body-background);object-fit:contain;width:100%;height:100%;z-index:1;cursor:zoom-out;padding:1rem}.markdown .book-asciinema{margin:1rem 0}.markdown .book-hero{min-height:24rem;align-content:center}.markdown .book-hero h1{font-size:3em}.markdown .book-codeblock-filename{background:var(--gray-100);border:1px solid var(--gray-200);border-bottom:0;font-size:var(--font-size-smaller);margin-top:1rem;padding:.25rem .5rem;border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius)}.markdown .book-codeblock-filename a{color:var(--body-font-color)}.markdown .book-codeblock-filename+.highlight pre{margin-top:0;border-start-start-radius:0;border-start-end-radius:0}:root{--body-background:white;--body-background-tint:none;--body-font-color:black;--color-link:#0055bb;--color-visited-link:#5500bb;--icon-filter:none;--gray-100:#f8f9fa;--gray-200:#e9ecef;--gray-500:#adb5bd;--color-accent-default:#64748b;--color-accent-default-tint:rgba(100, 116, 139, 0.1);--color-accent-note:#4486dd;--color-accent-note-tint:rgba(68, 134, 221, 0.1);--color-accent-tip:#3bad3b;--color-accent-tip-tint:rgba(59, 173, 59, 0.1);--color-accent-important:#8144dd;--color-accent-important-tint:rgba(129, 68, 221, 0.1);--color-accent-warning:#f59e42;--color-accent-warning-tint:rgba(245, 158, 66, 0.1);--color-accent-caution:#d84747;--color-accent-caution-tint:rgba(216, 71, 71, 0.1);--color-accent-info:#4486dd;--color-accent-info-tint:rgba(68, 134, 221, 0.1);--color-accent-success:#3bad3b;--color-accent-success-tint:rgba(59, 173, 59, 0.1);--color-accent-danger:#d84747;--color-accent-danger-tint:rgba(216, 71, 71, 0.1)} \ No newline at end of file diff --git a/docs/public/categories/index.html b/docs/public/categories/index.html new file mode 100644 index 0000000..88cde22 --- /dev/null +++ b/docs/public/categories/index.html @@ -0,0 +1,3 @@ +Categories | dhamps-vdb Documentation + +\ No newline at end of file diff --git a/docs/public/categories/index.xml b/docs/public/categories/index.xml new file mode 100644 index 0000000..609fa75 --- /dev/null +++ b/docs/public/categories/index.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/en.search-data.min.4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945.json b/docs/public/en.search-data.min.4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/docs/public/en.search-data.min.4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/docs/public/en.search.min.e5f99ffd1cb4862bdf1bad2554c0460e5894c72e7286c511967078b24f321f5e.js b/docs/public/en.search.min.e5f99ffd1cb4862bdf1bad2554c0460e5894c72e7286c511967078b24f321f5e.js new file mode 100644 index 0000000..0241b04 --- /dev/null +++ b/docs/public/en.search.min.e5f99ffd1cb4862bdf1bad2554c0460e5894c72e7286c511967078b24f321f5e.js @@ -0,0 +1 @@ +"use strict";(function(){const o="/dhamps-vdb/en.search-data.min.4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945.json",i=Object.assign({cache:!0},{includeScore:!0,useExtendedSearch:!0,fieldNormWeight:1.5,threshold:.2,ignoreLocation:!0,keys:[{name:"title",weight:.7},{name:"content",weight:.3}]}),e=document.querySelector("#book-search-input"),t=document.querySelector("#book-search-results");if(!e)return;e.addEventListener("focus",n),e.addEventListener("keyup",s),document.addEventListener("keypress",a);function a(t){if(t.target.value!==void 0)return;if(e===document.activeElement)return;const n=String.fromCharCode(t.charCode);if(!r(n))return;e.focus(),t.preventDefault()}function r(t){const n=e.getAttribute("data-hotkeys")||"";return n.indexOf(t)>=0}function n(){e.removeEventListener("focus",n),e.required=!0,fetch(o).then(e=>e.json()).then(e=>{window.bookSearchIndex=new Fuse(e,i)}).then(()=>e.required=!1).then(s)}function s(){for(;t.firstChild;)t.removeChild(t.firstChild);if(!e.value)return;const n=window.bookSearchIndex.search(e.value).slice(0,10);n.forEach(function(e){const n=c(" Categories on dhamps-vdb Documentation https://mpilhlt.github.io/dhamps-vdb/categories/Recent content in Categories on dhamps-vdb Documentation Hugo en-us "),s=n.querySelector("a"),o=n.querySelector("small");s.href=e.item.href,s.textContent=e.item.title,o.textContent=e.item.section,t.appendChild(n)})}function c(e){const t=document.createElement("div");return t.innerHTML=e,t.firstChild}})() \ No newline at end of file diff --git a/docs/public/favicon.png b/docs/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..6f545a570a80bd385cfdc27af249c7babe647e31 GIT binary patch literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gj_dQ)4Ln;{Ggf? hJij@#(b6^dom#@~z%SdyL~kcG zYv&2dTggrTEH&wQ;ZCv66P2$DAJ6*q?T%^Em(HugM FiE%XK3moU)_^$#b% z5a#;KXjuDfy+i)YhIdamCaKwEiDx}N^}EimL-~4?+Jd!`8cCgN>(m;Y1P-=tmH4z) ulvO8j-G87@|Nl?rlH~?^l?~)oW` \ No newline at end of file diff --git a/docs/public/fuse.min.js b/docs/public/fuse.min.js new file mode 100644 index 0000000..0d509f3 --- /dev/null +++ b/docs/public/fuse.min.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v7.1.0 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2025 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;n e.length)&&(t=e.length);for(var n=0,r=new Array(t);n 0&&void 0!==arguments[0]?arguments[0]:{},n=t.getFn,i=void 0===n?O.getFn:n,u=t.fieldNormWeight,o=void 0===u?O.fieldNormWeight:u;r(this,e),this.norm=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3,n=new Map,r=Math.pow(10,t);return{get:function(t){var i=t.match(j).length;if(n.has(i))return n.get(i);var u=1/Math.pow(i,.5*e),o=parseFloat(Math.round(u*r)/r);return n.set(i,o),o},clear:function(){n.clear()}}}(o,3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return u(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,A(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();A(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t 2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?O.getFn:r,u=n.fieldNormWeight,o=void 0===u?O.fieldNormWeight:u,c=new I({getFn:i,fieldNormWeight:o});return c.setKeys(e.map(w)),c.setSources(t),c.create(),c}function R(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,u=void 0===i?0:i,o=t.expectedLocation,c=void 0===o?0:o,a=t.distance,s=void 0===a?O.distance:a,h=t.ignoreLocation,l=void 0===h?O.ignoreLocation:h,f=r/e.length;if(l)return f;var d=Math.abs(c-u);return s?f+d/s:d?1:f}var N=32;function P(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,u=void 0===i?O.location:i,o=r.distance,c=void 0===o?O.distance:o,a=r.threshold,s=void 0===a?O.threshold:a,h=r.findAllMatches,l=void 0===h?O.findAllMatches:h,f=r.minMatchCharLength,d=void 0===f?O.minMatchCharLength:f,v=r.includeMatches,g=void 0===v?O.includeMatches:v,y=r.ignoreLocation,p=void 0===y?O.ignoreLocation:y;if(t.length>N)throw new Error("Pattern length exceeds max of ".concat(N,"."));for(var A,m=t.length,C=e.length,k=Math.max(0,Math.min(u,C)),E=s,F=k,M=d>1||g,b=M?Array(C):[];(A=e.indexOf(t,F))>-1;){var D=R(t,{currentLocation:A,expectedLocation:k,distance:c,ignoreLocation:p});if(E=Math.min(D,E),F=A+m,M)for(var B=0;B =$;z-=1){var T=z-1,K=n[e.charAt(T)];if(M&&(b[T]=+!!K),W[z]=(W[z+1]<<1|1)&K,_&&(W[z]|=(x[z+1]|x[z])<<1|1|x[z+1]),W[z]&L&&(w=R(t,{errors:_,currentLocation:T,expectedLocation:k,distance:c,ignoreLocation:p}))<=E){if(E=w,(F=T)<=k)break;$=Math.max(1,2*k-F)}}if(R(t,{errors:_+1,currentLocation:k,expectedLocation:k,distance:c,ignoreLocation:p})>E)break;x=W}var q={isMatch:F>=0,score:Math.max(.001,w)};if(M){var J=function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:O.minMatchCharLength,n=[],r=-1,i=-1,u=0,o=e.length;u =t&&n.push([r,i]),r=-1)}return e[u-1]&&u-r>=t&&n.push([r,u-1]),n}(b,d);J.length?g&&(q.indices=J):q.isMatch=!1}return q}function W(e){for(var t={},n=0,r=e.length;n 1&&void 0!==arguments[1]?arguments[1]:{},u=i.location,o=void 0===u?O.location:u,c=i.threshold,a=void 0===c?O.threshold:c,s=i.distance,h=void 0===s?O.distance:s,l=i.includeMatches,f=void 0===l?O.includeMatches:l,d=i.findAllMatches,v=void 0===d?O.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?O.minMatchCharLength:g,p=i.isCaseSensitive,A=void 0===p?O.isCaseSensitive:p,m=i.ignoreDiacritics,C=void 0===m?O.ignoreDiacritics:m,k=i.ignoreLocation,E=void 0===k?O.ignoreLocation:k;if(r(this,e),this.options={location:o,threshold:a,distance:h,includeMatches:f,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:A,ignoreDiacritics:C,ignoreLocation:E},t=A?t:t.toLowerCase(),t=C?z(t):t,this.pattern=t,this.chunks=[],this.pattern.length){var F=function(e,t){n.chunks.push({pattern:e,alphabet:W(e),startIndex:t})},M=this.pattern.length;if(M>N){for(var b=0,D=M%N,B=M-D;b1&&void 0!==arguments[1]?arguments[1]:{},o=u.location,c=void 0===o?O.location:o,a=u.threshold,s=void 0===a?O.threshold:a,h=u.distance,l=void 0===h?O.distance:h,f=u.includeMatches,d=void 0===f?O.includeMatches:f,v=u.findAllMatches,g=void 0===v?O.findAllMatches:v,y=u.minMatchCharLength,p=void 0===y?O.minMatchCharLength:y,A=u.isCaseSensitive,m=void 0===A?O.isCaseSensitive:A,C=u.ignoreDiacritics,k=void 0===C?O.ignoreDiacritics:C,E=u.ignoreLocation,F=void 0===E?O.ignoreLocation:E;return r(this,n),(i=t.call(this,e))._bitapSearch=new T(e,{location:c,threshold:s,distance:l,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:m,ignoreDiacritics:k,ignoreLocation:F}),i}return u(n,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),n}(K),Y=function(e){c(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return u(n,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var u=!!r.length;return{isMatch:u,score:u?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),n}(K),Z=[J,Y,V,G,Q,H,U,X],ee=Z.length,te=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/,ne=new Set([X.type,Y.type]),re=function(){function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=n.isCaseSensitive,u=void 0===i?O.isCaseSensitive:i,o=n.ignoreDiacritics,c=void 0===o?O.ignoreDiacritics:o,a=n.includeMatches,s=void 0===a?O.includeMatches:a,h=n.minMatchCharLength,l=void 0===h?O.minMatchCharLength:h,f=n.ignoreLocation,d=void 0===f?O.ignoreLocation:f,v=n.findAllMatches,g=void 0===v?O.findAllMatches:v,y=n.location,p=void 0===y?O.location:y,A=n.threshold,m=void 0===A?O.threshold:A,C=n.distance,k=void 0===C?O.distance:C;r(this,e),this.query=null,this.options={isCaseSensitive:u,ignoreDiacritics:c,includeMatches:s,minMatchCharLength:l,findAllMatches:g,ignoreLocation:d,location:p,threshold:m,distance:k},t=u?t:t.toLowerCase(),t=c?z(t):t,this.pattern=t,this.query=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(te).filter((function(e){return e&&!!e.trim()})),r=[],i=0,u=n.length;i2&&void 0!==arguments[2]?arguments[2]:{}).auto,r=void 0===n||n;return he(e)||(e=le(e)),function e(n){var i=Object.keys(n),u=function(e){return!!e[ae]}(n);if(!u&&i.length>1&&!he(n))return e(le(n));if(function(e){return!g(e)&&k(e)&&!he(e)}(n)){var o=u?n[ae]:i[0],c=u?n[se]:n[o];if(!A(c))throw new Error(function(e){return"Invalid value for key ".concat(e)}(o));var a={keyId:L(o),pattern:c};return r&&(a.searcher=ue(c,t)),a}var s={children:[],operator:i[0]};return i.forEach((function(t){var r=n[t];g(r)&&r.forEach((function(t){s.children.push(e(t))}))})),s}(e)}function de(e,t){var n=e.matches;t.matches=[],E(n)&&n.forEach((function(e){if(E(e.indices)&&e.indices.length){var n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.src),e.idx>-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function ve(e,t){t.score=e.score}var ge=function(){function e(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},u=arguments.length>2?arguments[2]:void 0;r(this,e),this.options=t(t({},O),i),this.options.useExtendedSearch,this._keyStore=new x(this.options.keys),this.setCollection(n,u)}return u(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof I))throw new Error("Incorrect 'index' type");this._myIndex=t||$(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}},{key:"add",value:function(e){E(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n 1&&void 0!==arguments[1]?arguments[1]:{}).limit,n=void 0===t?-1:t,r=this.options,i=r.includeMatches,u=r.includeScore,o=r.shouldSort,c=r.sortFn,a=r.ignoreFieldNorm,s=A(e)?A(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return function(e,t){var n=t.ignoreFieldNorm,r=void 0===n?O.ignoreFieldNorm:n;e.forEach((function(e){var t=1;e.matches.forEach((function(e){var n=e.key,i=e.norm,u=e.score,o=n?n.weight:null;t*=Math.pow(0===u&&o?Number.EPSILON:u,(o||1)*(r?1:i))})),e.score=t}))}(s,{ignoreFieldNorm:a}),o&&s.sort(c),m(n)&&n>-1&&(s=s.slice(0,n)),function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?O.includeMatches:r,u=n.includeScore,o=void 0===u?O.includeScore:u,c=[];return i&&c.push(de),o&&c.push(ve),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return c.length&&c.forEach((function(t){t(e,r)})),r}))}(s,this._docs,{includeMatches:i,includeScore:u})}},{key:"_searchStringList",value:function(e){var t=ue(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,u=e.n;if(E(n)){var o=t.searchIn(n),c=o.isMatch,a=o.score,s=o.indices;c&&r.push({item:n,idx:i,matches:[{score:a,value:n,norm:u,indices:s}]})}})),r}},{key:"_searchLogical",value:function(e){var t=this,n=fe(e,this.options),r=function e(n,r,i){if(!n.children){var u=n.keyId,o=n.searcher,c=t._findMatches({key:t._keyStore.get(u),value:t._myIndex.getValueForItemAtKeyId(r,u),searcher:o});return c&&c.length?[{idx:i,item:r,matches:c}]:[]}for(var a=[],s=0,h=n.children.length;s 1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?O.getFn:n,i=t.fieldNormWeight,u=void 0===i?O.fieldNormWeight:i,o=e.keys,c=e.records,a=new I({getFn:r,fieldNormWeight:u});return a.setKeys(o),a.setIndexRecords(c),a},ge.config=O,function(){ie.push.apply(ie,arguments)}(re),ge},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Fuse=t(); \ No newline at end of file diff --git a/docs/public/icons/menu.svg b/docs/public/icons/menu.svg new file mode 100644 index 0000000..a121a2a --- /dev/null +++ b/docs/public/icons/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/index.html b/docs/public/index.html new file mode 100644 index 0000000..71df4c0 --- /dev/null +++ b/docs/public/index.html @@ -0,0 +1,3 @@ + dhamps-vdb Documentation | dhamps-vdb Documentation + +\ No newline at end of file diff --git a/docs/public/index.xml b/docs/public/index.xml new file mode 100644 index 0000000..58d695d --- /dev/null +++ b/docs/public/index.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/public/katex/auto-render.min.js b/docs/public/katex/auto-render.min.js new file mode 100644 index 0000000..32a7dd8 --- /dev/null +++ b/docs/public/katex/auto-render.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={757:function(t){t.exports=e}},n={};function r(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={exports:{}};return t[e](i,i.exports,r),i.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var o={};r.d(o,{default:function(){return p}});var i=r(757),a=r.n(i);const l=function(e,t,n){let r=n,o=0;const i=e.length;for(;r dhamps-vdb Documentation https://mpilhlt.github.io/dhamps-vdb/Recent content on dhamps-vdb Documentation Hugo en-us e.left.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"))).join("|")+")");for(;n=e.search(o),-1!==n;){n>0&&(r.push({type:"text",data:e.slice(0,n)}),e=e.slice(n));const o=t.findIndex((t=>e.startsWith(t.left)));if(n=l(t[o].right,e,t[o].left.length),-1===n)break;const i=e.slice(0,n+t[o].right.length),a=s.test(i)?i:e.slice(t[o].left.length,n);r.push({type:"math",data:a,rawData:i,display:t[o].display}),e=e.slice(n+t[o].right.length)}return""!==e&&r.push({type:"text",data:e}),r};const c=function(e,t){const n=d(e,t.delimiters);if(1===n.length&&"text"===n[0].type)return null;const r=document.createDocumentFragment();for(let e=0;e -1===e.indexOf(" "+t+" ")))&&f(r,t)}}};var p=function(e,t){if(!e)throw new Error("No element provided to render");const n={};for(const e in t)t.hasOwnProperty(e)&&(n[e]=t[e]);n.delimiters=n.delimiters||[{left:"$$",right:"$$",display:!0},{left:"\\(",right:"\\)",display:!1},{left:"\\begin{equation}",right:"\\end{equation}",display:!0},{left:"\\begin{align}",right:"\\end{align}",display:!0},{left:"\\begin{alignat}",right:"\\end{alignat}",display:!0},{left:"\\begin{gather}",right:"\\end{gather}",display:!0},{left:"\\begin{CD}",right:"\\end{CD}",display:!0},{left:"\\[",right:"\\]",display:!0}],n.ignoredTags=n.ignoredTags||["script","noscript","style","textarea","pre","code","option"],n.ignoredClasses=n.ignoredClasses||[],n.errorCallback=n.errorCallback||console.error,n.macros=n.macros||{},f(e,n)};return o=o.default}()})); \ No newline at end of file diff --git a/docs/public/katex/fonts/KaTeX_AMS-Regular.ttf b/docs/public/katex/fonts/KaTeX_AMS-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c6f9a5e7c03f9e64e9c7b4773a8e37ade8eaf406 GIT binary patch literal 63632 zcmbrn2Y_5vy+1zZ+}>v9PA|K&Q+8*zm#LfW$)@jadhd`*Ab^yRkN_cst`re8fFO26 z#RAAr;bDJIeHH}8=ksBCzJ@$SAHF|-WoQ4NbM9;*28G{0lVoS^y>st5-}3p^bJj&% zE|=SN!X>zNtz6rcUwmSDhs*VoZ8*AX_n~tx{`1$L`aC{A<#Gw@b|1bhseDj*%;kFN z>p0)N@8bQ3&h7m3F_-ISUfjgof6k>B<2c^Gb`yT8`!6`U@ARI3`V!v1>~j6!r2~7< z*|Re}iYGoV;#m0r4v0%s5ANTK&y5ETU3TQBzs}@wegMz=(*+mpKIa|(@8F9r*R!v= zTvR`F&XJ4B7u_%5^G|Sn^1^ct?cM&R vwxxE-iKO zC3`R4yYSQZ<9r>i|Co!qaBtT?&>&{3xLgIhVCM>UCV$VKe4Yj;f0HMlb%|{J^cnj1 zu71~Q*A~|vmo=jA*t&Mj@}X`j=G&SAN+zAlR?3BZxm+k$O2u3*6O 7zl;&!aJPC zdH(WM%HfdD$4(UTp T$Krh0--=4C>3!8bX0<1N~Or&vqvb3 z90H$kKR>@9#}8J@Wr;B%o5=*jp@8t%Ga*6Wm-h9y7*=tKPKj~h=EUbeLoJgihSaQT zln@~plLg68h {YojyW|NWG((uC}qDBHCjGEf$mqR=Pzz#>68i*hrG)1#8FGE~q3) z(7 s{Mi`WLosm>6yF2w;)7kOzgB zc!$6hnaMhc3)b*sILsuuTq>6FhO<#lvkM#sFiuv=jzr^hm~o1IwMou^_~9RaC8`cy zoHA6+5|qyKZdx&a%|Va6aox>siFoDS;jSjjXShX)5J_}vRJ1k~n^Tcc=X^`eOGIgS z;G2R>sVMEr1<46_*zzcKWPSBpMD1%aY|We77*AfmCDU>5x!c^7M1x*a)kP_~W^Ggw zHQ8v}9JV#z`KvnqOR`rz`=84R7$L6zryDjG+zRn3DL@F<{m(&;FL0f7-Rt@&)BO+L zclV9QE?Q2Y`~Gr&FB9Mb#F)t!W(xv2b1Xp6&Xij=`7p#Z9mMsv7uN?0j_?vLkc?1@ zn+AeGKZpvlD @i!<2~P*`1zh2l2p;ReC|1y1S{mC*MRmHyKZvb=6bK|{VvaldH?A 6kkqI(m zVJ7XcA{b_0s0lw8e^Sh30-+#d9G(l{#w{4^NTm!e6-wY8z7h6U0A&SwO+8g<^ e|7&VvA@h#5&Mm`V8Eqqnb#S#E~oHCH2XWCf;|isI~XFlh>Wpg9;Uu_PSJ~ zPbFWYf?M!be_wsYi#JpbX~ZI?7HaNR41e{7b*Am1RP$B;RF}0RXb nhhN z@DSQ_!}Zr({f@nT0cJVV5N?1jw|mf{yhDCRef}>w3EneaIjj`k$S}q(hGgJ2SjL_z zlpsBvY2*{Wi4m1hSt~`UWzdgYn}d#=jcth;w#nv1v95=+%~-Hdlod&5cHmnMf6im< zftKJ;Us>|o*T}LzB+0h@jX?jvwWlOo29SH9HPg!TH`(O%H20|vo1~EuTaq;K-up!D z46f(_ow)z4$3q25+PVm}eAOSVmXx3-2&xU9WT$>hUk1{z1y>G&FW||MLkIWl+PFXj zPY5jGERr*@uc?U~^g7zWq#oJ>WdrQ@i6LNLFaS+*;BT&ZfW-)h2>9XPe<&P=+7Tc> z-U+e-e5+JS6~>RA46dZh8f#cD$(9t{I@lWvtV`PORpUEh3#qr?GTge>(A3@9m2J>8 zE7a*GG<@j|D;990vE?4SJKLGA-nM~ThHa~LX({; Xoq}rHEVTj5UJWH#~m<@T)ggvn1yjn$J^x=G%KEd-eAY zy{}26(kAlXuonrv+S}`sWFL5=2YqrN?txvk;kOcKz)9EpT%U$r{qX7AuInevu0l#N zY2XdBk-=cW!6fe4a1ow4$$Xq3QxfhfDYF))rXuB12|C2xM|Q>8wCD}qlLLt1xlmTj zmKc&1%S~rJS|{9pyiv5=QiNgHA4I_)XCP+i;(c?Dv%?X|ZHbM?k2TFvo Os zvDhFybfSqdAu4zzT~q{-;S0eJMbssa5M%^?=o)6)nV&j)5e3nvFXArii$u9dVc&Ir zOp_VLi3*Y|%GKXgU&0RwaR4rwL%iuLOyVUTbLI*d`FWPtYmroCblv#6(MpQAy ~`l3IO zZe3NVpnGD3ilRCGO7&UM1K6!wtTHP}y~5;!2`S)-1ub}4S_)Vk0Xk@)$`RPc2VIYF z@A3op-Er#ZaFp4{VgY^$^Hcy4(*STb^*)=s^9FPYSB5C_aU8PpQyjD5s4!UJFAglZ ztZ-HYXv(!x$7@xB47dGRl9u^bgF*6(sBKE$kVL=4AmFblx-kPJaX*NCeso@f*v)WB zuHKyxC4XaY^MT >XIY;Ry?& zgTTQ>rq{g*AV5z{J*ZJ5;GX i;rF5@ag1&kK@||D_*k z6Q5#Ceq@y7eFU1p?B++H8T+6c?{wY8G~@PnoxJk!g-uS}1rINrg~~Jcp(f9O4Omi{ zg-#&OteqSZSs+%nZGrOeRmcYTHFLbp=}WwDZ8-LjEOT;@$YFF2`2fp|$Tn^;9kKZ; z S2i#i!52{mLq-GgqMU({CMjG|W_?A%parJqEU~>gI@~-Ni zyQs&6{e=@d=c*tH5Li}OF{*+js6STJmsQm=FRT8`Es=T}Ak|-0e (Bj5~gRG}2HfAQw;{Hs}Q#WAF|JDN$l+pQPHt_T^zF*WO4F6r&)E zEd9$1Ae|h`M2#BZi!WVv)R6w;7q%}DQ$QJ_rUmLTKn+Q#o bD?K)C@fuolp{_uIEOvNl@FYAWw&u~#qmX< z)UN6ucHt+ &1#$he zKiF<>_~tzuvO!4@H%{kDHiF{=uC3FN#ifUK@7O#Mo1HHYu@M5C)ttRq%EaF(6tj30 zOC_G=o0~q#` c13Mv$y?YyuTP8bsc@Wwfg`O)ftove0_2(K%Z- zWb$f+kFu;9?qJw{hS;)KpPL<_O__# YShMAB0e zT&7xG!>(1XU9QVnPGtY?ZEMFDmfP!1B!lr7Ue-siqE1Lt0mcA{itiH~I*wp1? z^@3b(v2{rfCj9#OvLs7hiAWhaureO^$W5O-1$m9lN^qx_e&Fp%@8q|k-Ic{~%l!FZ z&=6dpw3GG!6lD3N>rU4PT%Tc-e&mDqy!)2xPh8MIKxu? Jl4}~G#5IUs^Bs16@6-WtR)hK z!Pxac92rBmBmtNK$*13Y
rboF!i3^)6qj7s+Uz$tV=hy5_3bWchRy)#v_V?NxMAh!ySDi z4b^8%1iiLQUfX=0L@c+h8}!T6qC#+fS4O9m&VZHbiA4i_Ya`KA!^pVD@@wv2TG8UX z1s;;`cdKiM+B$lEjD(<+Oe7{BjSY56Ub<$UFC1@ b^iLv7cno%9foln>4_g`iH*Q$HZ1HHyu^Xvc>K5d2sGiFpu;%HIS$6>Ro6d*W zc7_p>y&S6w_pOp4cE(0X?V-?!te>ZB+bKzs1`(dEfY?DM(s0L+Mrg1x#8ZGLofM!c zH3lis`nTEeEfiZrim%?^pbwDV?aYt52!YH$59)P(=7kBGuxW#+B8>Kowlqj^kbW+Q z^qC-daP;X_&h zt{1sA5D=-!au>Qd6Midj;J*93d*y^i^z==}0}pE5^N{9b-Y+s7NO|+Xt+$}Q?_TxN ztF~+ibvzmAY0cJ$V3$k^J~*igD!MNnYD*SxLpJMzI-zh8U36Y&{YarTR*xskVpybt z;Xxe}hG6P9^m)*JKD^kip#A0z6AKXv0Tx&lf#Cs& ?#u#> zWfuf8!$_UYQI^F(At=DChc8?KyC|z>;Om)?Uqx!|y+=4{Efo3arj<0d(kHq7D$0zK zhwk0u6oggM=qJ@GJX0e1+>4{08 TtcO=w`|f|48XnP!xHM& zl& vvOrq;xzI&E$*I#$3so8L?@qfcXX0=a<~;4 zC}Y4EgXXGJzY$ngXCH9$-KdX2bMLw9hGU2JM>Up9Ms^=5{t6R6p6LWEQpyT8IRS4S z?}UO9FqG^vT;!0_O_^EshbultN&W&=XGzT{9S*RR1}cdxk?->Xn=6c{>^>5sE4zcw z7Yvju@Ca)snnGbl;EJ%)AM;6Kc x#2DsT>n9^Ed`6OCdpVM-#-yz}A|!VA&z)kzcWqT%r>IhjlzyFxM#?w|w;bcsO_ zbKOiA%okNlIB$<%*9`02AYwt{a}&ct7%|eAwG|ja&|k0Ds^6%7pQy-gkgxsxUt}F3 z+eFG%s}Ar6y0EG|1she#YCWg=M+kgUA-l95#6OA$ihu|O5nf0y|Kzwx674;KYZk~` zG3x3I)&J@48X}8U$(o@8w5V(}7}eiZUn8_s7q3sljZN4dB>(CggNd|efww-O@>jsy z3tZQ@Zg<_oc>C@ | jO= z+IZxWV-kQK34U5}PJzdaKa5{BnP8IRN0)>n1;}*sM~3_?0|;@!ryz1$@JZR@$d*tB z*%~QW-=ipP32HVdj=sFY)rA_1mKY0~yS^OIbwvkM(0fmddUSY)DSIjv*$&y7?mq9J zs2fUrFt&3z^4T;rPS?fv+)a&p=qb X-EMpkZ`tyfVX<3F3NJ$ zL+`V7)a$Z6W4hMcA_O8W%B*gQYt4 y!|lc8=)BOsaP*4vb~Gx$EaK>yqe vmss^mU_ynwguGmT6-Z2QF5RLk0 zv(|8OY_AWLS$)Jr6N<))q$dwFK*1DmP*6T_qafCWh+2KcB8Q&?=o3+x*UMA3RQUzX z`$>#}oGNlFfSg53_=86;xk4kNg=BXvF5?E6YMSTV9e2Kfz6oX!YN$vB#a;cyPgKJS zH%X)`0X3`MB<$X!Qd37JT+mIb9=bp<$Y$Eu0R0Go%Ev+FF7yR0fpvg>tR7oDQt3%D z?3+#QA+oQOT@|Je_zO8_sKv|C%pfQ{Y()-_H3bBe0E=4vd7rP6QDi{~=bPXqrjaE? zDS%!eIeEOvpNO_9kfXx9#dY&HDFQM#8oVpGF@J!=MyjU-vSEX@{E#747wGXi31v;Y zjePDYeP2-e!p92*@=l_Xlw!me|Bosu&$2uoRlFkI2dCUz))VloT??NGX4?`b-;JNt z*t0l7?vzO|02j_X`6-enB~MyI2I+SQ1coS0$vVO%r&}Thn(RPT ~309>tAiy7 5$3)q3b&iM#f-}> z*dQPz8Br3ioCH{W>gUJGNLK@RvI?*C21z#RqYI6C5EztEZ3V{m+YBJID0~ !H?Y0|BF67=)prVo~F@Xb)whEnMDU0`o0(1XB;2i%1vf&-@7gwUT zo39jtOmqX`1Z|*&3J+Rx{M_^@Ilv4zgd5kPD+2G8fLlbppjO#06oM$kGq?_i%T(}Q zb hprVT%Yxo-k;j#po0m90~06P}zTOpK93g6fWE0$S(BzJ;3Z%c+QnP|3{< zk92m4b$(6t?cQK2o|SQ7`}+h*l)cM0#LHJ*jkjPTbXjxT=2wws_H>2DJ3CCnFxv#7 zlNi$SamF%cD=BRVZ4Oh3y(7Y7-~%d5w3Fz9m{Aig#yqlO+!Wki+KCfVlXw~~fCYxP zZ$SWwY9qrSvV08gK5l#u= %r$=r>#mc#XHYKE$-({qMPALwC~4;u!)_ z2aT8=Lo}~A0VBkdc`hJt7?cOS@wZm 9-d!|(^~Z{BLAQ|6`H>+6<#sASLISN)UmcZTZrqqk6&MAMZ0?(1#~ zb*u;=^)TEbDGd?RZJ2>1Y+po;20U@mFYNR|1B?!Ivk=j+@r`RU?P9@ ??@nO z`{^?>@titmU?^S}>`a_~{?fg7Cpv@ix=S|=tql<++4}ySk+?tOzU6B+*s?s5+Ip!w zgHrU~)h8#92tqS-M<=(VO_GpiQEsV^C%tm2i0C)iRT}mBIxtu*+NQxsn4{_rqZJ;+ z)-B9eZYEds Np Cjx>56VyLX`ngWlJlfs|BkS@}8FN^3u$JLAy%B;Y37R+9uW6nYi5Ev0a9@ zs+)EIG1i^>{B WY*Is-Ex;6=z^w5`&BKK3W3y}H;=2~ z>hG5aQt81}-tdOuMIM=Y7Ao@mk=CYoIMPF@vR{F1Nlp+6Y^GJZ(BBp)FC2?EG$&g3 zmeR-*6Ib`tA88wX0#-u){Aw>@)T^*cCr~KMEdi(i?*y%J4Bp|<1#y5QJ;)FWaT?$V z(;ZG!hoI<|1))52`j`(f6-b)h;$5x!>Vl|Bz!2U}%*eKF9`9F3z&|)%Ss-YgKB5|R zJ#Im^0;1WI^ha}fy~`4MeClXHNhc+%!3>WiQ|U&E)PbfaG+jc7X!{La+e%dv?%}F& zGFUE#J%y`#LT-JqfZC K@uV5|ng)@tx(e9iB2)iE=W8v( zO8D9G=Mit^+k|{$AulraXVQ&nk%dkw6>k@*co&u2;-8PQzixWbFMf{Jt;T1bX;eT< z7rj%f6Hd80ahEW`T^xAf+MjF!m2f{$M8WNtq%pr*WJCktFja39I=#sqONV!DTQ T-p_ET0bq?W5&<0r@Hh#7uMp1*k25&pc!K{QTNAL cZ$qA%P$y-(bLo2So4IO4bh4A*u4@j_uKKR+HzCHUtw=YMCnd zLXx?Qd}`3Ik53qq6c2ZLj><;N)P(ld(aTItmf&|w3SuyKwla_^_4Y_IIWjP4#SBeu zZSTo1uQQ3|QSG*Q3@=R7&t9e7h->7}!~6m-xLhN+S7g)%A<8hF!@AhymM)4#MEWU0 z(>m>>NxeFxh?;@`>N7}wWW31e%%abENb)=J5S#oN*ilN$8RxcWy~$=X>C79TDacy= z9Y_M;fbaoE^f!Yj^1xXj $1crs{VSITEYEJmMp*td_scRb*7 zA;* PxjMw^ z@T t(Z2kPSh|AWliyneVw@(b2n4jUPPgc9R=kAuCW?u6T)i@_?kFI<1ff++gYt zQ!mp;P%SS2K{z3~rt((69Be twM)#p-_`=^;sKSAozU{}Y;Ph}@9!b$UaJYN2BkiS0y6 zLu=ENfJhL|80Dk;KDn3v*;T<%1H1u=!-|~iL@yAxy-Y{IOBO^R{9^3QVYuA2;Y}Fi z-g!vlYG|_;SddwtR>i}Iz24>`l@A=;w%=3Fs_e1_aSh2AT&R}lEd{S${_sOP=KDI) zXDPR&>(dhIqq09&guDGc*-YByHo@Z!tH47y_)wYpF+Bnb0)q*{WZ1og$VTam#x9+O ziu6b_iq=D_vl5smj6OX@{Qmdn5bvii4$zxo$i9x>99UJ~+g)Et=1qTpf(WmkNi{50 zBCZ@XW- z6$oMWQR<*OU9$NJm^F s$q?%51yyQW=XS3n54mOJ z7d3652R y)<(a; pk2_z(&+Qcn9)ERxPJ;i#akkBJErTZj0t5l%fGY!FhJ z^C>-u*}P$>=pFhIAF43+OOk!#P{~94M<`iv?%4*48qOO=%EkyviVDWqK9`aZiW((Z zLM9Ys^qUs!Gw4TuI8DImaZGmpRhl)waSXH6T8WV)FcOB+Z=@CzJBM04&y1W?I6N(> z!X0x-G}(FPXy_05XwFWSGsc2I$<;gcg79@Z3~v(Fn~`B!cbNuo_l@(>Mnck_Ly{(z zeq?8m+=6uVp5N$*7kMGxw0qme(WRM*0xWv9Wtuj0a&XZ|uOgDBotuKeKaK8j7?!!M zG`4#4*eY} I3UmVPWA5e87`m8tH4zw{LDD z9^bG^9@;k_T}=82R>LuMz(~DQ#A;kz`NYB%9V;`=m=BN4pVw#TJ^R~wEdoiK=UOWj zz~4;{T|p~_X>1uu!!Uj@4~BOjL*lps Xp=)V2qeyBvy9UC!43d=nJo%u zpholdH+PEhUgdZ$C#t$Iuv)LoYZg`QzDTs(E$FJ4%N u>+&a3uonO0fktn4z zO^p+xu8DWkdjw4vCmNeFX-QnPkX;-OOYMIeI!_-RQk!$g1CqhDdZiDmC58K9(Q*%~ z5apWHp;5h}59bEXXV;+ sb9a?UI8(F7g!pA(IN`7uB>a+}|B)pib{T> PL z6WLsO{*D}^wLYuA6L~kDV4e9f=gsUnysQ18$c3TBh{_R}P8!WoyHl#~OW7jr>Dv!k z?rp(xE2IF0#XqRNBACc7qIcPBFJy_es5`C+oO3SnC5$(V@fKOc0|{iJ?BEl6PWVc0 ztdpt0)>qhgr^4afeo<;|t@BEqcC^`; %Z-!Bebv!>wzKiZjcpi@9g(on-5kr8UK$dv-7t@p4X*l0ZKiEk| zUyu=hg(sP1C*p@Mt8dIE4nAN}yD(%*nq+pY2%*NNbnUhz-M6I2AjU|~UF-exmbGp? z>X8z4$o1a#Tv(PvAGyTv7NRoq_9Xw34zIPdO;#*hRT@f$ad6}48r2)@c=VzaM%{@~ zzkpE#t18@+U;|+cb%uqIm=lo=7_(vF3_l%a)SI3izhX2<&F|dpO^1$bxzJCHo+UnA zw03tR+EYU-I{5g93N|&$Tkj487xax^9EOi9E3{q@y#GH;YOmb!fMJvx6xBw(q;03= z?O5WMBm n_bdVi%1*a*Wdbbf2n8`a^jUUM@{T$q>DGI425s%gS=y>Y=wk7#Z0=mTI^VtH zY7-pawZvlOdVeWBvS#gxV~=>V2jhw$#SH|lh7=O~MdbM!ni07AbB5IwQpLobJKgQ> z%hqNijh_0u4=8=grW+ekDuy#A8V#+h_Z?op`Qc+7`HRaQR5v@jVq6y#V||Cn@VOVW zeb$++ pV>VwGu-cYSj6Ybgu8-CF|r`h%8LU|q64SVP*LJ>JGVCE)Uii2_e4Ix zx)ZF?ot7y2pS-#*eDS@oaOR;^n$_0QCd;jzx;tsqCENkC!4yIW7z7j`B|(WF%zEE@ zNNIieSYu<~?zjQh&@E9Vd14~8G<4a6qVK`WFxwsPPKaux!;<7?AIQ>70^YU?Oc 4rMGLaG`uBWDk$Q6jrQKLn`jCq8@EUSuH)PEA>epZZJ^D-$ODvE2EhWJ3p|$s= zC~3&{(@&Vy@#;9c0l9`;t+j;oa9EAz=8P@OP?0HvZ8HUC+RYR}0nYp#;&X(((>F`v z?w>FXv3hOw+L=4`n}F=c=8SI6{TwWvR<-H-y w4m- zw8z)ysoO7B+K`a4JR&hVi%g0uQ=bASmhfEDEMXHg$nrld5Ml?V6r?*8W J@Z9m8>G zPozYDEBdG4KkLPzpoHjusHlJ5O)SCGatSX2hYKZXd7IbUwp`!e%-o1(?e$kJ;%3~_ zdSW`GK&%H_le~eps6M6e=q#MlP&f>tv9>1sgiLUWNHxvMLl$dfXQZU!5%f6}+}3;s z1)0Qqbdk{; *msdX#NAHHcQC9-ESl$Q7nh_Ay8fuIqBJ`r>P6^0Cphb2!Vyj zmf3)994R|T94uk 8 z*~q42W<<)M@z;cK*a)_0K+J2nvW-{A% s}FY zV$q+2N Qg^BvBnN7)A 5GX0Q1?3wiQfAAMFYWTXJP^OxTaGc3#czV~-ZvJsFn1)UTdl2{b)@1rMAdUW~b zTVuILK1tw*c&evUDN*v86JBrfV;|EymxywusNZ;_CA?G6%Zp63J!tP<95e;&dLYEE z+op+{Xf7)2V-wW$)7y|ywvg2y*^I_UtWdU;l`BNa{93kYoT^Ppfkv8D7#gf+`MK~- z8p5th?{HlUN>qw`aSwyG49kqN7xOvFEHH8+2+ZCg1+I~U3UBnmRgiH+n{3()>+wR) z%gJpy1c9xF`-wm#B{L^494=Iv(DuT_5O2%Op(pQZ|Du2pQUbt=;==1w$e9OHw+_K4 zQ9@E(>Ev}`%MvZsB4=J_7;n5T5*7tZHCNkO^_Q7JT`#4zE3P1G*nfr 3OtKsqgM{JQb|jbb_-@F4?>CL-G5Cf>;>)qXUnzk|F(G~l$| zUcFsgIZvfay=4@Hs48Zg3)Fb=sB-*Q1}!vaQC5c+s~G4cSlNey9khljgX21@@%CcO z_hHOfro!^MjJ^(3IzAX9c$T9YTn~n(j8Q{EiDe#ZHVX$TFkrnV{WTj!^=H+eaUctk z(DXDi0-uI}rs>)=sxMVv;#W12MmCq~ZPnY%KcQ(B!@>X!8I4eHG7sl8n+Z{v#bVi9 zmM87Uhc{;a;Ep55!)Cy`WNM`mm@>wgFh*St ? k_C3FkAcfY9%6g1rSO#)_%T?+R0is>GpW4KOlEazj=$*lvObWMHS>B@jqs; zt~LY3 &gNK6Hk=QaqUG^g6KZhAD+!$O1lTdlwR_neV^@2!?% znC$ $a>NFLG1s5>Bt>jf J+hr=LI^EU3Aa(vc zDH@BeAHnfe6r(q&xUHX%&(B+Z!Lk8t${`qGog81$qK#g%WL_eOP7-%>X>rKA=5mBv z3obl80qbW3wH#6p=(^poWz)e`t^G)bx%<(^y$G8j;i$Z7%Vs6`L{3~XuudINy=`UE z=aas;WTDx=XDA=_VU4&CYx=FjYk5WR5RmI@ qY*uX24y(h=jMS1`DE_l# z0Gl9`i0f0KR3- bdZY9R=GKwO{ycSHPY5rSr{(1dQpkaWW`-6anmMu@NtbK z%fo^kQ#=SRY#%Y!kI44?joA*5Ok}SQWnW{LiQZML1WGV`UFZ6DR8ZY)_sVG ZH`t>-Gi*HZ*EBVgNf?DyGbk2HAH-^ zK%^f-WU4==-wo6!niKWaa!k4Je#=w+4&bKx9aJ+|A 4*%uICU7k zT)FHvLy^&I(GGs7=xdt%0dg+)sc8AFA`yT!(a85cBnZq)an;culAj>EIN!;JLZfpz z5S~+>!2lCOD18|8u1O@$@`O~=Oo9s-;IyF7A4Yn%)Wu98?2qs2UOBWX6yLwFur Z~B&Z$@R pKGczJXw*xQbVp(IK)$=QWahK3`6+T~ z%O>`q#(n-+V?+C(O3q)ttlVOvcY YK99%@|fiDiq$VoKTpNBiq)qiqOY*YKY_omMFtzLOx%1+z>B&x zd-8+MlcnkjEC=0nl^cv+yLV~h?TzR@W0BQ&>Af`PYUX-@c>xnJEv^m>p1G<+F9394 zyyL?+0tB*YF(5Jxp}QL-pQ?-&(E%cm4BI4=kn1$;5U5Q)Ct%^XKuxmoq6V2(-%Tz= zsy;*`o&feWO2=?Y6*oP#NmQ|nYQBuMQCLky5z?wy8UD#HuU_wYj6T-709@EQ^&8i9 zkVk|XAr$p^$b3A84POi;=q4XUKTdTs 3Z4CmOQU^RbWi=z7mbKZe#icC0o_2-|O6OQ)Y>+vRDSibn(iTQva% z`_Lim48lfR)9irYtJUZNjI)d7Tlj+u2WsOP7{q>POrw`AuZ?NWDYRFqW}pw1#s9>Z zs(-Bhv3TI=r`c#*Zur>100m}KSy?hx{nvjRVWTDnF^dQOZsv-93dc I`RT=EeLI3$Ocjc;28*vZ_ZTYrs+57ELH%=BAHA+^He<37>#6- DsgX-Ig8L_n!)-X1KZ+e?WKa(FPurZ%7nIj9GgmE zjqn>T;bat+4Kd>KYse2FmZ%4ZI|3U7!=!Jdt1+N#-a?R^!qVnB7l6uIX31_4o?Wn+ z;ee|VGyMT9IXqtU6mpv^aK{m%j4|WIrVJXh*odj5qoC8mfRDXydX>jBc^z?=vD_!u zoE)5&XimGWj$yV5Db|kK6RF0Q=F~u6eRJA!cMhSU_)+ejSC1$Aw}yN{PrlRR7qq2C zpQigoZ&=%x^#$yn+Q9_l$kc9l6L(_g5tonAnc3f&(G7{y5W-zrK2~NMYC{Dtc=C$H z*UT!hStyRv%cX0^ZDP}eRl|R5Wi+I{CU r9%)%q*7<& zszwi?rh9Ba4@E H8bR!eTh3XipQi+jLb{a=^? zlIiY|m$!8^EB-t- `e8OQ?V5&H zXPOw`48+comL }Dxginempolr~D z$FMqf^47B)#70pD|8`;H_Wbm>$*-Gt SZe(agVN2iSB$B>x0#`XB@$D z!8-nx?o#b5K+~S77wJ#vLLl8auJfJQ*Q8?(p8;!Jw3sx_l`jNQWtr291K(Tv28vWn zn^0Y&Uw01`;_*~d!{UtF;dKw(47aGwYYW)a6x;Ijs`eI{%YdrZWbLh7Jb4SmjNS-I zq6-`5t@dheqcf;SH4<$o)+fwR`-sIhm7HKl>dU(SrJ8)5^&&@1st@s;5QK;4i(M)t z=4IF)c*K0Y49J50v>nzvn+iCw38Ii7V$0ApUH~p?BEr^{F>{g2pu6u;W#wRHrTi$8 zzUS_S!&mXeD)oMBllpb9_v`#~yp&*{cb_Zko=1(v^{I&tTYA*ZQgYZoq{!2|vTvkL zZH{KJg8lPYK0fnFz#2wnMan%tmR2C|jxAxMXT4|`9RrQOGJ0%850Os&jbYN`JW)M) z-CrA1E&HrsU0(0hdw^W$z+VWsQZCpv7kmQ{5JZu AIj40Csc-}dMad@Wgqkp2Cf%HCyB0eW3 zOJhd}$myA*Ky`o-iw!iL^)NRjFQ0W3ba^@+Y>){q!7nCYj?N8d8OP)CRLm!u(G0py zutW3Iy?iVVO;9CaG~o&1H=zl(EaYx6H$owLl6gs){N!C{9ns3hSTr5;d%XS_8&P8< zdiEj3;E#nGu%&|3Fe5D&xiXALEJs}va+a(@pE8F#9`YbNi1tcE&qZuP6$m430_N!Q zk)ui^q8vQ}xrJ nLZE^gQbaGdH{jPdO7cQ~% zS*$&Yx*C~RhVF9idchaVhh!(lbX<$G%MSU&P*)}%s2kZb|2-=bZE_6Nw(4tVr4rrK zIqb9YbzMUvmozk&4bmcqa=$za>uTeAPS_5~iGV)cpbvboO4$dXEMvn9PMe&NrdVZp zR$=-w_Q`yjZBMGwr)9yLdyqUphN=NKy&6*e6)x3RIa+nnO@4*`66{jF6xU-b^C#h` zn@S7uSAR<%J=^F6)F)TdC+P28b*?5}abu#yqCpYmD88~yK|3CT7zhvCb;rfQ3P%T@ z0}4GY3mUpQ)>MD~;frq~Nm200)n8O!M7WIcwoiW9Lr1-y$uGH)Z}76W^mo+q$|QI5 ziqia>5d8V*O*doz1#C4yl*v9rL(W3^7Kc$3u3>wo#}`JtfL>JY$%_~m#)w=nr({#5 z3HwU^#n9GjSl8G@64+>iR$r|CLiS Wx|it9H&N^eE!}i@ zN|{`9=5QeCYpcJh(F@fW-Yxi7s0GkuStX1OkPb|TR(NQw{FkXYQz@n9It)`>`nTx$ zjQ8p2Igy7Sw!GwUXBXBAA&$+Y$H%z lad0j}EN0WDXCG!=SZ~G)n_G*wIq^5=*4v)(S(*9z z9dCE8%aLJt7_s*{*IqVP?!qAmnf8{s&&NzQ+rEJH0kH&*gZbQI*TNbuq3m1CBgTe^ zWu{4G#|!eaC45MIGw+e7y$<+QrMqAmO}P{p=uJkGSh!(aj p){mg zP1Cu?ZVa4xO`y@f^U~eectY9gp?yd||I{zmb(%&x7BJCD5DdgMR61|{f>`H`i%;Ha zVLRn`Ac%sU83;AaTo&~@mpv)Qy>;RjoXfmX{q^TgKl9nUWRn_4AM&MczN9V3 ~gH6Z6shi+c(B{Y8~Sf6pPI~_uhLy>Ug3|4sE?UGn5v|Dkk4E z^FTKe37~I>BMsKyIzt3-^S2K042z79IL4b4!g&ViA-3f;;`~6lLJvC=sousXV$145 zP0x9kG9WrU-o7Aw`;!bVKh(4#7$Emx*9xDQ- t@$Ou9Nr=b z$Dp?8n%N}OW$<67$jUL`UcpIgF!tfA3Omi-%N}fP$OyyCD+~chk|8IR{u24ek9JA2 zF8}B!@f@yK-L$V%MzTos=Ld&lP2}p6qJ@gUon$w*$iNyK$!;`E@i1Y#bL<`*8ocdx zL}r?F+XqX<4?IYo@!Wm_2}YJ>R_Od~RB>zXa*}8weIJ?>`Ugd%>z*MmQ(ece2e4W; z)YsFB ~C2zv`p4ATu#46EvYdl zO~4d^`BqSqzh%Nc=Uz9c8-6oZPA1BcTx-N`=|!mSSv?^8@@>g{?N|lX`JcYIukiY4 zTwQqx9Yt~?7VmOLWNW-DlbpB++gkcN_~)@gYR)XfuElR|v5>QQ8xwqUQ)RvNseS8g z9>?oj-^!#gtM(~@zDOT-)dO+Sdk#_L63C>H*ZO!XO-O{2@`R`3FJVK0nl)<5HiBRD zMO54-4=Xwp^^~Y$r-QvCJAQWb{bb%rVoa_;2M4_c8>%Ujhmm#D$=;WU_WtVMpCtO@ ziuF5DbYrKH-&zXR2WG>W)gAqpb`!b6&d4EI{GD@R!@2x0(>{#&3m9>buVlnH3ZdGl z#F`h^NXq HbL0=81C{<8Ydl5I4SmnMHt2hk~sU9KlzIduv< zOBwJye^&h^AVi>fdV`|*h2P;9>`3IwE3tE%Wd879sy9iN>=lI 5bY))D}O1|y Eq!iX%C;j< zbiz8Il~7QkSLR9)!7ul18_JrLs8-K;t*sA+bnmiqX#l<0dce#a$1lTUwB5(Y-iG}+ z9j}ZHkL@aqA3T%CJdB!~$>zcPejoveMA#?fYxsrmMA=JTYhfR5t@&IM8|d`QieTAa zyXg54cCka9$!gd)axt62r(3OSed@x?e=kO)h$LFkKOZa^RHJ!Sj}LcWjLoUwp|duq z`pW%&3BSND*s`nA7-kchcWuk$GPF-FybP`NDt;-BLG**=WgZiAnMS2%mS IL1vXy1m{Y+9i*d6Td0PrhpJ1d{bL(H37|n^;4kR}^@yhy~N;T7`c8xu=T@GiD zB$df`zh!m)Oj!30cI}TKU#o<$O`H @ z?_?-vvPhfF2m+G>2kwx$Mw%T`Q9HoJ*n>5tc=1&P@MxGzn&Yna$25yZOQBZ$8VX^{ zC5`}Gas9r%qNpqQ{X{#q%bC-*AmNwSsYWW4-=!BllDK{SsxbCC!jgE|U3|>K)ynrg z8tFN&ef6biXXb3Dp>N@rJ2KpqzW4-j_g`yc<3>|WuZRsbiP#=b^UmwvmXh$>o+MRh zOdf0%*!5sCG}xK##4N{W_QioW4a16;n>zSCEHH&&4hI7qKFKgP-kc@|yjHfAX1+`v z7Qb_+jnQJyR_kf%IoZM1EvsHrZu%K!<$w2R2*1k@dlI7Kvw7gUsQOLHsqpuMOD6OX z@o iw-%dZF92G(BX(ksfYTooKI|88(WcvHz%t0K-Z$Q>_N*hCjzZriMAD$ z5Qb6fxF_1^TE?V0 i!k6g|W&O{E`FmZw#eDfyd#0@_3T%Kmq23 zXGE4yn2W2zoG8rm>{AB8SmWkQLnq1!EQo~nm65oA4?^-C4073Z?$aNNb|(nxy70D} zvy!|iVMz9=4be(lGWqpyc&zVzZ;qqxc3;)dKz8i#SO@m+4(DTfrd_%Jz2pl}2$J43 zx-pCffmJfy7Q+TDZu(i&%~X2sW+a;C?MZiL!t0J>C+hJD)(1aJ2GD729GJ*jJYViT zGRU?Pkg&4*Uw@7bfOGS `bRc!8^*SV&gW}PX9QtXVS>%&eOfkv z1(-TZ{>*}b%({b_tjw&Bhm7$u)w+i+CH{t7K60POdhp{mQC44w6>0i-B~>42yyZd5 zbNc Bbva}K;ojXVUeW(a9(}%xK$^f&@V)+G9EweLcS%}%G!&oqHkELPOPESno-@8Y> z|1@Tg!0L>pnzfI!C-9E{nLWWjo*yJ6nXHVVyrz#;GIb8+NGyZUPXG+i`oudf0m1&9 z%V4iSD_wW&xJ3^@P4sKFM;3>-pH!WWUrBY>ojc_9`MpCmsu69n-BNO8MjjAVPJuq4 zS6j0idkp0mu{ct`06B@Xz5bCeaJr!Q|Jj_dRy65C6jPt4Z7k=AZCfbfOV$Nt;y?61 zqR9z6YD~RAzwO+C*KAqKAJ0vv?9_;x9#T|v;N68i1lLS{lKd%#-vBfDV_jewXYwk` zGqXLUo~4TsH+3SWSdCk99ELJ8p0O8m8^ItZc4hy}qQmLdCZ}O!hQl-0hKpE9&3Ed- zZcZJQW_90+4M9<~7muGGYV2s-ag)tHRK9rhlk>fGtM^^WeoS;GVt#i^KlU8b2L8Y9 zzC5sv>e_qmjP~7*rZFML 1v)*1 z6T}}};fFh1y1`MXwqgQExB$*p5@`Ct**kG2&Cj-IG`l6T%LQw+k_kRcB`A317fhAZ zS~Hdwp(#$6-#-J+P%SX7*N~r2ahW4uiMU-0@68T 7 v z>T9lHhO5&hL5H*nytrPS`s9Ic{xxUtgv6}iM)7sdkO#;R@%qvWUB(-(rFqwA%JWYv zo4OcO7tt_5V&TmGJOfx`jgN3w>8uqtmx_IZ_y0i#Ugc4rO8h1JkZxe1V4p_D?I~Ir zxL{!Nu1=qgvv1%iDAmGVAS_=qtnS*xyAb>lJwHUq3(S=$y+cS(PiV~H6tNghV*+`f zpGouOOyZjXjJzw8=-|e5@~PJ_1jzi5ns~|%oW_lr_PDt`mChv-VCNybd&kJ`^o#{@ z=z9u}DoN9l(=?3CqX~+Pb)?CiTpJik(xW+M0vO1h*__^z$$Bkp!i%gnO5D2b+5@l{ z1~iRy`4yL5ih=hxd0X?@)@@X%d7&a05y0GoH8#K_0QmjdefJXN5pc}gZs((G;Xcod zeKV#D5-(plI0K7BK#^njo2&!VObV5!(c&*)t0 R zBPyqaFCB}XA8pB*TEO&HgLchYruVQzb9Z4+*bRIrahoIveIy@nQ5uE8kqoGJ1cSBc zc(9orO%qy!b%wHA7K_2&nx5TD#efZ^1;E7VV1oS4+wQy*-2S>vr!~U)_=XxH4k{^j zUNhpuHgjt5cD9}~oi!5{NoUaBbR%cFO`H0eg<1FQTT^u&$SN!h1gMt8Xtg3IRPtGL z#wOQ-(u8;&j9o_11`D zU E!uo4C(Q^%v^M2T87lT+@X4$#6;sAbFa4XCYhiSJY=HG` z2BVSI!6Ek3582eI7xOh|ItMG|fHP}i5=Xbw*)gNdMW2n1XxAHo|5b@KdW(EgENWBl zKUD^eslQ!X26#}hE%itEH->%T>#t+J16}anQN%8WdK8^b@8+u(qM?ZP4acPdzJV}T zr^a+9dIpbEf7OiIX&;xeKc;)35&08^$R9gH UYB4UX zP!+O!fX;zML`EPX)ERAA$vs7&1X%ZsY8!U1_!QUes+O%HW0oxbx4yI9X^G9&S(?IH z=d>;;1gDOrj&g0b`M_D u+nvFwMoqL#!}6-34Awbqk~>tdV>6>r!&LO!*y`qL zibRns;1r0)rMjUo`(GfnF^Z@y5~e&-7S=+;e8jzhjwklaSTgrFWx;Do*Gken~l8-cP($~Ex-LlZ= 7B z;>8ZWH}?;Rg8)U;0AX(=6|oVy>w(sg*FS{IEOB$TLrI!1JtS)KyqhMFNeoa)lB!hL z_Axv;sKtwRsV#6E`3@C8O-THoi)!&Xqn6j|BT)AE!~bgRu)Om;&)%#vu;UuiY{Zx> z3<%#K@2HsJ0ACM81Iz-TccFO$6ozl030kYlv~VGOr0BHwV?IW0Bo6f8+oBs qMYOArhufPIQtFQO;Fx9|A67MXj2y&9DgYm70YD!(M# zKDp4I4df0pubSs1gu1kO)5du|wH_$ O z)|nMHsU*^xIBz5N(Xd3%uoc5WpmK4JHNyck7mB3e@#W%JQ#)CAV9FB6Q4|_SZ$r-p z{gtDH3oa7?qEOa-!iY+iixr1jHjba*H8?6Vix!raQzu2u9d}3?9S&H Lm5y!(`JE8*@h Hoze$AN4MhkypP_{jMSpTJ>jsU~t zi*_>5i*@n7QpSsixD7&X-3CloFrdhkHv?r)myTHJ!+&z%6-w(Z5#P3=mCY#EWjl41 zEprRXLN2KLS|FA3y(w+83rqRE!Sih5UTlM0ZDD!d!pa2g*VNQunQ30rS~$6ie(R(3 z+tio(F 22$c2OVsex=D_m$I(7;=N*+&Qv*Sia*`jZYDck za;^zj&;!V -63hv%ngh zmNh-2(z^4P)-QqCredv@yqbwdsrdc RX_C 1xQNP5Q28XNET{devG*J^ zCvB%~IXY>FGEyDOoGoIDl5P>n jKT6SM5kZ5m>$b&PF8K)YDjGr4d|n^{8}eNY`W6RCV9i8jAhTmsFzcAPt42~$Hy zX!NCFhRYWpHETpVAW|Bm)5&9IB0*1?-b4gCaBw=iVGSA P-(i-wL0m^x=m4pfpYZxvP@fYxiEC>Uv|E-TY~|9e(*E6Z-3 z;`Ie)7gwm?c+FyY>|ll2Ur|@8d+afb? ^@Qz3+-q$w&fG?LeuSq3Nv|PKG zlcGHYhH6_^wBl5gYnIBih)b*)VnS8ZA|(~tH@{~#zx4w{^E{antM#$RbY=0nGVSA! znXMxTAw6-T;}o5=yTR8O@9D@sd$m$NZ2Lzdu4>v2MElP$A24{X@#?2;z>0o)E+j-W zh1#~kkTLpMgC3(2+2>G;(>5368r7WlmL0Q=2pH=(OoDZFrgvGNe#Q{ UV9q4h l=`AclqV49 ab4<^>fa^@1BH{zO3_W5Dcg}yS3=^xP&&Poa;5p&H&laS94m?_^?@oG zMNL=?zBY@YUPzuWX#ahW#Qiw^(Cbqlq*72BmBb>oN|G!ZX>bU-p{(`qeOPKUs?>Mi z3|tC4yN4hKY6}MI)NfM%4K3`y=MmpMQ+SaN4KMVRIEk(S5~M=Ks~**nfv~XHh#y=E z7{}N|Yl>^#=|_S*a23n&n`q4(sD-ljw=Q_&{EmF4vEZ;cmlaBCwF;H@zKT`C%7~Wv zEhdu#u$Pohi&4&XZ|Zj&uzB4E|B=v60Y)E6*{|wg_ZVvx!&g%8b%~m0qfPVp)`l(d zkd0|{7WJL?0w@O0dAEy~u!D!RI+sp==vs{S8kXO2TNJAW8_XBqeiv;0s5Pl~vSB*x zrkj@C9u`$b9I{A4N9r>1h_qYC! 8d5RX&N&p{{6yT1q z_C;qJlVL{(=p0Qiru1z3y}2rrT|ao7oMwnx6BLP0S;Z-swY-JNJC3CTa6KmtzAZXL zy)^xW&wQyr{{ZHlc478u(1 toC<-0mWQX{o3G-G+6^>Pw7qUZGh!q=IC8O ze^}1UKJ)s8@Zo|LyF!f{D^^&*Bn)#Jh{R`oz>*U-ijEYi@ZJ(NHD@A2&GA$;SkTS^ z2r4S-!~|Uos?gj9kL%%vj{QL{Apqtw5n>HM1f15Zm~a6g1$Gsvffo&CW=?4hj?3O5 zh?4j&khz+dsEnGyBMK2mFx@07tJj2JHPQ|q8GyAqZ!;gi@#iUV09rAe259FHe?f{e z7z9cf$~gfDQ>TL4(U}}NZ_RIRgApWg?Zog)>;?G2;IJJjdq#BrO8BnpIV*mNdNrl& z4%(X|r *Fo{I^Nw!7on`y~Lh(06;Z8 zt~ce)ss+aQ{0RY =S gs%8l zaCrrwrQ(#Oo~(TA1^8=A+KMHIgJ~P?7)G+p4`%nSepwt1fnX=mMd`?vDIp&Sh1@4| zi#CjD8lM5QP|rX@K+P&G2Ci!2Dsc^l*>LQSfZUs4QyL%5IPeH?X@Mi`l*~`mq=7Ex zqD9YYH2w2DRvjEvNE&POp-OP-AyH)lj^Uw(x_N|g0~V@fPK{Pv5)6KhN#M?l<(nf~ zDY!X ~-xv44P( zk}yX>FqXnvUo%!n@P|jMG(M+7Uxk4#tx|%nh*S(|(-U@?16kCU!_E{odoUGsyQv=& zk7!aai#2WVK%z6Y`g~h1kVtW4Jx+EonK$)4Xwzcf$6K(gpdM z-NnLU77i>A2H6kETNIt_$cW51Fx7L=?@X4WSfNtU;Zir7R;Dy3gtiE$5#xTXpLft% zz-Hs54QpC;NfA!{L3^z`;B#WzAX0RGcJqy^7|~@jT_4$1hwlD6hwEc%=-3%6C*`ms z8k}$eWd)Elo2pHHVv |Uz74PRdMN~w#Aa>O?Ej^m(~lK2hAXiKE=Kw468I& zR4Rk4pQbK+TY*SJ{h>>tIWNW)tkJ;-nz~SpD#%SRW4J #lBG027jMj!wI-30tvSwCGHh7Hf$fdl+PJ z2hM{Wjs<@BdYw8Q`a N6$4{YX4I5V4U;46jy z5Wfk$=1R=+U^xS4Jx*C6jGTF8blA?$FjA=keu+l!Vc~$f5%DovOX^~Qtmz{7=!OA2 zEm#{Z5!*H9UFEgw8ASKctr~5C@vMwfxk4uMuYj&p (;F9aM)c?&1QmqI6t}s_7ExG>T1B+vRGK41@h z6eXg0dUIA;{gRD(du{ct^R;#jw3|F-tM?Z9S}!U#T!JOi 7?6oe)qCnwMEsPrzJ zo$|d%Vpj?Kimjt*{av0MkN=9r^Sc($U2=urb2L7?fembbc8exkb<=T+_|ZdOe7Ddv z`22!8(X47K*T7N?LmSmtESj8cr8QNi`k1bKdQa=(N}U~3dkxI*i59bFX?vh1&t;w( z(}kyr-^upgzOHrFk_C$|_f4McyL|D2B?H%la 6uO=v3MM7ncyI@Dr#s~lkmkd8HR$|A+# z;S2%wXRnKgaQp_O%V&?N;G&RH# gCTv}ie?~ioUuv3mJGz*))+?GCVdF>5uDr^cjT2Qi>EI1mwWZ!m z{ZeftOMSy{bHvDL@1w!d2{d$QN2OFG?%1(oyZG~8d_KuC>61@C{Y2!lIE+U592Vtt zNXF@?6HeEMpsNw6N!ijA#v%;RkiIS}bSJO65=Q6E8qw>5!#;KDml$l-q)pW|lzz)~ z`g@KVIC!_C`x%+Q|Ni_Jhh~D7QR#>UKx7;}^yw#5!)Yo|OJiVm#(?Ua4$(oW;zO5W z|Nn>RI|)O 6%uY!ikp hscx6UO*;(m(}43|@<*vrDq8~~g2#1qwY;Nd+gGFC(MYS 4@Ed1vexx1lWg5(
*PR=T%S2XN#8ew$evwF!>bGX~6SD`IxtFBoy*#&C^#kXR&SJI>w zWg>YaOZJ2e(114?M@JH0MrDBA@K4CLzLll--IuMGQVY?-jU8*vs;pZ7&5N;nMauId zquE!@vuIe-lr2Xpy19!Yf#zY!9A7TS6~;5lpnRHezZjCrfogRKP~4Oe&%`q)$39aC z{(Gs@LGAM0Q1n{V#?dBt^^d^v45NA+8uPD2wMJ!7Y1HDc;Llr|ffq^ePsCw8Pbt^I z4m~!XPe8v9EHh|Q#W15V3^k&DIE`q_CrdSaOo9B3xhsW-F6_i`=dIjE%vm;=2EQdb zG|a=`;|Y6?m`%xcV#G_vSMpBz?CiR$a4dx3R3%%A8Cga*AvwmgLPBLH6Nu@vGT{uH zJ8iJ51I&_qW(I$wRpO?x)U#8wMLV!1WFiJfF)*r%d0;H{Bkba(fPAhKmh;{Ws{ 8djvp;`zTex~! zqG4kfj&h*ex~45yE$*9I6P#(&vok&v|DuK)b7_E`z@)-Xy3#O>3YF-=J0`cU%<)td zHB>pY28Yhvx&p?~ipsn;$M?s5lHtc+F}I14rk_0j&K#40Xv}?!gtO9mc5aNL6W)DB zYR~vk{LyHgakB K&w`|uALOjNPER7fwA!__a$tFb3nC)~hcy>ZC<4j$lwA`yE(ryDZ z2Fu%7Q8MO(72W7|?5-udm#5#A2WT(}GaNuQFb?w8UZb=C^y~a^gL;57weK~p*|y3h zd8`f1TWg-hBtv U?X__pSMD6f zYyY2p6><+Ni+1s0SccW mx)fjMFGSReN&a x2+Js30T6rZmZl_PD8 zOxUaBeRC0+m?zY&M!AkY$fr2+0l9cYZ!`}62|h#q2D1rz)!=u4CDw}dgU?vRR_I{D z;+uT6ngGF^O4!xGmvm5$&}&nF!7I`SWFh2bsc$7Lm*X7#KgkH3T6$;jVi -gIUCTr!9T&$l+msmd +;Em|M&uY5wbo(d@5Fz#JhKg*M8Bu4KH?Z6_r4X2J zpR-ZB|2f@)igOsQU?ljfZAaw{$7h$GtChR7MZZ!yooBM@29|Ez-Uz{!utg1r1pJek zu&P~uAA|16!cJdyMMLV>V1i`N27swdF(qTt!i04xEY5@nQ|HPX_Dgb#r 2!8iOed!tO6 2A!)EA5ypPSe1bqStGf z;)?{h(Qz_T-w@A-PoMm`)ns;FHbrit7XqDyR&zGau|-vu=n5Oki^wW{zRJ*AV{lHb zSKDp*`EzSzJLMB^I-fnpYfNX1Ixp3z4SIE=N$ubbh-}&W rqUtW0= zYG|pi|EuZ^e&UA8fc^X!8Y;DfpvSx`Rxj30&{U4?wCVjs6;e){)U&rHq=b@c8!?%u z{<~8tdpWS>3UU>_GzLMX;c{y?<7u9xJ#3_xCS#bj8B@!IPu@qYNW_y9RV*1S994eo zeWb3#Iw}ji-*~Q^mrmai0n!!iXxI!i@3{CRf*b9@L_wK@PPC4&W3m ?;S6U%}f zoo4H+S3mqPl+^E-GF=*V+tuyD1NPCxYSCfVPM-noDXi~Qm6cqB{a*gJwmp5f3Th)} zEjK1D&%I?nmBXqLBoy=K3D@!{lSaH7pD3RuDi7z%CUJ10^77g5&XH5($Lf9IV`wOI zeO|E1$%i(1y9l5tnn8{*yMO_4IoHrHDFVb4CZfxt+y+&k1b*u9GZ $># ?r6;m6yjDAnV{e%EDlZ4zXq@?)Y>njg;z*s`UU{#U@2kYj130}hXx6UJo|hdA z0{5f!=bLQ~tnnR=FYODE8>uheo;}ae-A$&0-3_zT54SBVz+{74B6?d~N#|p3OsDO1 zTEaU(58rb~au$(K&{1AAFVg2cKhoL3Bsa_UmPRMBEM2xu|BV+|pk(uAJvP6$ZVl*N z2C)M=L<`s~zzML^MYe*N=1X_ML97IOe(bP+X(=}R8GNC9CceXta=TAyKG6m(pNtTT z=**EY9CRnmO7M;NOO}%9dHNTgLy3}h4E;XLY*?;TBAUyAc`Ge$b3bjF Sd2ja~S5%Gpf~j z?h5N(jWHMdFX3SGE)!uTB(qwJEecCXtic 3WrgZS zuAlR1f90$@*hC+#W}L89f$bx*SV0#>W} ?1 zuiVE_WQXO7j{#{J|I~=w2!zWeyFpTeENmtnB1`t+kjiL~%ENRXc8D|s(<@&$ZwYi5 zGtNHtgh?CT*hrrx4SO8GAar)3# T&k2CJ10|$NIA1lsh|BWCgokY+%nnr&3$RtS?l3P5k(A)@~(>va#6F%1J>B zfNQFTx&sFYyhGbnFg~b+cIMS**(?_QPtkml5o}K}wrqyt@D4{@&Y2~!!f=;(jG>cJ zW7(V K*^KKmjkJz zk$(DAI1c;e=1}RyFiwIkyM-lzzr!~8%9U(!C}gHxy^TxGKY%knowT8s)q#AexMbxr z)p*vy=cJ%151oz0#D^H<1U0U}ASPxP9-W|@&+Ih5;xrUER%7RBK%R;Lk%hc%X3wg{ z$b>D!-y}x5yOS_4YEM1P63t{r-Xb1Y*(a&3%*d_AKe|Mc_5$oVG2`&fh#M?}&YaEk z(EW#*!2 nQW}c~i43j7C@=)u-xer>8(i%xxIDD8N z2V;O8*#^gMkq%gZt?_Hr?%T&{FSp()!BW^)3d;ZjMiXp;-vEqS@M_kWh{Z11C&8$` z_J8M!XKaf@y)2&k*}l|; $OEL+J&PRYFY<9kZwK@B?D~R0(|C z#eN};*C55;i*TyI@~@kU{;DhW47l0=bnQNYYf+((iN#828wdY+mxJ+><6B6B0Ua4^ zz+#4y>9OcHzn`8%W3=b@F!0bIjHW23!Yd4|5{Z%FR8~T0Oh4@u;Oq?=XR$8J!#=G8 zdZSXlwQW6ImXe*!3%2TQ@GGEo7>!Po1-M%12o)A?-*NLTR`b30v_Aj5+5GKq`+WcX z-!LcQw-v%4tkM!#YC)Hz{4}^~0m)iK>VzV$0%IW$hO&)NHflYUgJK4F*xG&RO3A z!>KtdU=h=WIg2RTG?OO5xi#a8Lx1_JY+jUR+Au#wmQ{1L#VvGNq752cIM?My7Us+o z#go8!b7QmZ8%=-49Hle$9A6>g5aDdAL*g$O!`Vp81)E23Q(mIrPKKK}ht8}^z+8G; zsjxGwQt~$S`uxB_*;%x>L}j$DU%P(2*}QY-+JAr0Z0=9`d}_5?Yhux-ESCSsYO)== z&usq6Bh{6Qm*sX}o25R(T2oUK1@srXtzER?s8v{{T%g RTC{(l>_KWI<#W2dc #7Flzh1(Qr8WN=4@w;5()vhlg5JEFmkXsu-H6NfOw!x?0~jI)*G zl}~I4iHRThPmDyLAv1V*8EC(Ld2UfoVd0A}S WGw?>q8L%W|!6O$ mnuCu6?A_xikDU1sZRjg1dJNQM>5!y4G~ zq}#J+o2^eYHs<>CR@?v|=i{40*03nI=nDjLAi%i ULS@0Mi#9hqKaaU!% zY$ nFOUE8mg`y7yyxF zMKE)!vghSzJ4r%({;Za)uURaw`rthUP#EB8jAE1#N`AqnAT_OFGsq+C94o>>ghnO! zk}XqYB6mYB {OsKDli2*9$d;t%ZjRKl5DZ`M_)U)_ME9*L#2CoA3LI?=$~W z|NrrSRMb%PV)35hKa?yeNd}C8ErIWs7L*<=%PQ+F+gbKV*&D%duqXJX;71`T)D+qs z8VK9NE#aHOKP+!8zqev)#j{horW~8{$IAN3%PU{6GF5d~JyP|j$c)H0t81!Xuc@fH zE1DmDu-09BW$lSrD0Y49qxg>ar*)g_PSmfc|6uCIsY4B(hQ@~Hr|qA1YI^SU_UU&| ze|`EVGYV&{nDJ<%qj5*$ubP}qZA}N7zMF6*W+X04ypZ^D^RnjqnoqS9w5)7-yygAY zDXmA^+->{Xe%?N{{i62d%-WgH%qpC9<*ZY)n`ZxH&bm2&o_o!_z`Qr+SI Cd|l^@u$e^?YYp_OeHoS1(VkczNZ6t1e$PuzLFHw|md)J=`bs z&FFh#P0^Yg*G^e`Y+c>DU#