diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..07f6c89d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,127 @@ +# Global Classifier AI Coding Instructions + +## Code Style Requirements + +**CRITICAL: Never use emojis in any generated code, comments, log messages, or documentation. Use plain text only.** + +## Architecture Overview + +Global Classifier is a machine learning platform built on the **BYK Stack** - a microservices architecture with specialized DSL-based components: + +- **Ruuter**: API gateway handling REST endpoints via YAML DSL configurations (`DSL/Ruuter.public/`, `DSL/Ruuter.private/`) +- **Resql**: Database abstraction layer using SQL files as endpoints (`DSL/Resql/global-classifier/`) +- **Data Mapper**: Template engine for dynamic content generation (`DSL/DMapper/`) +- **TIM**: Authentication and authorization service +- **CronManager**: Scheduled task execution + +## Key Development Patterns + +### DSL-First API Development +APIs are defined declaratively in YAML files, not traditional controllers: +```yaml +# DSL/Ruuter.private/global-classifier/POST/inference/deploy.yml +declaration: + call: declare + method: post + accepts: json + allowlist: + body: + - field: modelId + type: string +``` + +### Database Operations via Resql +Database interactions use `.sql` files as endpoints, not ORM models: +```sql +-- DSL/Resql/global-classifier/POST/insert-data-models.sql +INSERT INTO public.data_models (model_name, deployment_env, base_models) +VALUES (:modelName, :deploymentEnv, :baseModels::jsonb) +``` + +### Service Configuration in constants.ini +All service URLs are centralized in `constants.ini` using `[#SERVICE_NAME]` placeholder syntax: +```ini +GLOBAL_CLASSIFIER_RUUTER_PRIVATE=http://ruuter-private:8088/global-classifier +GLOBAL_CLASSIFIER_RESQL=http://resql:8082/global-classifier +``` + +## Development Workflows + +### Environment Setup +```bash +# Use uv package manager (mandatory) +uv venv && uv sync +source .venv/bin/activate + +# Build required BYK stack images first +docker build -t ruuter . # in cloned Ruuter repo +docker build -t resql . # in cloned Resql repo +``` + +### Testing Requirements +x- **Linting**: All code must pass `ruff check .` and `ruff format .` + +### Branch Strategy +1. **wip** → **testing** → **dev** (three-tier workflow) +2. All PRs target `wip` branch first +3. Automated validation in `testing` before promoting to `dev` + +## Component Structure + +### Python Services (`src/`) +- `training/`: ML model training scripts +- `inference/`: Model serving (prod/testing environments) +- `classifier-service/`: Node.js mock service for chat classification +- `dataset_file_handler/`: Data processing utilities + +### Frontend (`GUI/`) +- React + TypeScript with Vite +- Radix UI components and TanStack Query +- Multi-language support via `translations/` + +### Experiments (`experiments/`) +- `base_model_training/`: BERT/RoBERTa/XLM model experiments +- `ood_detection/`: Out-of-distribution detection research + +## Critical Integration Points + +### Authentication Flow +Routes use `.guard` files for auth checks. Private routes require cookie-based authentication validated through TIM service. + +### Model Deployment Pipeline +1. Create model metadata → Resql `insert-data-models.sql` +2. Update dataset connections → `update-datasets-connected-models.sql` +3. Initiate training → Call `/datamodels/train` endpoint +4. Environment progression: undeployed → testing → production + +### Configuration Management +- Docker services defined in multiple compose files (dev, inference-cpu, inference-gpu) +- Environment-specific configurations in `config.env` and `sidecar.env` +- Service discovery via DNS names in docker network `bykstack` + +## Common Anti-Patterns to Avoid + +- **Don't** create traditional REST controllers - use Ruuter YAML DSL +- **Don't** write raw SQL in application code - use Resql `.sql` files +- **Don't** hardcode service URLs - reference `constants.ini` placeholders +- **Don't** bypass authentication guards on private routes +- **Don't** use pip/conda - project requires `uv` package manager + +## Quick Reference + +### Adding New API Endpoint +1. Create YAML in `DSL/Ruuter.{public|private}/global-classifier/{METHOD}/` +2. Add SQL queries in `DSL/Resql/global-classifier/{METHOD}/` +3. Update service constants if calling external services +4. Add authentication guard if private endpoint + +### Running Services Locally +```bash +docker-compose up -d # Full stack +docker-compose -f docker-compose-dev.yml up # Development mode +``` + +### Model Training/Inference +- Training: Triggered via `/datamodels/train` POST endpoint +- Inference: Environment-specific deployments in `src/inference/{prod|testing}/` +- Models support: BERT, RoBERTa, XLM base architectures diff --git a/.github/workflows/ci-cd-development.yml b/.github/workflows/ci-cd-development.yml index d5b01599..481f4e6a 100644 --- a/.github/workflows/ci-cd-development.yml +++ b/.github/workflows/ci-cd-development.yml @@ -4,11 +4,39 @@ on: push: branches: - dev + pull_request: + branches: + - dev jobs: + lint: + name: Run ESLint + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: 'GUI/package-lock.json' + + - name: Install dependencies + working-directory: ./GUI + run: npm install --legacy-peer-deps + + - name: Lint code + working-directory: ./GUI + run: npx eslint "src/**/*.{ts,tsx}" --quiet + deploy: runs-on: [self-hosted, development] - + if: github.event_name == 'push' + steps: - name: Checkout code uses: actions/checkout@v3 @@ -16,6 +44,7 @@ jobs: - name: Stop and remove existing containers run: | docker compose down + - name: Build & up Docker container run: | docker compose up --build -d \ No newline at end of file diff --git a/.gitignore b/.gitignore index cd3d2221..dd5fb7f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ tim-db +global-classifier-db .vscode/ .idea/ # Node modules @@ -33,7 +34,7 @@ logs/ test/logs/ **/logs/ *.log.* -*.zip + *.bkp *.dtmp @@ -41,4 +42,15 @@ test/logs/ # Python cache files __pycache__/ .ruff_cache/ -.pytest_cache/ \ No newline at end of file +.pytest_cache/ + +models +data +output_datasets/ +venv +dataset_artifacts/ +venv +/signed.py +/minio_presigned_urls.txt +minio_data/ +temp_models/ \ No newline at end of file diff --git a/.llm-contexts/README.md b/.llm-contexts/README.md new file mode 100644 index 00000000..5e87f8bb --- /dev/null +++ b/.llm-contexts/README.md @@ -0,0 +1,5 @@ +## LLM CONTEXTS + +--- + +This folder contains the necessary contexts for any LLM tool to understand the contexts about Ruuter, Resql, Data Mapper, so that any AI-based coding tools that developers use like Co-Pilot have necessary context to give useful recommendations \ No newline at end of file diff --git a/.llm-contexts/byk-stack-summary.md b/.llm-contexts/byk-stack-summary.md new file mode 100644 index 00000000..6db4939d --- /dev/null +++ b/.llm-contexts/byk-stack-summary.md @@ -0,0 +1,28 @@ +# Bürokratt Project Overview + +## Goal and Vision + +Bürokratt is an initiative by the Estonian Ministry of Economic Affairs and Communications (MKM), with technical implementation managed by the Information System Authority (RIA). +The core vision is to create a seamless, unified, and user-friendly channel for citizens and businesses to access Estonia's approximately 3,000 public e-services (and potentially private sector services). +It aims to function as an interoperable network of public and private sector AI solutions, acting as a virtual assistant accessible via text and voice, 24/7. +The goal is to make interacting with the state radically easier, moving beyond simple chatbots to a system that understands user needs and proactively offers bundled services, potentially based on life events. + +## Development and Architecture (Based on Codebase & ADRs) + +- **Open Development:** Bürokratt follows an open development model, with all code, planning, and project management happening publicly on GitHub. This facilitates collaboration with numerous development partners. +- **Modular Architecture:** The system is designed to be highly modular, with functionalities developed as independent, replaceable services communicating via APIs (ARCH-001). +- **DSL-Based Development:** A key principle is Domain-Specific Language (DSL)-based development. Instead of traditional programming languages for many tasks, developers work with YAML (e.g., for Ruuter configurations), SQL files (for Resql), and Handlebars (for DataMapper) to define service logic and data transformations. + +### Key Components: + +- **Ruuter:** Acts as the central router and reverse proxy, handling service requests (POST, GET, etc.), conditional logic, and templated services. Mock services are often built first using Ruuter's capabilities. +- **Resql:** Manages all PostgreSQL database interactions. Each query resides in a separate .sql file, exposed as a REST endpoint. ADRs emphasize strict standards for SQL: no UPDATE/DELETE, standardized formatting, no SELECT *, no cross-table joins (enforcing denormalization), query parameterization, and security declarations. +- **DataMapper (DMapper):** Uses Handlebars templates to restructure JSON outputs. +- **TIM (TARA Integration Module):** Facilitates TARA-based authentication and JWT management. +- **CVI:** A fork of a front-end component library for reusable React GUI elements. +- **PostgreSQL:** The relational database used. Schema management uses Liquibase with specific rules (SQL-only definitions tracked via XML, UUIDs for all objects, use of ENUMs and TEXT, mandatory indexing, defined schemas instead of public). +- **Liquibase:** Used for database schema version control. +- **OpenSearch:** Increasingly used, particularly for its Query DSL capabilities. Storage ADRs indicate OpenSearch is a reference for document-oriented storage for metadata/structured text, separate from blob storage (like S3) for raw files. +- **Rasa:** Used for chatbot functionalities like rules and stories, though intent recognition might be replaced. +- **Centops:** It's the central orchestrator for the entire BYK stack +- **DMR:** Stands for distributed message rooms, a message orchestration system which can recieve messages from one Buerokratt chatbot and pass it to another \ No newline at end of file diff --git a/.llm-contexts/global-classifier-summary.md b/.llm-contexts/global-classifier-summary.md new file mode 100644 index 00000000..9e96bf88 --- /dev/null +++ b/.llm-contexts/global-classifier-summary.md @@ -0,0 +1,168 @@ +# Global Classifier: Detailed Outline + +## Core Purpose and Vision + +The Global Classifier is a central intelligent routing system within the overarching Bürokratt network. Its primary purpose is to accurately direct user inquiries that an initial agency-specific Bürokratt (chatbot) cannot resolve to the correct government agency or, if necessary, to a human agent. + +### Vision: + +- To act as a smart "switchboard" ensuring citizens' queries reach the most appropriate point of contact within the Estonian government's e-services network, even if the user initially contacts the "wrong" agency's chatbot. +- To improve the efficiency of the Bürokratt network by reducing misrouted queries and the need for manual re-direction. +- To enhance the user experience by providing a more seamless journey across different government services, making the network feel more like a single, unified entity. +- To continuously learn and improve its routing accuracy through feedback mechanisms. + +## Key Actors & Workflow + +(Based on the provided image and PRD) + +- **End User:** Initiates a conversation with an agency-specific Bürokratt (e.g., BYK A - Police and Border Guard). +- **Agency-Specific Bürokratt (BYK A, BYK B, etc.):** The individual chatbot instance for a specific government agency. + - Attempts to answer the user's query. + - If BYK A cannot answer or determines the query is outside its scope, it escalates the conversation to the Global Classifier. +- **Global Classifier:** + - Receives the escalated conversation (likely the conversation history or the specific problematic user inquiry). + - Uses its trained ML model to classify the inquiry and determine the most relevant target government agency. + - Crucially, it must also be able to determine if the inquiry does not fit any known agency (Out-of-Distribution / "Agency Undetected"). +- **DMR (Distributed Message Rooms) / BYK Central Com:** (As per the image) This component seems to be the central communication hub. + - If the Global Classifier identifies a target agency (e.g., Agency B) that has its own Bürokratt (BYK B), the Global Classifier likely informs the DMR (or a similar central routing mechanism) to forward the conversation/query to BYK B. + - If the Global Classifier determines "Agency Undetected" or if the target agency has no Bürokratt, the query might be routed to a general human agent pool or a specific backoffice for manual handling (as per PRD, "calls another endpoint within the Buerokratt chatbot management interface which will let the human answer the question"). +- **Bürokratt Backoffice / Agency Admins:** + - Receive conversations routed to their agency. + - Can report conversations misclassified by the Global Classifier as "irrelevant" to their agency. +- **Global Classifier Admins & Model Trainers:** + - Manage users, agencies, and data sources. + - Train, evaluate, and deploy classification models. + - Review and re-classify "corrected conversations." + +### Workflow Summary: + +- User asks BYK A a question. +- BYK A cannot answer and escalates to Global Classifier (via a POST endpoint). +- Global Classifier analyzes the query and classifies it to: + - Target Agency B: Informs DMR/Central Com to route to BYK B. + - "Agency Undetected" / Human: Informs DMR/Central Com to route to a human agent/central backoffice. +- The conversation is handled by the new target (BYK B or human). +- Agency admins can flag misrouted conversations, which are sent back to the Global Classifier's "Corrected Conversations Module." +- Global Classifier trainers use this feedback to improve the model. + +## Main Features & Modules + +(Based on PRD and our discussions) + +### Classification Core (Model Inference): + +- **Function:** The heart of the system. Receives conversation data and uses a trained ML model (e.g., fine-tuned Gemma 2 9B) to predict the most appropriate government agency. +- **Input:** Conversation history/user query (JSON payload via a POST endpoint). +- **Output:** Predicted agency label (or "Agency Undetected") and potentially confidence scores. +- **Technical:** Deployed as an API endpoint, optimized for low latency (target <1000ms). + +### "Agency Undetected" / Out-of-Distribution (OOD) Handling: + +- **Function:** A critical capability to identify queries that do not clearly belong to any of the known/trained agency classes. Prevents misdirection and allows for appropriate fallback (e.g., to a general human agent). +- **Implementation:** Likely a hybrid approach: + - Training the classifier with an explicit "Agency Undetected" class using diverse negative samples. + - Applying softmax probability thresholding as a secondary check. + +### Datasets Module: + +- **Function:** Allows Admins/Trainers to manage data sources for training the classifier. +- **Features:** + - Per-agency data source management (represented as "tiles" in the UI). + - Supported data sources: Websites (URLs), information documents (PDF, DOCX, etc.), conversation text files, and textual descriptions of agency scope/services. + - Indication of dataset deployment status (used for training or not). + - Dataset versioning. + - Ability to download generated/aggregated datasets. + - Status indicators for synthetic data generation (e.g., "data generated," "generation in progress," "generation failed"). +- **Backend:** Manages storage and processing of these diverse sources. + +### Synthetic Conversation Generation Module: + +- **Function:** Addresses the lack of readily available labeled training data by generating synthetic conversations. +- **Process:** + - Uses a larger LLM (e.g., Gemma 2 9B/27B). + - Takes unstructured content from the "Datasets Module" (agency websites, documents, descriptions) as input. + - Uses zero-shot/few-shot prompting and prompt injection to generate realistic user-bot conversational snippets relevant to specific agencies, ending with a user query needing classification. + - Generates examples for known agencies and for the "Agency Undetected" class. +- **Output:** Labeled conversational data suitable for training the classifier. + +### Model Training & Versioning Module: + +- **Function:** Enables Admins/Trainers to initiate and manage the training of new classifier models. +- **Features:** + - Ability to select datasets/versions for training. + - Initiate training jobs (potentially via a CronManager endpoint or UI trigger). + - Track training progress (e.g., via SSE for UI progress bar). + - Store training metrics and metadata (integrated with MLflow). + - Version trained models (aligning with ADR VC-002). + - Tag models as "outdated" if not trained on the current set of classes/agencies, preventing deployment of such models. + - Display evaluation results (F1, Accuracy) for each model version. + - Mechanism to push models to a testing environment and then to production. +- **Technical:** Involves ML pipelines for fine-tuning (e.g., Gemma 2 9B with PEFT/LoRA) and experiment tracking. + +### Testing Module: + +- **Function:** Provides an interface for Admins/Trainers to test deployed models. +- **Features:** + - Select either the production model or a model in the test environment. + - Input a user conversation (in the same structure the model expects during inference) to get a classification prediction. + - Helps validate model behavior before full deployment or after updates. + +### Corrected Conversations Module (Feedback Loop): + +- **Function:** A crucial component for continuous improvement. Receives conversations flagged as "incorrectly classified" by government agency admins from their Bürokratt backoffice. +- **Process:** + - Incorrectly routed conversations appear in a tabular format in the Global Classifier UI. + - Triggers notifications (e.g., email) to Trainers/Admins. + - Trainers review the conversation and re-classify it to the correct department/agency (or confirm "Agency Undetected"). + - The corrected, anonymized conversation is then added to the training dataset to improve future model versions. + +### User Management Module: + +- **Function:** Manages access to the Global Classifier's backoffice functionalities. +- **Implementation:** Leverages existing Bürokratt TIM/TARA for authentication. +- **Roles:** + - **Administrator:** Full access, including user management, organization/agency management (adding agencies, their IP addresses for routing), and pushing models to production. + - **Model Trainer:** Can create/manage training data, train models, push models to testing, and handle corrected conversations. + +### Dashboard Module: + +- **Function:** Provides statistical insights into the Global Classifier's performance and workload. +- **Metrics Displayed:** + - Number of classified conversations. + - Number of reported (misclassified) conversations. + - Number of pending conversations needing human re-classification. + - Last model accuracy in production (based on re-assignment rates). + +## Technical Integration Points + +- **Incoming Classification Requests:** A POST endpoint (e.g., `/api/v1/global-classifier/classify` managed via Ruuter) that individual Bürokratt instances call when escalating a conversation. + - **Payload:** JSON containing conversation history/query. +- **Outgoing Routing Instructions:** After classification, the Global Classifier needs to communicate the routing decision. This likely involves calling an endpoint on the DMR or a central Bürokratt platform management interface. + - **Payload:** JSON containing `conversation_id` and `target_agency_id` (or a special identifier for "human" / "undetected"). +- **Feedback Loop Integration:** An endpoint (or mechanism) for the Global Classifier to receive "corrected conversation" data from the agency backoffices. +- **Common Knowledge Base (CKB) Integration:** An API or interface to pull unstructured documents, URLs, and other content for agencies to be used in synthetic data generation. +- **User Authentication:** Integration with TIM/TARA for securing its own backoffice/admin interface. +- **MLflow Integration:** For experiment tracking, model versioning, and metrics storage. +- **Ruuter:** Utilized for exposing its own API endpoints and potentially for orchestrating calls to other services. + +## How It Will Be Used (By Persona) + +- **End User:** Interacts indirectly. Experiences it as a more seamless transition if their query needs to be handled by a different part of the government network. +- **Agency-Specific Bürokratt Admin/CSA (Customer Support Agent):** + - Receives queries correctly routed to their agency by the Global Classifier. + - Uses their backoffice to flag conversations they believe were misrouted by the Global Classifier, triggering the feedback loop. +- **Global Classifier Administrator:** + - Manages user access to the Global Classifier system. + - Onboards new government agencies into the classifier (defines them as classes, adds their IP addresses/routing info). + - Oversees the health and performance of the Global Classifier via the dashboard. + - Has the final say on promoting tested models to the production environment. +- **Global Classifier Model Trainer:** + - Manages data sources for each agency in the Datasets Module. + - Initiates and monitors the synthetic data generation process. + - Defines training runs, selects data, and trains new versions of the classification model. + - Evaluates trained models using metrics and the Testing Module. + - Promotes models to the testing environment. + - Reviews conversations in the "Corrected Conversations Module," re-classifies them, and ensures they are incorporated into future training data. + - Monitors model performance via the dashboard and MLflow. + +This outline provides a comprehensive view of the Global Classifier, its intended functionality, and its role within the larger Bürokratt ecosystem. \ No newline at end of file diff --git a/DSL/CronManager/DSL/callback_formatter.yml b/DSL/CronManager/DSL/callback_formatter.yml new file mode 100644 index 00000000..80e21aad --- /dev/null +++ b/DSL/CronManager/DSL/callback_formatter.yml @@ -0,0 +1,5 @@ +callback_format: + trigger: off + type: exec + command: "../app/scripts/callback_format.sh" + allowedEnvs: ['filePath', 'results', 'taskId'] \ No newline at end of file diff --git a/DSL/CronManager/DSL/data_model.yml b/DSL/CronManager/DSL/data_model.yml new file mode 100644 index 00000000..88c459e4 --- /dev/null +++ b/DSL/CronManager/DSL/data_model.yml @@ -0,0 +1,6 @@ +model_trainer: + agency_data_sync: + trigger: "0 0/1 * * * ?" + # trigger: off + type: exec + command: "../app/scripts/train_script_starter.sh" \ No newline at end of file diff --git a/DSL/CronManager/DSL/data_model_progress_session.yml b/DSL/CronManager/DSL/data_model_progress_session.yml new file mode 100644 index 00000000..4ad1adbf --- /dev/null +++ b/DSL/CronManager/DSL/data_model_progress_session.yml @@ -0,0 +1,4 @@ +delete_completed_sessions: + trigger: "0 0 0 * * ?" + type: exec + command: "../app/scripts/delete_completed_data_model_progress_sessions.sh" \ No newline at end of file diff --git a/DSL/CronManager/DSL/data_resync.yml b/DSL/CronManager/DSL/data_resync.yml new file mode 100644 index 00000000..dc9c53cc --- /dev/null +++ b/DSL/CronManager/DSL/data_resync.yml @@ -0,0 +1,5 @@ +agency_data_resync: + trigger: "0 0/1 * * * ?" + # trigger: off + type: exec + command: "../app/scripts/agency_data_resync.sh -s 10" diff --git a/DSL/CronManager/DSL/data_sync.yml b/DSL/CronManager/DSL/data_sync.yml new file mode 100644 index 00000000..fe85ca33 --- /dev/null +++ b/DSL/CronManager/DSL/data_sync.yml @@ -0,0 +1,5 @@ +agency_data_sync: + trigger: "0 0/1 * * * ?" + # trigger: off + type: exec + command: "../app/scripts/agency_data_sync.sh -s 10" diff --git a/DSL/CronManager/DSL/dataset_pipeline_scheduler.yml b/DSL/CronManager/DSL/dataset_pipeline_scheduler.yml new file mode 100644 index 00000000..60f124cf --- /dev/null +++ b/DSL/CronManager/DSL/dataset_pipeline_scheduler.yml @@ -0,0 +1,5 @@ +dataset_pipeline_schedule: + trigger: off + type: exec + command: "../app/scripts/dataset_pipeline_s3.sh" + allowedEnvs: ['signedUrls', 'datasetId', 'majorVersion', 'minorVersion'] \ No newline at end of file diff --git a/DSL/CronManager/DSL/deploy.yml b/DSL/CronManager/DSL/deploy.yml new file mode 100644 index 00000000..df025882 --- /dev/null +++ b/DSL/CronManager/DSL/deploy.yml @@ -0,0 +1,7 @@ +deploy_model: + trigger: off + type: exec + command: "/app/scripts/deployment_script_starter.sh" + allowedEnvs: ['modelId','currentEnv','targetEnv','firstDeployment'] + + \ No newline at end of file diff --git a/DSL/CronManager/DSL/fetch_chunk_without_filter.yml b/DSL/CronManager/DSL/fetch_chunk_without_filter.yml new file mode 100644 index 00000000..6f12fb8e --- /dev/null +++ b/DSL/CronManager/DSL/fetch_chunk_without_filter.yml @@ -0,0 +1,5 @@ +fetch_single_chunk: + trigger: off + type: exec + command: "../app/scripts/fetch_single_chunk.sh" + allowedEnvs: ['datasetId', 'pageNum'] \ No newline at end of file diff --git a/DSL/CronManager/DSL/fetch_multi_chunk.yml b/DSL/CronManager/DSL/fetch_multi_chunk.yml new file mode 100644 index 00000000..f52a735e --- /dev/null +++ b/DSL/CronManager/DSL/fetch_multi_chunk.yml @@ -0,0 +1,5 @@ +multi_chunk: + trigger: off + type: exec + command: "../app/scripts/fetch_multi_chunk.sh" + allowedEnvs: ['datasetId', 'chunkIds'] \ No newline at end of file diff --git a/DSL/CronManager/config/config.ini b/DSL/CronManager/config/config.ini new file mode 100644 index 00000000..3c7934c6 --- /dev/null +++ b/DSL/CronManager/config/config.ini @@ -0,0 +1,4 @@ +[DSL] + +CLASSIFIER_RESQL=http://resql:8082 +INIT_DATESET_PROCESSOR_API=http://dataset-processor:8001/init-dataset-process \ No newline at end of file diff --git a/DSL/CronManager/script/agency_data_resync.sh b/DSL/CronManager/script/agency_data_resync.sh new file mode 100755 index 00000000..631258b6 --- /dev/null +++ b/DSL/CronManager/script/agency_data_resync.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# DEFINING ENDPOINTS + +CHECK_RESYNC_DATA_AVAILABILITY_ENDPOINT=http://ruuter-public:8086/global-classifier/agencies/data/update + +# Construct payload to update training status using cat +payload=$(cat < /tmp/callback_stdout.log 2> /tmp/callback_stderr.log +exit_code=$? + +log "🪵 Python STDOUT:" +cat /tmp/callback_stdout.log + +log "🪵 Python STDERR:" +cat /tmp/callback_stderr.log + +log "🔍 Python script exit code: $exit_code" + +if [ -f "$temp_response" ]; then + log "📄 Contents of output JSON:" + cat "$temp_response" +else + log "⚠️ No output JSON file was generated." +fi + +# Check if script execution was successful +if [ "$exit_code" -eq 0 ] && [ -f "$temp_response" ]; then + log "✅ Python script execution successful" + + response_body=$(cat "$temp_response") + log "🔍 Response: $response_body" + + # Parse the response to get status information + if command -v jq >/dev/null 2>&1; then + status=$(echo "$response_body" | jq -r '.status // "unknown"') + message=$(echo "$response_body" | jq -r '.message // "unknown"') + + log "📊 Callback Processing Status:" + log " - Status: $status" + log " - Message: $message" + log " - Dataset ID: $dataset_id" + + else + # Fallback parsing without jq + log "⚠️ jq not available, using grep/sed for parsing" + + status=$(echo "$response_body" | grep -o '"status":"[^"]*"' | sed 's/.*"status":"\([^"]*\)".*/\1/' || echo "unknown") + message=$(echo "$response_body" | grep -o '"message":"[^"]*"' | sed 's/.*"message":"\([^"]*\)".*/\1/' || echo "unknown") + + log "📊 Callback Processing Status:" + log " - Status: $status" + log " - Message: $message" + log " - Dataset ID: $dataset_id" + fi + + # Check if callback processing was completed + if [ "$status" = "completed" ]; then + log "✅ Dataset generation callback processed successfully" + log "🔄 Callback payload has been sent to status update endpoint" + log " - agencies: [{agencyId: X, syncStatus: Synced_with_CKB/Sync_with_CKB_Failed}, ...]" + log " - datasetId: $dataset_id" + log " - generationStatus: Generation_Success/Generation_Failed" + + else + log "⚠️ Unexpected status received: $status" + log "⚠️ Message: $message" + fi + + # Cleanup temp file + rm -f "$temp_response" + +else + log "❌ Python script execution failed with exit code: $exit_code" + if [ -f "$temp_response" ]; then + log "Error response: $(cat $temp_response)" + rm -f "$temp_response" + fi + exit 1 +fi + +log "✅ Dataset generation callback processing completed successfully" +log "📋 Summary: Dataset ID: $dataset_id, Request Status: $status" + +exit 0 \ No newline at end of file diff --git a/DSL/CronManager/script/dataset_pipeline_s3.sh b/DSL/CronManager/script/dataset_pipeline_s3.sh new file mode 100755 index 00000000..898b7619 --- /dev/null +++ b/DSL/CronManager/script/dataset_pipeline_s3.sh @@ -0,0 +1,285 @@ +#!/bin/bash + +# Check if environment variable is set +if [ -z "$signedUrls" ] || [ -z "$datasetId" ] || [ -z "$majorVersion" ] || [ -z "$minorVersion" ]; then + echo "Please set the signedUrls, datasetId, majorVersion, minorVersion environment variables." + exit 1 +fi + +# Logging function +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +echo "Started Shell Script for S3 DataSet Processing" +PROGRESS_CREATE_URL="http://ruuter-public:8086/global-classifier/datasets/progress/create" +PROGRESS_UPDATE_URL="http://ruuter-public:8086/global-classifier/datasets/progress/update" + +# Send progress session creation request +progress_payload=$(cat </dev/null 2>&1; then + sessionId=$(echo "$progress_response" | jq -r '.response.sessionId') +else + # Fallback using grep/sed (works for simple JSON) + sessionId=$(echo "$progress_response" | grep -o '"sessionId":[ ]*[0-9]*' | grep -o '[0-9]*') +fi + +echo "Extracted sessionId: $sessionId" + +data_generation_request="$signedUrls" +chmod 777 /app/data + +# Install required Python packages if not present +echo "🔍 Installing required Python packages..." +python3 -m pip install --quiet --no-cache-dir requests pydantic || { + echo "❌ Failed to install packages" + exit 1 +} +echo "✅ Required packages installed" + +log "S3 data processing request received" +log "Encoded data length: ${#data_generation_request} characters" + +# Direct Python script path for downloading datasets (inside container) +DOWNLOAD_SCRIPT="/app/src/s3_dataset_processor/download_source_dataset.py" +CURRENT_DATASET_ID="$datasetId" +CURRENT_DATASET_ID=$(echo "$CURRENT_DATASET_ID" | tr -d '"') + +log "🔍 Calling direct Python script to download files..." + +# Update progress session with initial status +progress_update_payload=$(cat < "$temp_payload" + + first_entry=true + + # Extract folder information and build the payload list + # Use improved parsing to handle the extracted_folders array + if command -v jq >/dev/null 2>&1; then + # Use jq if available - more reliable + echo "$response_body" | jq -r '.extracted_folders[]? | "\(.agency_id):\(.agency_name):\(.folder_path)"' 2>/dev/null | while IFS=':' read -r agency_id agency_name folder_path; do + if [ -n "$agency_id" ] && [ -n "$agency_name" ] && [ -n "$folder_path" ]; then + if [ "$first_entry" = false ]; then + echo ',' >> "$temp_payload" + fi + echo " {" >> "$temp_payload" + echo " \"agency_id\": \"$agency_id\"," >> "$temp_payload" + echo " \"agency_name\": \"$agency_name\"," >> "$temp_payload" + echo " \"data_path\": \"$folder_path\"," >> "$temp_payload" + echo " \"output_filename\": \"$CURRENT_DATASET_ID\"," >> "$temp_payload" + echo " \"version_id\": \"$CURRENT_DATASET_ID\"," >> "$temp_payload" + echo " \"session_id\": \"$sessionId\"" >> "$temp_payload" + echo " }" >> "$temp_payload" + first_entry=false + fi + done + else + # Fallback parsing without jq - improved regex + log "⚠️ jq not available, using grep/sed for parsing" + + # Clean the response body and extract agency_id and folder_path pairs + cleaned_response=$(echo "$response_body" | tr -d '\n\r\t' | tr -s ' ') + echo "$cleaned_response" | grep -o '"agency_id"[[:space:]]*:[[:space:]]*"[^"]*"[[:space:]]*,[[:space:]]*"agency_name"[[:space:]]*:[[:space:]]*"[^"]*"[[:space:]]*,[[:space:]]*"folder_path"[[:space:]]*:[[:space:]]*"[^"]*"' | while read -r folder_info; do + agency_id=$(echo "$folder_info" | sed 's/.*"agency_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + agency_name=$(echo "$folder_info" | sed 's/.*"agency_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + folder_path=$(echo "$folder_info" | sed 's/.*"folder_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') + + if [ -n "$agency_id" ] && [ -n "$agency_name" ] && [ -n "$folder_path" ]; then + if [ "$first_entry" = false ]; then + echo ',' >> "$temp_payload" + fi + echo " {" >> "$temp_payload" + echo " \"agency_id\": \"$agency_id\"," >> "$temp_payload" + echo " \"agency_name\": \"$agency_name\"," >> "$temp_payload" + echo " \"data_path\": \"$folder_path\"," >> "$temp_payload" + echo " \"output_filename\": \"$CURRENT_DATASET_ID\"," >> "$temp_payload" + echo " \"version_id\": \"$CURRENT_DATASET_ID\"," >> "$temp_payload" + echo " \"session_id\": \"$sessionId\"" >> "$temp_payload" + echo " }" >> "$temp_payload" + first_entry=false + fi + done + fi + + # Close the JSON array and object + echo '' >> "$temp_payload" + echo ']}' >> "$temp_payload" + + # Read the complete payload + payload_content=$(cat "$temp_payload") + log "🔍 Dataset generation payload: $payload_content" + + # Call the dataset generation service with the list payload + log "🔄 Calling dataset generation service for bulk processing..." + + dataset_response=$(curl -s -o /tmp/dataset_response_body.txt -w "%{http_code}" -X POST "http://dataset-gen-service:8000/generate-bulk" \ + -H "Content-Type: application/json" \ + -d "$payload_content") + + dataset_http_code="$dataset_response" + dataset_response_body=$(cat /tmp/dataset_response_body.txt) + + log "🔍 Dataset Generation HTTP Status Code: $dataset_http_code" + log "🔍 Dataset Generation Response: $dataset_response_body" + + if [ "$dataset_http_code" = "200" ]; then + progress_update_payload=$(cat <&2 +} + +log "Multi-chunk download request started" +log "Dataset ID: $datasetId" +log "Chunk IDs: $chunkIds" + +# Clean the parameters +DATASET_ID=$(echo "$datasetId" | tr -d '"') +CHUNK_IDS=$(echo "$chunkIds" | tr -d '"') + +log "Cleaned Dataset ID: $DATASET_ID" +log "Cleaned Chunk IDs: $CHUNK_IDS" + +# Validate chunk IDs format +if [[ ! "$CHUNK_IDS" =~ ^[0-9]+([[:space:]]+[0-9]+)*$ ]]; then + log "❌ Invalid chunk IDs format. Expected space-separated numbers." + error_response="{\"success\": false, \"dataset_id\": \"$DATASET_ID\", \"chunk_ids\": \"$CHUNK_IDS\", \"error\": \"Invalid chunk IDs format\", \"message\": \"Expected space-separated numbers like '1 2 3'\"}" + echo "$error_response" + exit 1 +fi + +# Create temp_chunks directory if it doesn't exist +mkdir -p /app/temp_chunks +log "Created/verified temp_chunks directory" + +# Install required Python packages if not present +log "🔍 Installing required Python packages..." +python3 -m pip install --quiet --no-cache-dir requests pydantic || { + log "❌ Failed to install packages" + exit 1 +} +log "✅ Required packages installed" + +# Direct Python script path for downloading multiple chunks (inside container) +DOWNLOAD_SCRIPT="/app/src/s3_dataset_processor/fetch_multi_chunk.py" + +log "🔍 Calling Python script to download and aggregate chunks..." + +# Create temporary file for response +temp_response="/tmp/multi_chunk_response.json" + +# Call the Python script +python3 "$DOWNLOAD_SCRIPT" \ + --dataset-id "$DATASET_ID" \ + --chunk-ids "$CHUNK_IDS" \ + --output-json "$temp_response" + +exit_code=$? +log "🔍 Python script exit code: $exit_code" + +if [ "$exit_code" -eq 0 ] && [ -f "$temp_response" ]; then + log "✅ Multi-chunk processing successful" + + response_body=$(cat "$temp_response") + + # Check if aggregation was successful + success_check=$(echo "$response_body" | grep -o '"success"[[:space:]]*:[[:space:]]*true' | wc -l) + + if [ "$success_check" -gt 0 ]; then + log "✅ Chunks aggregated successfully" + + # Extract summary information for logging + if command -v jq >/dev/null 2>&1; then + total_items=$(echo "$response_body" | jq -r '.download_summary.total_items_aggregated // 0' 2>/dev/null || echo "0") + successful_chunks=$(echo "$response_body" | jq -r '.download_summary.successful_downloads // 0' 2>/dev/null || echo "0") + failed_chunks=$(echo "$response_body" | jq -r '.download_summary.failed_downloads // 0' 2>/dev/null || echo "0") + + log "📊 Aggregation Summary:" + log " - Total items aggregated: $total_items" + log " - Successful chunk downloads: $successful_chunks" + log " - Failed chunk downloads: $failed_chunks" + else + log "📊 Multi-chunk aggregation completed (install jq for detailed summary)" + fi + + # Output the JSON response to stdout (this goes to CronManager caller) + cat "$temp_response" + + # Cleanup + rm -f "$temp_response" + + log "✅ Multi-chunk aggregation completed successfully" + exit 0 + else + log "❌ Multi-chunk aggregation failed - check response for details" + + # Still output the response so caller can see the error + cat "$temp_response" + + # Cleanup + rm -f "$temp_response" + exit 1 + fi + +else + log "❌ Python script execution failed with exit code: $exit_code" + + # Create error response + error_response="{\"success\": false, \"dataset_id\": \"$DATASET_ID\", \"chunk_ids\": \"$CHUNK_IDS\", \"error\": \"Script execution failed\", \"message\": \"Python script failed with exit code $exit_code\"}" + echo "$error_response" + + if [ -f "$temp_response" ]; then + log "Error response: $(cat $temp_response)" + rm -f "$temp_response" + fi + exit 1 +fi \ No newline at end of file diff --git a/DSL/CronManager/script/fetch_single_chunk.sh b/DSL/CronManager/script/fetch_single_chunk.sh new file mode 100755 index 00000000..77df4869 --- /dev/null +++ b/DSL/CronManager/script/fetch_single_chunk.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +echo "Started Shell Script for Chunk Download" + +# Check if environment variables are set +if [ -z "$datasetId" ] || [ -z "$pageNum" ]; then + echo "Please set the datasetId and pageNum environment variables." + exit 1 +fi + +# Logging function +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >&2 +} + +log "Chunk download request started" +log "Dataset ID: $datasetId" +log "Page Number: $pageNum" + +# Clean the parameters +DATASET_ID=$(echo "$datasetId" | tr -d '"') +PAGE_NUM=$(echo "$pageNum" | tr -d '"') + +log "Cleaned Dataset ID: $DATASET_ID" +log "Cleaned Page Number: $PAGE_NUM" + +# Install required Python packages if not present +log "🔍 Installing required Python packages..." +python3 -m pip install --quiet --no-cache-dir requests pydantic || { + log "❌ Failed to install packages" + exit 1 +} +log "✅ Required packages installed" + +# Direct Python script path for downloading chunk (inside container) +DOWNLOAD_SCRIPT="/app/src/s3_dataset_processor/fetch_chunk_without_filter.py" + +log "🔍 Calling Python script to download chunk..." + +# Create temporary file for response +temp_response="/tmp/chunk_response.json" + +# Call the Python script +python3 "$DOWNLOAD_SCRIPT" \ + --dataset-id "$DATASET_ID" \ + --page-num "$PAGE_NUM" \ + --output-json "$temp_response" + +exit_code=$? +log "🔍 Python script exit code: $exit_code" + +if [ "$exit_code" -eq 0 ] && [ -f "$temp_response" ]; then + log "✅ Chunk download successful" + + response_body=$(cat "$temp_response") + log "🔍 Response: $response_body" + + # Check if download was successful + success_check=$(echo "$response_body" | grep -o '"success"[[:space:]]*:[[:space:]]*true' | wc -l) + + if [ "$success_check" -gt 0 ]; then + log "✅ Chunk downloaded successfully" + + # Output the JSON response to stdout (this goes to CronManager caller) + cat "$temp_response" + + # Cleanup + rm -f "$temp_response" + + log "✅ Chunk download completed successfully" + exit 0 + else + log "❌ Chunk download failed - check response for details" + + # Still output the response so caller can see the error + cat "$temp_response" + + # Cleanup + rm -f "$temp_response" + exit 1 + fi + +else + log "❌ Python script execution failed with exit code: $exit_code" + + # Create error response + error_response="{\"success\": false, \"dataset_id\": \"$DATASET_ID\", \"page_num\": $PAGE_NUM, \"error\": \"Script execution failed\", \"message\": \"Python script failed with exit code $exit_code\"}" + echo "$error_response" + + if [ -f "$temp_response" ]; then + log "Error response: $(cat $temp_response)" + rm -f "$temp_response" + fi + exit 1 +fi \ No newline at end of file diff --git a/DSL/CronManager/script/train_script_starter.sh b/DSL/CronManager/script/train_script_starter.sh new file mode 100755 index 00000000..790429e3 --- /dev/null +++ b/DSL/CronManager/script/train_script_starter.sh @@ -0,0 +1,255 @@ +#!/bin/bash +# File: DSL/CronManager/script/train_script_starter.sh + +# API Endpoints +CHECK_JOB_STATUS_IN_PROGRESS_SQL="http://resql:8082/global-classifier/get-training-job-status-in-progress" +GET_FIRST_COME_TRAINING_JOB_SQL="http://resql:8082/global-classifier/get-queued-training-job" +GET_DATA_MODEL_BY_MODEL_ID_SQL="http://resql:8082/global-classifier/get-data-model-info-by-given-model-id" +UPDATE_JOB_STATUS="http://resql:8082/global-classifier/update-training-job-status" + +echo "🔄 [START] Training script starter" + +# Check if training is in progress +echo "🔍 [CHECK] Checking if training is in progress..." +response_job_status_in_progres=$(curl -s -X POST "$CHECK_JOB_STATUS_IN_PROGRESS_SQL") +echo "🔍 [DEBUG] Training status response: '$response_job_status_in_progres'" + +if [ $? -ne 0 ] || [ -z "$response_job_status_in_progres" ]; then + echo "❌ [ERROR] Failed to check training status" + exit 1 +fi + +if echo "$response_job_status_in_progres" | grep -q '"hasTrainingInProgress":true'; then + echo "⚠️ [INFO] Training is already in progress. Exiting..." + exit 0 +fi + +echo "✅ [AVAILABLE] No training in progress." + +# Get first queued training job +echo "🎯 [QUEUE] Getting first queued training job..." +response_first_come_training_job=$(curl -s -X POST "$GET_FIRST_COME_TRAINING_JOB_SQL") +echo "🔍 [DEBUG] First queued job response: '$response_first_come_training_job'" + +# Handle empty response (no queued jobs) - this is normal, not an error +if [ -z "$response_first_come_training_job" ]; then + echo "ℹ️ [INFO] No queued training jobs found. Nothing to process." + echo "✅ [DONE] Training script starter completed - no work to do" + exit 0 +fi + +# Handle explicit "no jobs" responses from API - INCLUDING EMPTY ARRAY +if echo "$response_first_come_training_job" | grep -q '"hasQueuedJobs":false' || \ + echo "$response_first_come_training_job" | grep -q '"modelId":null' || \ + echo "$response_first_come_training_job" | grep -q '"jobId":null' || \ + [ "$response_first_come_training_job" = "{}" ] || \ + [ "$response_first_come_training_job" = "null" ] || \ + [ "$response_first_come_training_job" = "[]" ]; then + echo "ℹ️ [INFO] No queued training jobs available. Queue is empty." + echo "✅ [DONE] Training script starter completed - no work to do" + exit 0 +fi + +# Extract model_id and job_id +model_id=$(echo "$response_first_come_training_job" | sed -E 's/.*"modelId":[[:space:]]*([0-9]+).*/\1/') +job_id=$(echo "$response_first_come_training_job" | sed -E 's/.*"jobId":"?([0-9a-zA-Z-]+)"?.*/\1/') +model_name=$(echo "$response_first_come_training_job" | sed -E 's/.*"modelName":"?([^",}]+)"?.*/\1/') +major_version=$(echo "$response_first_come_training_job" | sed -E 's/.*"majorVersion":[[:space:]]*([0-9]+).*/\1/') +minor_version=$(echo "$response_first_come_training_job" | sed -E 's/.*"minorVersion":[[:space:]]*([0-9]+).*/\1/') +latest=$(echo "$response_first_come_training_job" | sed -E 's/.*"latest":[[:space:]]*(true|false).*/\1/') +deployment_environment=$(echo "$response_first_come_training_job" | sed -E 's/.*"deploymentEnvironment":"?([^",}]+)"?.*/\1/') + +echo "🔍 [----DEBUG----] Raw response: '$response_first_come_training_job'" + +if [ -z "$model_id" ]; then + echo "❌ [ERROR] Model ID not found in response" + echo "🔍 [DEBUG] Raw response: '$response_first_come_training_job'" + exit 1 +fi + +if [ -z "$job_id" ] || [ "$job_id" = "$response_first_come_training_job" ]; then + echo "❌ [ERROR] Job ID not found or invalid in response" + echo "🔍 [DEBUG] Raw response: '$response_first_come_training_job'" + exit 1 +fi + +echo "📦 [MODEL] Model ID: $model_id" +echo "📦 [JOB] Job ID: $job_id" +echo "📦 [MODEL] Model Name: $model_name" +echo "📦 [VERSION] Major Version: $major_version" +echo "📦 [VERSION] Minor Version: $minor_version" +echo "📦 [VERSION] Latest: $latest" +echo "📦 [ENVIRONMENT] Deployment Environment: $deployment_environment" + +response_update_job_status=$(curl -s -X POST "$UPDATE_JOB_STATUS" \ + -H "Content-Type: application/json" \ + -d "{\"jobId\": $job_id, \"jobStatus\": \"training-in-progress\"}") +echo "🔍 [DEBUG] Update job status response: '$response_update_job_status'" + +# Get dataset ID +response_get_dataset_id=$(curl -s -X POST "$GET_DATA_MODEL_BY_MODEL_ID_SQL" \ + -H "Content-Type: application/json" \ + -d "{\"model_id\": $model_id}") +echo "🔍 [DEBUG] Dataset ID response: '$response_get_dataset_id'" + +# Handle empty response +if [ -z "$response_get_dataset_id" ] || [ "$response_get_dataset_id" = "[]" ]; then + echo "❌ [ERROR] No dataset information found for model ID: $model_id" + exit 1 +fi + +dataset_id=$(echo "$response_get_dataset_id" | sed -E 's/.*"connectedDsId":([0-9]+).*/\1/') + +if [ -z "$dataset_id" ] || [ "$dataset_id" = "$response_get_dataset_id" ]; then + echo "❌ [ERROR] Connected Dataset ID not found in response" + echo "🔍 [DEBUG] Raw response: '$response_get_dataset_id'" + exit 1 +fi + +echo "📦 [DATASET] Dataset ID: $dataset_id" + +base_models_json=$(echo "$response_get_dataset_id" | sed -nE 's/.*"value":"(\[[^]]+\])".*/\1/p' | sed 's/\\"/"/g') + +if [[ "$base_models_json" == "["* ]] && [[ "$base_models_json" == *"]" ]]; then + model_types="$base_models_json" + echo "📦 [MODELS] Model types extracted from DB: $model_types" +else + echo "❌ [ERROR] Failed to extract base models from response" + echo "❌ [ERROR] Raw response: $response_get_dataset_id" + echo "❌ [ERROR] Extracted base_models: $base_models_json" + exit 1 +fi + +# Activate existing virtualenv +echo "✅ Activating existing virtualenv at /app/python_virtual_env" +source /app/python_virtual_env/bin/activate || { echo "❌ Failed to activate virtualenv"; exit 1; } +export PYTHONPATH="/app:/app/src:/app/src/training:/app/src/s3_dataset_processor:$PYTHONPATH" +echo "🔍 [DEBUG] PYTHONPATH set to: $PYTHONPATH" +# Add these debug commands +echo "🔍 [DEBUG] Virtual environment debugging:" +echo " - VIRTUAL_ENV: $VIRTUAL_ENV" +echo " - Python path: $(which python)" +echo " - Python version: $(python --version)" +echo " - Pip path: $(which pip)" +echo " - Site packages: $(python -c "import site; print(site.getsitepackages())")" + +# List installed packages +echo "📦 [DEBUG] Installed packages in current environment:" +pip list | head -20 # Show first 20 packages + +# Check required packages +echo "🔍 [DEBUG] Testing individual package imports inside virtualenv..." +missing_pkgs=() +for pkg in torch transformers sklearn mlflow pandas numpy loguru; do + echo "🔍 [DEBUG] Testing import for $pkg" + if ! python -c "import $pkg" &>/dev/null; then + echo "❌ [MISSING or failed import] Package '$pkg'" + missing_pkgs+=("$pkg") + else + echo "✅ [FOUND] Package '$pkg'" + fi +done + +# Install if missing +if [ ${#missing_pkgs[@]} -ne 0 ]; then + echo "⚡ [ACTION] Missing packages detected: ${missing_pkgs[*]}" + + if ! command -v uv &>/dev/null; then + echo "⚡ Installing uv inside virtualenv..." + pip install uv || { echo "❌ Failed to install uv"; exit 1; } + else + echo "✅ uv already installed." + fi + + if [ ! -f /app/src/training/requirements-gpu.txt ]; then + echo "❌ /app/src/training/requirements-gpu.txt not found!" + exit 1 + fi + + echo "📦 [INSTALL] Installing from /app/src/training/requirements-gpu.txt using uv..." + uv pip install -r /app/src/training/requirements-gpu.txt || { + echo "⚠️ uv install failed — trying pip as fallback..." + pip install -r /app/src/training/requirements-gpu.txt || { + echo "❌ Both uv and pip install failed inside virtualenv" + exit 1 + } + } + + echo "🎉 [SUCCESS] Required packages installed successfully inside virtualenv." +else + echo "🎉 [SUCCESS] All required Python packages are already installed inside virtualenv." +fi +echo "✅ [VIRTUALENV] All checks passed, proceeding with training script..." +echo "🚀 [TRAINING] Starting training for Model ID: $model_id, Dataset ID: $dataset_id, Model Major Version: $major_version, Model Minor Version: $minor_version, Model Name: $model_name" + +# Set up training parameters +TRAINING_SCRIPT="/app/src/training/model_trainer.py" +TRAINING_OUTPUT_DIR="/app/models" +MLFLOW_TRACKING_URI="${MLFLOW_TRACKING_URI:-http://mlflow:5000}" +PROCESSED_DATA_DIR="/app/data/processed" + +# Create output directory for this training job +training_output_dir="${TRAINING_OUTPUT_DIR}/model_${model_id}" +mkdir -p "$training_output_dir" + +# # Set default training parameters (can be made configurable) +# max_seq_length=128 +# num_epochs=3 +# batch_size=8 +# learning_rate=2e-5 + +echo "📋 [PARAMS] Training parameters:" +echo " - Dataset ID: $dataset_id" +echo " - Model ID: $model_id" +echo " - Model Type: $model_types" +echo " - Output Dir: $training_output_dir" +echo " - MLflow URI: $MLFLOW_TRACKING_URI" +echo " - Model Name: $model_name" +echo " - Major Version: $major_version" +echo " - Minor Version: $minor_version" +echo " - Is Latest: $latest" +echo " - Deployment Environment: $deployment_environment" + +# Call the training script +echo "🎓 [EXECUTE] Calling training script..." + +python3 "$TRAINING_SCRIPT" \ + --model_types "$model_types" \ + --model_id "$model_id" \ + --job_id "$job_id" \ + --dataset_id "$dataset_id" \ + --model_name "$model_name" \ + --major_version "$major_version" \ + --minor_version "$minor_version" \ + --latest "$latest" \ + --deployment_environment "$deployment_environment" \ + # --data_dir "$PROCESSED_DATA_DIR" \ + # --output_dir "$training_output_dir" \ + # --mlflow_tracking_uri "$MLFLOW_TRACKING_URI" \ + +training_exit_code=$? + +# Check training result +if [ $training_exit_code -eq 0 ]; then + echo "🎉 [SUCCESS] Training completed successfully" + echo "📁 [OUTPUT] Training outputs saved to: $training_output_dir" + + # Update job status to trained + echo "[UPDATE] Updating job status to trained..." + response_update_job_status=$(curl -s -X POST "$UPDATE_JOB_STATUS" \ + -H "Content-Type: application/json" \ + -d "{\"jobId\": $job_id, \"jobStatus\": \"trained\"}") + + echo "🔍 [DEBUG] Update job status to trained response: '$response_update_job_status_trained'" +else + echo "[FAILED] Training failed with exit code: $training_exit_code" + + echo "[UPDATE] Updating job status to training-failed..." + response_update_job_status=$(curl -s -X POST "$UPDATE_JOB_STATUS" \ + -H "Content-Type: application/json" \ + -d "{\"jobId\": $job_id, \"jobStatus\": \"training-failed\"}") + + exit 1 +fi + +echo "✅ [DONE] Training script starter completed" \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/agencies-sync-summary.handlebars b/DSL/DMapper/global-classifier/hbs/agencies-sync-summary.handlebars new file mode 100644 index 00000000..d6093ef2 --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/agencies-sync-summary.handlebars @@ -0,0 +1,10 @@ +{ + "syncSummary": { + "timestamp": "{{now}}", + "lastSyncedTimestamp": "{{inputTimestamp}}", + "totalAgenciesWithoutData": {{totalFound}}, + "totalNewAgenciesAdded": "{{addResult}}", + "dataImportResult": "{{importResult}}", + "status": "completed" + } +} \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/extract_agency_ids.handlebars b/DSL/DMapper/global-classifier/hbs/extract_agency_ids.handlebars new file mode 100644 index 00000000..bb866c49 --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/extract_agency_ids.handlebars @@ -0,0 +1,5 @@ +[ + {{#each agencies}} + "{{agencyId}}"{{#unless @last}},{{/unless}} + {{/each}} +] \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/extract_unavailable_agencies.handlebars b/DSL/DMapper/global-classifier/hbs/extract_unavailable_agencies.handlebars new file mode 100644 index 00000000..45b9c9ee --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/extract_unavailable_agencies.handlebars @@ -0,0 +1 @@ +{{{extractNewAgencies gcAgencies centopsAgencies}}} \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/format_agency_sync_results.handlebars b/DSL/DMapper/global-classifier/hbs/format_agency_sync_results.handlebars new file mode 100644 index 00000000..6bc963f0 --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/format_agency_sync_results.handlebars @@ -0,0 +1,9 @@ +{ + "syncSummary": { + "timestamp": "{{now}}", + "lastSyncedTimestamp": "{{inputTimestamp}}", + "totalAgenciesFound": {{totalFound}}, + "totalAgenciesAdded": "{{addResult}}", + "status": "completed" + } +} \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/format_connected_models.handlebars b/DSL/DMapper/global-classifier/hbs/format_connected_models.handlebars new file mode 100644 index 00000000..ec47e7eb --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/format_connected_models.handlebars @@ -0,0 +1,5 @@ +[ + {{#each connectedModels}} + "{{modelName}} V{{major}}.{{minor}}"{{#unless @last}},{{/unless}} + {{/each}} +] \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/format_data_urls.handlebars b/DSL/DMapper/global-classifier/hbs/format_data_urls.handlebars new file mode 100644 index 00000000..24cbb15a --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/format_data_urls.handlebars @@ -0,0 +1,11 @@ +{ + "datasetId": "{{datasetId}}", + "presignedUrls": [ + {{#each agencies}} + { + "agencyId": "{{agencyId}}", + "signedS3Url": "{{dataUrl}}" + }{{#unless @last}},{{/unless}} + {{/each}} + ] +} diff --git a/DSL/DMapper/global-classifier/hbs/format_datamodel_versions.handlebars b/DSL/DMapper/global-classifier/hbs/format_datamodel_versions.handlebars new file mode 100644 index 00000000..6e224046 --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/format_datamodel_versions.handlebars @@ -0,0 +1,5 @@ +[ +{{#each datamodels}} + {"id":{{id}},"version":"{{modelName}} V{{major}}.{{minor}}"}{{#unless @last}},{{/unless}} +{{/each}} +] \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/format_dataset_versions.handlebars b/DSL/DMapper/global-classifier/hbs/format_dataset_versions.handlebars new file mode 100644 index 00000000..445d532a --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/format_dataset_versions.handlebars @@ -0,0 +1,5 @@ +{{#each datasets}} + {{#if @first}}[{{/if}} + {"id":{{id}},"version":"V{{major}}.{{minor}}"}{{#unless @last}},{{/unless}} + {{#if @last}}]{{/if}} +{{/each}} \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/get-data-available-agencies.handlebars b/DSL/DMapper/global-classifier/hbs/get-data-available-agencies.handlebars new file mode 100644 index 00000000..07c90c97 --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/get-data-available-agencies.handlebars @@ -0,0 +1,7 @@ +[ + {{#each agencies}} + {{#if isDataAvailable}} + "{{agencyId}}"{{#unless @last}},{{/unless}} + {{/if}} + {{/each}} +] \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/get_all_chunks.handlebars b/DSL/DMapper/global-classifier/hbs/get_all_chunks.handlebars new file mode 100644 index 00000000..bfe6c6f3 --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/get_all_chunks.handlebars @@ -0,0 +1 @@ +{{{getAllChunksFromS3 datasetId pageNum}}} \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/get_auth_header.handlebars b/DSL/DMapper/global-classifier/hbs/get_auth_header.handlebars new file mode 100644 index 00000000..e94d1d22 --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/get_auth_header.handlebars @@ -0,0 +1,4 @@ +{ + "val": "{{{getAuthHeader username token}}}" +} + diff --git a/DSL/DMapper/global-classifier/hbs/get_filtered_chunks.handlebars b/DSL/DMapper/global-classifier/hbs/get_filtered_chunks.handlebars new file mode 100644 index 00000000..c828c673 --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/get_filtered_chunks.handlebars @@ -0,0 +1 @@ +{{{filterDataByAgency aggregatedData startIndex agencyId}}} \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/get_paginated_chunk_ids.handlebars b/DSL/DMapper/global-classifier/hbs/get_paginated_chunk_ids.handlebars new file mode 100644 index 00000000..d4f63d0f --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/get_paginated_chunk_ids.handlebars @@ -0,0 +1 @@ +{{{getPaginatedChunkIds chunks agencyId pageNum 5}}} \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/get_single_chunk_data.handlebars b/DSL/DMapper/global-classifier/hbs/get_single_chunk_data.handlebars new file mode 100644 index 00000000..8eed996b --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/get_single_chunk_data.handlebars @@ -0,0 +1 @@ +{{{getSingleChunkData chunkData}}} \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/mock_agency_data_availability.handlebars b/DSL/DMapper/global-classifier/hbs/mock_agency_data_availability.handlebars new file mode 100644 index 00000000..f93ee39d --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/mock_agency_data_availability.handlebars @@ -0,0 +1,8 @@ +[ + {{#each agencyIds}} + { + "agencyId": "{{this}}", + "isDataAvailable": "{{getAgencyDataAvailable this}}" + }{{#unless @last}},{{/unless}} + {{/each}} +] \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/no-new-agencies.handlebars b/DSL/DMapper/global-classifier/hbs/no-new-agencies.handlebars new file mode 100644 index 00000000..df54ffc7 --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/no-new-agencies.handlebars @@ -0,0 +1,10 @@ +{ + "syncSummary": { + "timestamp": "{{now}}", + "lastSyncedTimestamp": "{{inputTimestamp}}", + "totalAgenciesFound": 0, + "totalAgenciesAdded": 0, + "status": "completed", + "message": "No new agencies found" + } +} \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/prepare_agencies_for_import.handlebars b/DSL/DMapper/global-classifier/hbs/prepare_agencies_for_import.handlebars new file mode 100644 index 00000000..e5eb704d --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/prepare_agencies_for_import.handlebars @@ -0,0 +1,16 @@ +[ + {{#each agencyMetadata}} + { + "agencyId": "{{agencyId}}", + "agencyName": "Agency {{agencyId}}", + "groupKey": "", + "isLatest": true, + "deploymentStatus": "undeployed", + "isEnabled": false, + "agencyDataHash": "{{agencyDataHash}}", + "enableAllowed": false, + "lastModelTrained": "", + "syncStatus": "Available_in_CKB" + }{{#unless @last}},{{/unless}} + {{/each}} +] \ No newline at end of file diff --git a/DSL/DMapper/global-classifier/hbs/return_random_string.handlebars b/DSL/DMapper/global-classifier/hbs/return_random_string.handlebars new file mode 100644 index 00000000..ecefc239 --- /dev/null +++ b/DSL/DMapper/global-classifier/hbs/return_random_string.handlebars @@ -0,0 +1,3 @@ +{ + "randomHexString": "{{{getRandomString}}}" +} diff --git a/DSL/DMapper/global-classifier/lib/helpers.js b/DSL/DMapper/global-classifier/lib/helpers.js new file mode 100644 index 00000000..6f5e74f9 --- /dev/null +++ b/DSL/DMapper/global-classifier/lib/helpers.js @@ -0,0 +1,271 @@ +import { randomBytes } from "crypto"; +import fs from "fs/promises"; +import path from "path"; + +export function getAuthHeader(username, token) { + const auth = `${username}:${token}`; + const encodedAuth = Buffer.from(auth).toString("base64"); + return `Basic ${encodedAuth}`; +} + +export function mergeLabelData(labels, existing_labels) { + let mergedArray = [...labels, ...existing_labels]; + let uniqueArray = [...new Set(mergedArray)]; + return { labels: uniqueArray }; +} + +export function platformStatus(platform, data) { + const platformData = data.find((item) => item.platform === platform); + return platformData ? platformData.isConnect : false; +} + +export function isLabelsMismatch(newLabels, correctedLabels, predictedLabels) { + function check(arr, newLabels) { + if ( + Array.isArray(newLabels) && + Array.isArray(arr) && + newLabels.length === arr.length + ) { + for (let label of newLabels) { + if (!arr.includes(label)) { + return true; + } + } + return false; + } else { + return true; + } + } + + const val1 = check(correctedLabels, newLabels); + const val2 = check(predictedLabels, newLabels); + return val1 && val2; +} + +export function getOutlookExpirationDateTime() { + const currentDate = new Date(); + currentDate.setDate(currentDate.getDate() + 3); + const updatedDateISOString = currentDate.toISOString(); + return updatedDateISOString; +} + +export function findDuplicateStopWords(inputArray, existingArray) { + const set1 = new Set(existingArray); + const duplicates = inputArray.filter((item) => set1.has(item)); + const value = JSON.stringify(duplicates); + return value; +} + +export function findNotExistingStopWords(inputArray, existingArray) { + const set1 = new Set(existingArray); + const notExisting = inputArray.filter((item) => !set1.has(item)); + const value = JSON.stringify(notExisting); + return value; +} + +export function getRandomString() { + const randomHexString = randomBytes(32).toString("hex"); + return randomHexString; +} + +export function base64Decrypt(cipher, isObject) { + if (!cipher) { + return JSON.stringify({ + error: true, + message: 'Cipher is missing', + }); + } + + try { + const decodedContent = !isObject ? Buffer.from(cipher, 'base64').toString('utf8') : JSON.parse(Buffer.from(cipher, 'base64').toString('utf8')); + const cleanedContent = decodedContent.replace(/\r/g, ''); + return JSON.stringify({ + error: false, + content: cleanedContent + }); + } catch (err) { + return JSON.stringify({ + error: true, + message: 'Base64 Decryption Failed', + }); + } +} + +export function base64Encrypt(content) { + if (!content) { + return { + error: true, + message: 'Content is missing', + } + } + + try { + return JSON.stringify({ + error: false, + cipher: Buffer.from(typeof content === 'string' ? content : JSON.stringify(content)).toString('base64') + }); + } catch (err) { + return JSON.stringify({ + error: true, + message: 'Base64 Encryption Failed', + }); + } +} + +export function jsEscape(str) { + return JSON.stringify(str).slice(1, -1) +} + +export function isValidIntentName(name) { + // Allows letters (any unicode letter), numbers, and underscores + // Matches front-end validation with spaces replaced with underscores + return /^[\p{L}\p{N}_]+$/u.test(name); +} + +export function eq(v1, v2) { + return v1 === v2; +} + +export function getAgencyDataHash(agencyId) { + // Generate a random hash based on agency ID + // Create a consistent but seemingly random hash for each agencyId + const baseHash = agencyId.padEnd(10, agencyId); // Ensure at least 10 chars + let hash = ''; + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + + // Use the agencyId as a seed for pseudo-randomness + for (let i = 0; i < 16; i++) { + // Get character code from the baseHash, or use index if out of bounds + const charCode = i < baseHash.length ? baseHash.charCodeAt(i) : i; + // Use the character code to get an index in our chars string + const index = (charCode * 13 + i * 7) % chars.length; + hash += chars[index]; + } + + return hash; +} + +export function getAgencyDataAvailable(agencyId) { + // Use agencyId as a seed for deterministic but seemingly random result + // This ensures the same agencyId always gets the same result in the same session + + // Create a hash from the agencyId + let hashValue = 0; + for (let i = 0; i < agencyId.length; i++) { + hashValue = ((hashValue << 5) - hashValue) + agencyId.charCodeAt(i); + hashValue |= 0; // Convert to 32bit integer + } + + // Add a time component to make it change between sessions + // Use current date (year+month only) so it changes monthly but not every request + const date = new Date(); + const timeComponent = date.getFullYear() * 100 + date.getMonth(); + + // Combine the hash and time component for pseudo-randomness + const combinedValue = hashValue + timeComponent; + + // Return true or false based on even/odd value + return (combinedValue % 2) === 0; +} + +export function json(context) { + return JSON.stringify(context); +} + +/** + * Helper function to check if a value is an array + * @param {any} value - The value to check + * @returns {boolean} - True if value is an array, false otherwise + */ +export function isArray(value) { + return Array.isArray(value); +} + +/** + * Returns an array of agencies that are in centopsAgencies but not in gcAgencies (by agencyId). + * @param {Array} gcAgencies - Array of existing agencies, each with an agencyId property. + * @param {Array} centopsAgencies - Array of agencies from CentOps, each with an agencyId property. + * @returns {Array} Array of new agency objects from centopsAgencies. + */ +export function extractNewAgencies(gcAgencies, centopsAgencies) { + const existingIds = new Set(gcAgencies.map(a => a.agencyId)); + const newAgencies = centopsAgencies.filter(a => !existingIds.has(a.agencyId)) + // return newAgencies; + return JSON.stringify({ + agencies: newAgencies, + }); +} + +/** + * Downloads a JSON file from S3 and returns its parsed content. + * @param {string} datasetId + * @param {string|number} pageNum + * @returns {Object} Parsed JSON content of the file + */ +export function getSingleChunkData(chunkData) { + const mapped = chunkData?.map(item => ({ + clientId: item.agency_id, + id: item.id, + clientName: item.agency_name, + question: item.question + })); + + return JSON.stringify(mapped); +} + +export function getPaginatedChunkIds(chunks, agencyId, pageNum, pageSize = 5) { + let agencyRecordIndex = 0; // total agency records seen so far + let collected = 0; // agency records collected for this page + let resultChunks = []; + let startIndex = 0; + let foundPage = false; + + for (const chunk of chunks) { + let agencies = JSON.parse(chunk.includedAgencies.value) + + const count = agencies.filter(a => String(a) === String(agencyId)).length; + if (count === 0) continue; + + // If we haven't reached the start of this page, skip these records + if (!foundPage && agencyRecordIndex + count < (pageNum - 1) * pageSize + 1) { + agencyRecordIndex += count; + continue; + } + + // If this is the first chunk of the page, calculate startIndex + if (!foundPage) { + startIndex = (pageNum - 1) * pageSize - agencyRecordIndex; + foundPage = true; + } + + resultChunks.push(chunk.chunkId || chunk.chunkId); + collected += count; + + if (collected >= pageSize) break; + + agencyRecordIndex += count; + } + + return JSON.stringify( + { + chunks: resultChunks, + startIndex: startIndex + } + ); +} + +export function filterDataByAgency(aggregatedData, startIndex, agencyId, pageSize=5) { + + const filtered = aggregatedData.filter(item => String(item.agency_id) === String(agencyId)); + + const paginated = filtered.slice(startIndex, startIndex + 5); + + const result= paginated.map(item => ({ + clientId: item.agency_id, + id: item.id, + clientName: item.agency_name, // No mapping available, so use agency_id + question: item.question + })); + return JSON.stringify(result); + +} diff --git a/DSL/DMapper/global-classifier/lib/requestLoggerMiddleware.js b/DSL/DMapper/global-classifier/lib/requestLoggerMiddleware.js new file mode 100644 index 00000000..727a36fa --- /dev/null +++ b/DSL/DMapper/global-classifier/lib/requestLoggerMiddleware.js @@ -0,0 +1,30 @@ +/** + * @param res Original Response Object + * @param send Original UNMODIFIED res.send function + * @return A patched res.send which takes the send content, binds it to contentBody on + * the res and then calls the original res.send after restoring it + */ +const resDotSendInterceptor = (res, send) => (content) => { + res.contentBody = content; + res.send = send; + res.send(content); +}; + +export const requestLoggerMiddleware = + ({ logger }) => + (req, res, next) => { + logger( + `Request: {method: ${req.method}, url: ${ + req.url + }, params: ${JSON.stringify(req.params)}, query: ${JSON.stringify( + req.query + )}, body: ${JSON.stringify(req.body)}` + ); + res.send = resDotSendInterceptor(res, res.send); + res.on("finish", () => { + logger( + `Response: {statusCode: ${res.statusCode}, responseData: ${res.contentBody}}` + ); + }); + next(); + }; diff --git a/DSL/DatasetGenerator/config/config.yaml b/DSL/DatasetGenerator/config/config.yaml new file mode 100644 index 00000000..d3174a6f --- /dev/null +++ b/DSL/DatasetGenerator/config/config.yaml @@ -0,0 +1,119 @@ +# LLM Provider configuration +provider: + name: "ollama" + model_name: "gemma3:1b-it-qat" + api_url: "http://ollama:11434" + timeout: 60 + max_retries: 3 + retry_delay: 5 + +# API connection settings +api: + url: "http://localhost:8000" + timeout_seconds: 30 + +# Directory paths +directories: + input: "data" + output: "output_datasets" + templates: "templates" + user_configs: "user_configs" + +# Default generation settings (can be overridden per generation request) +generation: + default_num_examples: 10 + default_language: "et" + max_retries: 3 + parameters: + temperature: 0.7 + max_tokens: 4096 + +# Dataset generation configuration +dataset_generation: + structure_name: "single_question" + prompt_template_name: "institute_topic_question" + traversal_strategy: "pattern" # Options: "flat", "recursive", "institutional", "pattern" + output_format: "json" + num_samples: 10 + post_processing: "aggregation" # Options: "zip", "aggregation" + # Aggregation-specific configuration (only used when post_processing = "aggregation") + aggregation: + output_filename: "12" + output_format: "csv" + merge_strategy: "combine_arrays" + include_metadata: true + enable_shuffling: false + + field_mapping: + enabled: true + payload_to_output: + agency_name: agency_name + agency_id: agency_id + defaults: + item_id: auto_increment + dataset_version_id: version_id + content_fields: + question: data_item + + + csv_field_order: + - item_id + - agency_name + - agency_id + - data_item + - dataset_version_id + + parameters: + language: "et" + temperature: 0.7 + redundancy_factor: 1.5 + language_name: "Estonian" + difficulty: "medium" + style: "clear and concise" + system_prompt: "You are a helpful assistant for generating synthetic questions for given contexts." + filter: {} + +# Processing settings +processing: + wait_between_requests: 1 + +# MLflow tracking +mlflow: + experiment_name: "synthetic_data_generation" + +# Data source configuration +data_sources: + default: + strategy: "pattern" + base_path: "data" + patterns: ["**/cleaned.txt"] + recursive: true + +callback: + url: "http://ruuter-public:8086/global-classifier/data/callback" + max_retries: 3 + timeout: 30 + +# Relevance Score Analysis +relevance_score: + enabled: true + embedding_model: "paraphrase-multilingual-mpnet-base-v2" + segment_weight: 0.6 + query_weight: 0.3 + term_weight: 0.1 + threshold_good: 0.7 + threshold_acceptable: 0.5 + min_df: 1 + max_df: 0.9 + ngram_range: (1, 2) + +# Information Coverage Analysis +information_coverage: + enabled: true + similarity_threshold: 0.5 + +# Model settings +models: + embedding_model: "paraphrase-multilingual-mpnet-base-v2" + qualitative_model: "google/gemma-2-2b-it" + use_4bit_quantization: true \ No newline at end of file diff --git a/DSL/DatasetGenerator/config/model_config.yaml b/DSL/DatasetGenerator/config/model_config.yaml new file mode 100644 index 00000000..4eee2282 --- /dev/null +++ b/DSL/DatasetGenerator/config/model_config.yaml @@ -0,0 +1,39 @@ +# Ollama Client and Model Settings +model_name: "gemma3:1b-it-qat" +ollama_host: "http://ollama:11434" +ollama_timeout: 60 +ollama_max_retries: 3 +ollama_retry_delay: 5 + +# Generation defaults +generation_defaults: + temperature: 0.95 + max_tokens_per_response: 5000 + num_predict: 5000 + +# Content Processing +content_processing: + max_content_length: 15000 + content_overlap: 500 + +# Language and Prompt Settings +language_settings: + default_system_prompt: "You are a helpful assistant providing accurate information based on topic content." + default_language: "et" + supported_languages: + en: "English" + et: "Estonian" + fi: "Finnish" + +# Storage configuration +storage: + datasets_dir: "datasets" + templates_dir: "templates" + user_configs_dir: "user_configs" + +# Default Output Settings +output_defaults: + save_format: "json" + supported_formats: + - "json" + - "text" \ No newline at end of file diff --git a/DSL/DatasetGenerator/ollama-entrypoint.sh b/DSL/DatasetGenerator/ollama-entrypoint.sh new file mode 100644 index 00000000..b30cf39f --- /dev/null +++ b/DSL/DatasetGenerator/ollama-entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +MODEL_NAME=${MODEL_NAME:-"gemma3:1b-it-qat"} + +# Start Ollama in the background +ollama serve & +OLLAMA_PID=$! + +# Wait for Ollama API to be ready +until ollama list > /dev/null 2>&1; do + echo "Waiting for Ollama to start..." + sleep 2 +done + +# Pull the model if not already present +if ! ollama list | grep -q "$MODEL_NAME"; then + echo "Pulling model $MODEL_NAME..." + ollama pull "$MODEL_NAME" +fi + +echo "Ollama is ready with model $MODEL_NAME" +wait $OLLAMA_PID \ No newline at end of file diff --git a/DSL/DatasetGenerator/user_configs/dataset_structures/single_question.yaml b/DSL/DatasetGenerator/user_configs/dataset_structures/single_question.yaml new file mode 100644 index 00000000..17dd4b74 --- /dev/null +++ b/DSL/DatasetGenerator/user_configs/dataset_structures/single_question.yaml @@ -0,0 +1,6 @@ +name: single_question +description: Structure containing a single JSON file for Q&A pairs, intended to be placed within a topic-specific directory. +root: + files: + faqs: + format: json \ No newline at end of file diff --git a/DSL/DatasetGenerator/user_configs/dataset_structures/topic_conversations.yaml b/DSL/DatasetGenerator/user_configs/dataset_structures/topic_conversations.yaml new file mode 100644 index 00000000..5bd3fc67 --- /dev/null +++ b/DSL/DatasetGenerator/user_configs/dataset_structures/topic_conversations.yaml @@ -0,0 +1,8 @@ +name: topic_conversations +description: "Structure for topic-based conversations" +root: + description: "Root directory of the dataset" + files: + conversations: + description: "JSON file with conversation examples" + format: "json" \ No newline at end of file diff --git a/DSL/DatasetGenerator/user_configs/prompts/institute_topic_conversation.txt b/DSL/DatasetGenerator/user_configs/prompts/institute_topic_conversation.txt new file mode 100644 index 00000000..b5e5205e --- /dev/null +++ b/DSL/DatasetGenerator/user_configs/prompts/institute_topic_conversation.txt @@ -0,0 +1,41 @@ +Generate a natural and informative conversation in ${language_name} based on the given topic from the ${institute} curriculum. + +Topic: ${topic} +Content: +""" +${topic_content} +""" +Difficulty: ${difficulty:-medium} +Style: ${style:-clear and concise} +Number of turns: ${num_turns:-4} + +Instructions: +- Strictly reflect the information in the provided content. +- Use a realistic and natural back-and-forth tone between a curious "user" and a helpful "assistant". +- Ask and answer questions in a way that gradually explores the topic (e.g., starting from basic questions and going deeper). +- Avoid generic filler; ensure each message is contextually specific and informative. + +Evaluation Criteria - Your conversation will be assessed on: + + 1. **Content Accuracy**: All information strictly adheres to the provided topic content + 2. **Naturalness**: Conversation flows realistically with authentic user questions and assistant responses + 3. **Progressive Depth**: Discussion starts with basic concepts and gradually explores more complex aspects + 4. **Language Quality**: Proper grammar, vocabulary, and fluency in ${language_name} + 5. **Educational Value**: Each exchange meaningfully contributes to understanding the topic + 6. **Contextual Relevance**: Every message is specific to the topic, avoiding generic responses + 7. **Appropriate Difficulty**: Conversation complexity matches the specified difficulty level + 8. **Turn Management**: Effective use of the specified number of turns to cover the topic comprehensively + +Output Format: +Return ONLY a raw JSON object with the structure: +{ + "messages": [ + {"role": "user", "content": "First user message"}, + {"role": "assistant", "content": "Assistant's specific and helpful reply"}, + ... + ] +} + +Do NOT include markdown formatting, code block markers, or explanatory text around the output. + +Additional Instructions: ${additional_instructions:-Ensure the dialogue stays on-topic, sounds natural in ${language_name}, and explains concepts clearly based on the content. Do not invent facts or go beyond the given topic material.} \ No newline at end of file diff --git a/DSL/DatasetGenerator/user_configs/prompts/institute_topic_question.txt b/DSL/DatasetGenerator/user_configs/prompts/institute_topic_question.txt new file mode 100644 index 00000000..6958c041 --- /dev/null +++ b/DSL/DatasetGenerator/user_configs/prompts/institute_topic_question.txt @@ -0,0 +1,47 @@ +${system_prompt} + +Generate a realistic user question in ${language_name} that someone would naturally ask a chatbot about the following topic. The question should clearly relate to the agency's services and demonstrate clear intent. + +*Topic Information:* +Topic: ${file_name} +Content: """${file_content}""" + +*Generation Parameters:* +- Difficulty Level: ${difficulty} +- Query Style: ${style} +- Language: ${language_name} + +*Question Requirements:* + +*Natural User Language:* +- Use conversational, everyday language that real users employ +- Include common ways people phrase questions to chatbots +- Mix formal and informal expressions naturally +- Vary question length and structure + +*Question Types:* +- Information requests about services and procedures +- Help-seeking for specific problems or processes +- Procedural questions about steps and requirements +- Problem-solving queries about issues or complications + +*Content Accuracy:* +- Base questions strictly on the provided topic content +- Focus on services, information, or processes the agency handles +- Ensure questions are within the agency's scope of expertise +- Avoid topics outside the institute's domain + +*Output Format:* +Format the output as a single JSON object + +IMPORTANT: Return ONLY a single JSON object with a 'question' field. Do not return an array or code block., each with a "question" field. Example: + {{"question": "What is ...?"}} + +*Critical Requirements:* +- No markdown formatting, code blocks, or explanatory text +- Use natural ${language_name} grammar and expressions +- Ensure valid JSON syntax +- Generate only questions appropriate for this specific agency + +*Additional Instructions* +Create questions that real users would ask when seeking help or information from this agency through a chatbot interface. \ No newline at end of file diff --git a/DSL/Liquibase/changelog/classifier-script-v5-configuration.sql b/DSL/Liquibase/changelog/classifier-script-v5-configuration.sql new file mode 100644 index 00000000..cda1bb5e --- /dev/null +++ b/DSL/Liquibase/changelog/classifier-script-v5-configuration.sql @@ -0,0 +1,15 @@ +-- liquibase formatted sql + +-- changeset Erangi Ariyasena:classifier-script-v5-changeset1 +CREATE TABLE public.configuration ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + key VARCHAR(128), + value VARCHAR(128), + created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT configuration_pkey PRIMARY KEY (id) +); + +-- changeset Erangi Ariyasena:classifier-script-v5-changeset2 +INSERT INTO public.configuration (key, value) +VALUES ('session_length', '120'); diff --git a/DSL/Liquibase/changelog/global-classifier-script-v10-datasets-metadata.sql b/DSL/Liquibase/changelog/global-classifier-script-v10-datasets-metadata.sql new file mode 100644 index 00000000..9804c3b4 --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v10-datasets-metadata.sql @@ -0,0 +1,14 @@ +-- liquibase formatted sql + +-- changeset yourname:create-dataset-index-table +CREATE TABLE public.dataset_metadata ( + id BIGSERIAL PRIMARY KEY, + dataset_id VARCHAR(255) NOT NULL, + chunk_id VARCHAR(255) NOT NULL, + included_agencies JSONB NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Optionally, you can add an index for faster JSONB querying: +CREATE INDEX idx_dataset_index_included_agencies ON public.dataset_metadata USING GIN (included_agencies); +-- CREATE INDEX idx_dataset_index_row_ids ON public.dataset_index USING GIN (row_ids); \ No newline at end of file diff --git a/DSL/Liquibase/changelog/global-classifier-script-v11-model-training-jobs.sql b/DSL/Liquibase/changelog/global-classifier-script-v11-model-training-jobs.sql new file mode 100644 index 00000000..d083d116 --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v11-model-training-jobs.sql @@ -0,0 +1,31 @@ +-- liquibase formatted sql + +-- changeset charith:global-classifier-model-training-jobs-enum +-- Create training job status enum +CREATE TYPE training_job_status AS ENUM ('queued', 'training-in-progress', 'trained'); + +-- changeset charith:global-classifier-model-training-jobs-table +CREATE TABLE public.model_training_jobs ( + job_id BIGSERIAL PRIMARY KEY, + created_at INTEGER NOT NULL, + model_id BIGINT NOT NULL, + job_status training_job_status NOT NULL DEFAULT 'queued', + model_name VARCHAR(255) NOT NULL, + major_version INTEGER NOT NULL, + minor_version INTEGER NOT NULL, + latest BOOLEAN DEFAULT false, + deployment_environment VARCHAR(255) NOT NULL, + + + -- Add foreign key constraint to data_models table + CONSTRAINT fk_model_training_jobs_model_id + FOREIGN KEY (model_id) + REFERENCES public.data_models(model_id) + ON DELETE CASCADE +); + +-- changeset charith:global-classifier-model-training-jobs-indexes +-- Create indexes separately +CREATE INDEX idx_model_training_jobs_model_id ON public.model_training_jobs (model_id); +CREATE INDEX idx_model_training_jobs_status ON public.model_training_jobs (job_status); +CREATE INDEX idx_model_training_jobs_created_at ON public.model_training_jobs (created_at); \ No newline at end of file diff --git a/DSL/Liquibase/changelog/global-classifier-script-v14-dataset-progress-sessions.sql b/DSL/Liquibase/changelog/global-classifier-script-v14-dataset-progress-sessions.sql new file mode 100644 index 00000000..782abfdd --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v14-dataset-progress-sessions.sql @@ -0,0 +1,19 @@ +-- liquibase formatted sql + +-- changeset Erangi Ariyasena:classifier-script-v12-changeset1 +CREATE TYPE Generation_Progress_Status AS ENUM ('Initiating Dataset Generation', 'Downloading Source Datasets', 'Calling Dataset Generation', 'Dataset Generation In progress', 'New Dataset Uploading to s3', 'Success', 'Fail'); + +-- changeset Erangi Ariyasena:classifier-script-v12-changeset2 +CREATE TABLE dataset_progress_sessions ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + dataset_id BIGINT NOT NULL, + major_version INT NOT NULL DEFAULT 0, + minor_version INT NOT NULL DEFAULT 0, + latest BOOLEAN DEFAULT false, + process_complete BOOLEAN DEFAULT false, + progress_percentage INT, + generation_status Generation_Progress_Status, + generation_message TEXT , + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT dataset_progress_sessions_pkey PRIMARY KEY (id) +); \ No newline at end of file diff --git a/DSL/Liquibase/changelog/global-classifier-script-v15-data-model-progress-sessions.sql b/DSL/Liquibase/changelog/global-classifier-script-v15-data-model-progress-sessions.sql new file mode 100644 index 00000000..4775b20d --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v15-data-model-progress-sessions.sql @@ -0,0 +1,21 @@ +-- liquibase formatted sql + +-- changeset Erangi Ariyasena:classifier-script-v13-changeset1 +-- todo update with the latest changes +CREATE TYPE Training_Progress_Status AS ENUM ('Initiating Training', 'Training In-Progress', 'Deploying Model', 'Model Trained And Deployed','Training Failed','Deployment Failed'); + +-- changeset Erangi Ariyasena:classifier-script-v13-changeset2 +CREATE TABLE model_progress_sessions ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + model_id BIGINT NOT NULL, + model_name TEXT NOT NULL, + major_version INT NOT NULL DEFAULT 0, + minor_version INT NOT NULL DEFAULT 0, + latest BOOLEAN DEFAULT false, + process_complete BOOLEAN DEFAULT false, + progress_percentage INT, + training_progress_status Training_Progress_Status, + training_message TEXT , + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT model_progress_sessions_pkey PRIMARY KEY (id) +); \ No newline at end of file diff --git a/DSL/Liquibase/changelog/global-classifier-script-v16-inference-logs.sql b/DSL/Liquibase/changelog/global-classifier-script-v16-inference-logs.sql new file mode 100644 index 00000000..29f0ae2f --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v16-inference-logs.sql @@ -0,0 +1,12 @@ +-- liquibase formatted sql + +-- changeset thiru.dinesh:global-classifier-inference-logs-table +CREATE TABLE public.inference_logs ( + inference_id BIGSERIAL PRIMARY KEY, + chat_id VARCHAR(255) NOT NULL, + author_id VARCHAR(255) NOT NULL, + url VARCHAR(2048), + parsed_output JSONB NOT NULL, + created_timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + diff --git a/DSL/Liquibase/changelog/global-classifier-script-v17-remove-author-id.sql b/DSL/Liquibase/changelog/global-classifier-script-v17-remove-author-id.sql new file mode 100644 index 00000000..6633c827 --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v17-remove-author-id.sql @@ -0,0 +1,8 @@ +-- liquibase formatted sql + +-- changeset thiru.dinesh:v17-remove-author-id +-- comment: Remove author_id column from inference_logs table for privacy compliance + +ALTER TABLE public.inference_logs DROP COLUMN IF EXISTS author_id; + +-- rollback: ALTER TABLE public.inference_logs ADD COLUMN author_id character varying(255) NOT NULL; diff --git a/DSL/Liquibase/changelog/global-classifier-script-v18-testing-inference-logs.sql b/DSL/Liquibase/changelog/global-classifier-script-v18-testing-inference-logs.sql new file mode 100644 index 00000000..614dbd84 --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v18-testing-inference-logs.sql @@ -0,0 +1,19 @@ +-- liquibase formatted sql + +-- changeset Global-Classifier:v18-testing-inference-logs +-- comment: Create testing_inference_logs table for test model performance tracking + +CREATE TABLE IF NOT EXISTS public.testing_inference_logs ( + log_id BIGSERIAL PRIMARY KEY, + model_id VARCHAR(255) NOT NULL, + message TEXT NOT NULL, + inference_time_seconds INTEGER NOT NULL, + parsed_output JSONB NOT NULL, + created_timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_testing_inference_logs_model_id ON public.testing_inference_logs(model_id); +CREATE INDEX IF NOT EXISTS idx_testing_inference_logs_created_timestamp ON public.testing_inference_logs(created_timestamp); +CREATE INDEX IF NOT EXISTS idx_testing_inference_logs_inference_time ON public.testing_inference_logs(inference_time_seconds); + +-- rollback: DROP TABLE IF EXISTS public.testing_inference_logs; diff --git a/DSL/Liquibase/changelog/global-classifier-script-v19-add-inference-time-to-inference-logs.sql b/DSL/Liquibase/changelog/global-classifier-script-v19-add-inference-time-to-inference-logs.sql new file mode 100644 index 00000000..980ac2a0 --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v19-add-inference-time-to-inference-logs.sql @@ -0,0 +1,10 @@ +-- liquibase formatted sql + +-- changeset Global-Classifier:v19-add-inference-time-to-inference-logs +-- comment: Add inference_time_seconds column to inference_logs table for performance tracking + +ALTER TABLE public.inference_logs ADD COLUMN IF NOT EXISTS inference_time_seconds INTEGER; + +CREATE INDEX IF NOT EXISTS idx_inference_logs_inference_time ON public.inference_logs(inference_time_seconds); + +-- rollback: ALTER TABLE public.inference_logs DROP COLUMN IF EXISTS inference_time_seconds; diff --git a/DSL/Liquibase/changelog/global-classifier-script-v20-add-model-id-and-inference-time.sql b/DSL/Liquibase/changelog/global-classifier-script-v20-add-model-id-and-inference-time.sql new file mode 100644 index 00000000..f8d4162f --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v20-add-model-id-and-inference-time.sql @@ -0,0 +1,12 @@ +-- liquibase formatted sql + +-- changeset Global-Classifier:v20-add-model-id-to-inference-logs +-- comment: Add model_id column to inference_logs table and inference_time_seconds column + +ALTER TABLE public.inference_logs ADD COLUMN IF NOT EXISTS model_id VARCHAR(255); +ALTER TABLE public.inference_logs ADD COLUMN IF NOT EXISTS inference_time_seconds INTEGER; + +CREATE INDEX IF NOT EXISTS idx_inference_logs_model_id ON public.inference_logs(model_id); +CREATE INDEX IF NOT EXISTS idx_inference_logs_inference_time ON public.inference_logs(inference_time_seconds); + +-- rollback: ALTER TABLE public.inference_logs DROP COLUMN IF EXISTS model_id; ALTER TABLE public.inference_logs DROP COLUMN IF EXISTS inference_time_seconds; diff --git a/DSL/Liquibase/changelog/global-classifier-script-v21-rename-inference-time-to-milliseconds.sql b/DSL/Liquibase/changelog/global-classifier-script-v21-rename-inference-time-to-milliseconds.sql new file mode 100644 index 00000000..f20a4674 --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v21-rename-inference-time-to-milliseconds.sql @@ -0,0 +1,20 @@ +-- liquibase formatted sql + +-- changeset Global-Classifier:v21-rename-inference-time-to-milliseconds +-- comment: Rename inference_time_seconds to inference_time_ms to reflect millisecond precision + +-- Update inference_logs table +ALTER TABLE public.inference_logs RENAME COLUMN inference_time_seconds TO inference_time_ms; + +-- Update testing_inference_logs table +ALTER TABLE public.testing_inference_logs RENAME COLUMN inference_time_seconds TO inference_time_ms; + +-- Drop old indexes +DROP INDEX IF EXISTS idx_inference_logs_inference_time; +DROP INDEX IF EXISTS idx_testing_inference_logs_inference_time; + +-- Create new indexes with correct names +CREATE INDEX IF NOT EXISTS idx_inference_logs_inference_time_ms ON public.inference_logs(inference_time_ms); +CREATE INDEX IF NOT EXISTS idx_testing_inference_logs_inference_time_ms ON public.testing_inference_logs(inference_time_ms); + +-- rollback: ALTER TABLE public.inference_logs RENAME COLUMN inference_time_ms TO inference_time_seconds; ALTER TABLE public.testing_inference_logs RENAME COLUMN inference_time_ms TO inference_time_seconds; diff --git a/DSL/Liquibase/changelog/global-classifier-script-v22-model-training-jobs.sql b/DSL/Liquibase/changelog/global-classifier-script-v22-model-training-jobs.sql new file mode 100644 index 00000000..97de7907 --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v22-model-training-jobs.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql + +-- changeset thiru.dinesh:global-classifier-model-training-jobs-add-failed-status +-- Add 'training-failed' status to existing training_job_status enum +ALTER TYPE training_job_status ADD VALUE 'training-failed'; \ No newline at end of file diff --git a/DSL/Liquibase/changelog/global-classifier-script-v3-user-management.sql b/DSL/Liquibase/changelog/global-classifier-script-v3-user-management.sql new file mode 100644 index 00000000..f47656b8 --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v3-user-management.sql @@ -0,0 +1,41 @@ +-- liquibase formatted sql + +-- changeset Erangi Ariyasena:classifier-script-v3-changeset1 +CREATE TYPE user_status AS ENUM ('active','deleted'); + +-- changeset Erangi Ariyasena:classifier-script-v3-changeset2 +CREATE TABLE public."user" ( + id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, + login VARCHAR(50) NOT NULL, + password_hash VARCHAR(60), + first_name VARCHAR(50), + last_name VARCHAR(50), + id_code VARCHAR(50) NOT NULL, + display_name VARCHAR(50), + status user_status, + csa_title VARCHAR, + csa_email VARCHAR, + created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT user_pkey PRIMARY KEY (id) +); + +CREATE TABLE public."authority" ( + name VARCHAR(50) PRIMARY KEY +); + +CREATE TABLE public."user_authority" ( + id int8 NOT NULL GENERATED BY DEFAULT AS IDENTITY, + user_id VARCHAR(50) NOT NULL, + authority_name VARCHAR[] NOT NULL, + created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT user_authority_pkey PRIMARY KEY (id) +); + +-- changeset Erangi Ariyasena:classifier-script-v1-changeset3 + +INSERT INTO public."user" (login,password_hash,first_name,last_name,id_code,display_name,status,csa_title,csa_email) +VALUES ('EE30303039914','ok','classifier','test','EE30303039914','classifier','active','Title','classifier.doe@example.com'); + +INSERT INTO public."user_authority" ( user_id, authority_name) +VALUES ('EE30303039914', ARRAY['ROLE_ADMINISTRATOR', 'ROLE_MODEL_TRAINER'] ); + diff --git a/DSL/Liquibase/changelog/global-classifier-script-v4-authority-data.xml b/DSL/Liquibase/changelog/global-classifier-script-v4-authority-data.xml new file mode 100644 index 00000000..9a9ccc24 --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v4-authority-data.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/DSL/Liquibase/changelog/global-classifier-script-v6-integrated-agencies.sql b/DSL/Liquibase/changelog/global-classifier-script-v6-integrated-agencies.sql new file mode 100644 index 00000000..37b04f0c --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v6-integrated-agencies.sql @@ -0,0 +1,26 @@ +-- liquibase formatted sql + +-- changeset erangi:global-classifier-integrated-agency-enums +-- Create deployment status enum +CREATE TYPE deployment_status AS ENUM ('deployed', 'testing', 'undeployed'); + +-- Create sync status enum +CREATE TYPE sync_status AS ENUM ('Unavailable_in_CKB', 'Synced_with_CKB','Sync_with_CKB_Failed', 'Resync_needed_with_CKB','Resync_in_progress_with_CKB','Sync_in_progress_with_CKB'); + +-- changeset erangi:global-classifier-integrated-agency-table +CREATE TABLE public.integrated_agencies ( + agency_id VARCHAR(255) NOT NULL, + agency_name VARCHAR(255) NOT NULL, + group_key VARCHAR(255), + is_latest BOOLEAN NOT NULL DEFAULT TRUE, + deployment_status deployment_status NOT NULL, + is_enabled BOOLEAN NOT NULL DEFAULT FALSE, + agency_data_hash VARCHAR(255), + enable_allowed BOOLEAN NOT NULL DEFAULT FALSE, + last_model_trained VARCHAR(255), + last_updated_timestamp TIMESTAMP WITH TIME ZONE, + last_trained_timestamp TIMESTAMP WITH TIME ZONE DEFAULT NULL, + sync_status sync_status, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT integrated_agencies_pkey PRIMARY KEY (agency_id) +); diff --git a/DSL/Liquibase/changelog/global-classifier-script-v8-datasets.sql b/DSL/Liquibase/changelog/global-classifier-script-v8-datasets.sql new file mode 100644 index 00000000..2b3a71bf --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v8-datasets.sql @@ -0,0 +1,20 @@ +-- Liquibase changeset for creating the datasets table +-- changeset erangiar:datasets-table +CREATE TABLE public.dataset_versions ( + id BIGSERIAL PRIMARY KEY, + major INTEGER NOT NULL, + minor INTEGER NOT NULL, + generation_status VARCHAR(64) NOT NULL, + last_model_trained VARCHAR(255), + connected_models JSONB, + last_trained TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE public.datasets ( + item_id BIGSERIAL PRIMARY KEY, + data_item VARCHAR(64) NOT NULL, + agency VARCHAR(255), + dataset_version_id INTEGER NOT NULL, +); diff --git a/DSL/Liquibase/changelog/global-classifier-script-v9-data-models.sql b/DSL/Liquibase/changelog/global-classifier-script-v9-data-models.sql new file mode 100644 index 00000000..8a99502b --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-script-v9-data-models.sql @@ -0,0 +1,50 @@ +-- liquibase formatted sql + +-- changeset erangi:global-classifier-integrated-agency-enums +-- Create model status enum +CREATE TYPE model_status AS ENUM ('active', 'deprecated'); + +-- Create training status enum +CREATE TYPE training_status AS ENUM ('retraining_needed', 'trained', 'training_in_progress', 'training_failed', 'initiating_training'); + +-- Create deployment environment enum +CREATE TYPE deployment_environment AS ENUM ('undeployed', 'testing', 'production'); + +CREATE TYPE base_models AS ENUM ('bert', 'roberta', 'xlm','estbert', 'xlm-roberta', 'multilingual-distilbert'); + +-- changeset erangi:global-classifier-models-metadata-table +CREATE TABLE public.data_models ( + model_id BIGSERIAL PRIMARY KEY, + model_group_key VARCHAR(255) NOT NULL, + model_name VARCHAR(255) NOT NULL, + major INTEGER NOT NULL, + minor INTEGER NOT NULL, + latest BOOLEAN DEFAULT false, + deployment_env deployment_environment NOT NULL, + training_status training_status DEFAULT 'initiating_training', + base_models JSONB NOT NULL, + last_trained TIMESTAMP WITH TIME ZONE, + connected_ds_id BIGINT NOT NULL, + connected_ds_major_version INTEGER, + connected_ds_minor_version INTEGER, + model_status model_status DEFAULT 'active', + model_s3_location VARCHAR(2048), + training_results JSONB, + created_timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- changeset erangi:global-classifier-model_configurations +CREATE TABLE model_configurations ( + id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY, + base_models JSONB, + deployment_environments deployment_environment[], + CONSTRAINT model_configurations_pkey PRIMARY KEY (id) +); + +-- changeset erangi:global-classifier-model_configurations-add-data +INSERT INTO model_configurations (base_models, deployment_environments) VALUES +( + '["estbert", "xlm-roberta", "multilingual-distilbert"]'::JSONB, + ARRAY['undeployed', 'testing', 'production']::deployment_environment[] +); \ No newline at end of file diff --git a/DSL/Liquibase/changelog/global-classifier-v12-datasets.sql b/DSL/Liquibase/changelog/global-classifier-v12-datasets.sql new file mode 100644 index 00000000..e92544b5 --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-v12-datasets.sql @@ -0,0 +1,21 @@ +-- Liquibase changeset for creating the datasets table +-- changeset erangiar:datasets-table +CREATE TABLE public.dataset_versions ( + id BIGSERIAL PRIMARY KEY, + major INTEGER NOT NULL, + minor INTEGER NOT NULL, + generation_status VARCHAR(64) NOT NULL, + last_model_trained VARCHAR(255), + connected_models JSONB, + last_trained TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE public.datasets ( + item_id VARCHAR(64) PRIMARY KEY, + agency_name VARCHAR(255), + agency_id VARCHAR(255) NOT NULL, + data_item VARCHAR(400) NOT NULL, + dataset_version_id INTEGER NOT NULL +); \ No newline at end of file diff --git a/DSL/Liquibase/changelog/global-classifier-v13-dataset-update-stored-procedure.sql b/DSL/Liquibase/changelog/global-classifier-v13-dataset-update-stored-procedure.sql new file mode 100644 index 00000000..48c38031 --- /dev/null +++ b/DSL/Liquibase/changelog/global-classifier-v13-dataset-update-stored-procedure.sql @@ -0,0 +1,9 @@ +CREATE OR REPLACE FUNCTION import_csv_dynamic(file_path TEXT) +RETURNS void AS $$ +BEGIN + EXECUTE format( + 'COPY public.datasets FROM %L WITH (FORMAT CSV, HEADER TRUE, DELIMITER '','', ENCODING ''UTF8'')', + file_path + ); +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/DSL/Liquibase/changelog/mock-global-classifier-script-v7-centops-ckb.sql b/DSL/Liquibase/changelog/mock-global-classifier-script-v7-centops-ckb.sql new file mode 100644 index 00000000..1668065e --- /dev/null +++ b/DSL/Liquibase/changelog/mock-global-classifier-script-v7-centops-ckb.sql @@ -0,0 +1,16 @@ + +-- changeset erangi:global-classifier-centops-ckb-tables +CREATE TABLE public.mock_centops ( + id SERIAL PRIMARY KEY, + agency_id VARCHAR(255) NOT NULL, + agency_name VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE TABLE public.mock_ckb ( + agency_id VARCHAR(50) PRIMARY KEY, + agency_data_hash VARCHAR(255) NOT NULL, + data_url TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + diff --git a/DSL/Liquibase/data/authority.csv b/DSL/Liquibase/data/authority.csv new file mode 100644 index 00000000..c110c607 --- /dev/null +++ b/DSL/Liquibase/data/authority.csv @@ -0,0 +1,3 @@ +name +ROLE_ADMINISTRATOR +ROLE_MODEL_TRAINER diff --git a/DSL/Liquibase/master.yml b/DSL/Liquibase/master.yml index 0861e812..2d7f5500 100644 --- a/DSL/Liquibase/master.yml +++ b/DSL/Liquibase/master.yml @@ -2,4 +2,40 @@ databaseChangeLog: - include: file: changelog/global-classifier-script-v1-classification-results.sql - include: - file: changelog/global-classifier-script-v2-classification-feedback.sql \ No newline at end of file + file: changelog/global-classifier-script-v2-classification-feedback.sql + - include: + file: changelog/global-classifier-script-v3-user-management.sql + - include: + file: changelog/classifier-script-v5-configuration.sql + - include: + file: changelog/global-classifier-script-v4-authority-data.xml + - include: + file: changelog/global-classifier-script-v6-integrated-agencies.sql + - include: + file: changelog/mock-global-classifier-script-v7-centops-ckb.sql + - include: + file: changelog/global-classifier-script-v9-data-models.sql + - include: + file: changelog/global-classifier-script-v10-datasets-metadata.sql + - include: + file: changelog/global-classifier-script-v11-model-training-jobs.sql + - include: + file: changelog/global-classifier-v12-datasets.sql + - include: + file: changelog/global-classifier-v13-dataset-update-stored-procedure.sql + - include: + file: changelog/global-classifier-script-v14-dataset-progress-sessions.sql + - include: + file: changelog/global-classifier-script-v15-data-model-progress-sessions.sql + - include: + file: changelog/global-classifier-script-v16-inference-logs.sql + - include: + file: changelog/global-classifier-script-v17-remove-author-id.sql + - include: + file: changelog/global-classifier-script-v18-testing-inference-logs.sql + - include: + file: changelog/global-classifier-script-v20-add-model-id-and-inference-time.sql + - include: + file: changelog/global-classifier-script-v21-rename-inference-time-to-milliseconds.sql + - include: + file: changelog/global-classifier-script-v22-model-training-jobs.sql diff --git a/DSL/OpenSearch/deploy-opensearch.sh b/DSL/OpenSearch/deploy-opensearch.sh new file mode 100644 index 00000000..5ea4eb5c --- /dev/null +++ b/DSL/OpenSearch/deploy-opensearch.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +URL=http://localhost:9200 +AUTH=admin +MOCK_ALLOWED=${3:-false} + +if [[ -z $URL || -z $AUTH ]]; then + echo "Url and Auth are required" + exit 1 +fi + +# dataset_progress_sessions +curl -XDELETE "$URL/dataset_progress_sessions?ignore_unavailable=true" -u "$AUTH" --insecure +curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/dataset_progress_sessions" -ku "$AUTH" --data-binary "@fieldMappings/dataset_progress_sessions.json" +if $MOCK_ALLOWED; then curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/dataset_progress_sessions/_bulk" -ku "$AUTH" --data-binary "@mock/dataset_progress_sessions.json"; fi + + +# data_model_progress_sessions +curl -XDELETE "$URL/data_model_progress_sessions?ignore_unavailable=true" -u "$AUTH" --insecure +curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/data_model_progress_sessions" -ku "$AUTH" --data-binary "@fieldMappings/data_model_progress_sessions.json" +if $MOCK_ALLOWED; then curl -H "Content-Type: application/x-ndjson" -X PUT "$URL/data_model_progress_sessions/_bulk" -ku "$AUTH" --data-binary "@mock/data_model_progress_sessions.json"; fi \ No newline at end of file diff --git a/DSL/OpenSearch/fieldMappings/data_model_progress_sessions.json b/DSL/OpenSearch/fieldMappings/data_model_progress_sessions.json new file mode 100644 index 00000000..b21f7890 --- /dev/null +++ b/DSL/OpenSearch/fieldMappings/data_model_progress_sessions.json @@ -0,0 +1,24 @@ +{ + "mappings": { + "properties": { + "sessionId": { + "type": "keyword" + }, + "trainingStatus": { + "type": "keyword" + }, + "trainingMessage": { + "type": "keyword" + }, + "progressPercentage": { + "type": "integer" + }, + "timestamp": { + "type": "keyword" + }, + "sentTo": { + "type": "keyword" + } + } + } +} diff --git a/DSL/OpenSearch/fieldMappings/dataset_progress_sessions.json b/DSL/OpenSearch/fieldMappings/dataset_progress_sessions.json new file mode 100644 index 00000000..35e492a1 --- /dev/null +++ b/DSL/OpenSearch/fieldMappings/dataset_progress_sessions.json @@ -0,0 +1,24 @@ +{ + "mappings": { + "properties": { + "sessionId": { + "type": "keyword" + }, + "generationStatus": { + "type": "keyword" + }, + "generationMessage": { + "type": "keyword" + }, + "progressPercentage": { + "type": "integer" + }, + "timestamp": { + "type": "keyword" + }, + "sentTo": { + "type": "keyword" + } + } + } +} diff --git a/DSL/OpenSearch/mock/data_model_progress_sessions.json b/DSL/OpenSearch/mock/data_model_progress_sessions.json new file mode 100644 index 00000000..c4e63318 --- /dev/null +++ b/DSL/OpenSearch/mock/data_model_progress_sessions.json @@ -0,0 +1,8 @@ +{"index":{"_id":"1"}} +{"sessionId": "10001000","trainingStatus": "Initiating Training","trainingMessage": "","progressPercentage": 1,"timestamp": "1801371325497", "sentTo": []} +{"index":{"_id":"2"}} +{"sessionId": "10002000","trainingStatus": "Training In-Progress","trainingMessage": "","progressPercentage": 26,"timestamp": "1801371325597", "sentTo": []} +{"index":{"_id":"3"}} +{"sessionId": "10003000","trainingStatus": "Deploying Model","trainingMessage": "","progressPercentage": 52,"timestamp": "1801371325697", "sentTo": []} +{"index":{"_id":"4"}} +{"sessionId": "100040000","trainingStatus": "Model Trained And Deployed","trainingMessage": "","progressPercentage": 97,"timestamp": "1801371325797", "sentTo": []} diff --git a/DSL/OpenSearch/mock/dataset_progress_sessions.json b/DSL/OpenSearch/mock/dataset_progress_sessions.json new file mode 100644 index 00000000..b8f7fc12 --- /dev/null +++ b/DSL/OpenSearch/mock/dataset_progress_sessions.json @@ -0,0 +1,13 @@ +{"index":{"_id":"1"}} +{"sessionId": "10001000","generationStatus": "Initiating Validation","generationMessage": "","progressPercentage": 1,"timestamp": "1801371325497", "sentTo": []} +{"index":{"_id":"2"}} +{"sessionId": "20001000","generationStatus": "Validation In-Progress","generationMessage": "","progressPercentage": 26,"timestamp": "1801371325597", "sentTo": []} +{"index":{"_id":"3"}} +{"sessionId": "30001000","generationStatus": "Cleaning Dataset","generationMessage": "","progressPercentage": 52,"timestamp": "1801371325697", "sentTo": []} +{"index":{"_id":"4"}} +{"sessionId": "40001000","generationStatus": "Generating Data","generationMessage": "","progressPercentage": 97,"timestamp": "1801371325797", "sentTo": []} +{"index":{"_id":"5"}} +{"sessionId": "50001000","generationStatus": "Success","generationMessage": "","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} +{"index":{"_id":"6"}} +{"sessionId": "60001000","generationStatus": "Fail","generationMessage": "Validation failed because class called 'complaints' in 'police' column doesn't exist in the dataset`","progressPercentage": 100,"timestamp": "1801371325797", "sentTo": []} + diff --git a/DSL/Resql/global-classifier/POST/check-agency-data-availability.sql b/DSL/Resql/global-classifier/POST/check-agency-data-availability.sql new file mode 100644 index 00000000..6592a7bc --- /dev/null +++ b/DSL/Resql/global-classifier/POST/check-agency-data-availability.sql @@ -0,0 +1,10 @@ +WITH parsed_ids AS ( + SELECT unnest(string_to_array(:agencyIds, ' ')) AS agency_id +) +SELECT + p.agency_id, + CASE WHEN c.data_url IS NOT NULL THEN true ELSE false END AS is_data_available +FROM + parsed_ids p +LEFT JOIN + public.mock_ckb c ON p.agency_id = c.agency_id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/create-data-model-training-job.sql b/DSL/Resql/global-classifier/POST/create-data-model-training-job.sql new file mode 100644 index 00000000..3c6273f6 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/create-data-model-training-job.sql @@ -0,0 +1,15 @@ +INSERT INTO ModelTrainingJobs ( + model_id, + job_status, + created_at +) VALUES ( + :modelId, + 'training-in-progress', + EXTRACT(EPOCH FROM NOW())::INT +) +RETURNING + job_id, + model_id, + job_status, + created_at, + 'Training job created' AS message; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/create-datamodels-version.sql b/DSL/Resql/global-classifier/POST/create-datamodels-version.sql new file mode 100644 index 00000000..4334b5ce --- /dev/null +++ b/DSL/Resql/global-classifier/POST/create-datamodels-version.sql @@ -0,0 +1,51 @@ +INSERT INTO public.data_models ( + model_group_key, + model_name, + major, + minor, + latest, + deployment_env, + training_status, + base_models, + last_trained, + connected_ds_id, + connected_ds_major_version, + connected_ds_minor_version, + model_status, + model_s3_location, + training_results, + created_timestamp, + updated_timestamp +) VALUES ( + :modelGroupKey, + :modelName, + :major, + :minor, + true, + :deploymentEnv::deployment_environment, + 'initiating_training'::training_status, + :baseModels::JSONB, + NULL, + :connectedDsId, + :connectedDsMajorVersion, + :connectedDsMinorVersion, + 'active'::model_status, + NULL, + NULL, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP +) +RETURNING + model_id, + model_group_key, + model_name, + major, + minor, + latest, + deployment_env, + training_status, + base_models, + created_timestamp, + connected_ds_id, + connected_ds_major_version, + connected_ds_minor_version; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/delete-completed-data-model-progress-sessions.sql b/DSL/Resql/global-classifier/POST/delete-completed-data-model-progress-sessions.sql new file mode 100644 index 00000000..f675ab0e --- /dev/null +++ b/DSL/Resql/global-classifier/POST/delete-completed-data-model-progress-sessions.sql @@ -0,0 +1,3 @@ +DELETE FROM model_progress_sessions +WHERE process_complete = true +RETURNING id; diff --git a/DSL/Resql/global-classifier/POST/delete-completed-dataset-progress-sessions.sql b/DSL/Resql/global-classifier/POST/delete-completed-dataset-progress-sessions.sql new file mode 100644 index 00000000..eb0b5728 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/delete-completed-dataset-progress-sessions.sql @@ -0,0 +1,3 @@ +DELETE FROM dataset_progress_sessions +WHERE process_complete = true +RETURNING id; diff --git a/DSL/Resql/global-classifier/POST/delete-data-model-by-id.sql b/DSL/Resql/global-classifier/POST/delete-data-model-by-id.sql new file mode 100644 index 00000000..cd3deba4 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/delete-data-model-by-id.sql @@ -0,0 +1 @@ +DELETE FROM data_models WHERE model_id = :model_id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/delete-dataset-records.sql b/DSL/Resql/global-classifier/POST/delete-dataset-records.sql new file mode 100644 index 00000000..cbe829a5 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/delete-dataset-records.sql @@ -0,0 +1,2 @@ +DELETE FROM public.datasets +WHERE item_id = ANY(ARRAY[:itemIds]::text[]); \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/delete-dataset-version.sql b/DSL/Resql/global-classifier/POST/delete-dataset-version.sql new file mode 100644 index 00000000..29f2ccc4 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/delete-dataset-version.sql @@ -0,0 +1,3 @@ +DELETE FROM public.dataset_versions +WHERE id = :datasetVersionId +RETURNING *; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/delete-datasets-by-version.sql b/DSL/Resql/global-classifier/POST/delete-datasets-by-version.sql new file mode 100644 index 00000000..9642adc2 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/delete-datasets-by-version.sql @@ -0,0 +1,2 @@ +DELETE FROM public.datasets +WHERE dataset_version_id = :datasetVersionId; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/delete-user.sql b/DSL/Resql/global-classifier/POST/delete-user.sql new file mode 100644 index 00000000..eb8ccade --- /dev/null +++ b/DSL/Resql/global-classifier/POST/delete-user.sql @@ -0,0 +1,36 @@ +WITH active_administrators AS (SELECT user_id + FROM user_authority + WHERE 'ROLE_ADMINISTRATOR' = ANY (authority_name) + AND id IN (SELECT max(id) + FROM user_authority + GROUP BY user_id)), +delete_user AS ( +INSERT +INTO "user" (login, password_hash, first_name, last_name, id_code, display_name, status, created, csa_title, csa_email) +SELECT login, + password_hash, + first_name, + last_name, + id_code, + display_name, + 'deleted', + :created::timestamp with time zone, + csa_title, + csa_email +FROM "user" +WHERE id_code = :userIdCode + AND status <> 'deleted' + AND id IN (SELECT max(id) FROM "user" WHERE id_code = :userIdCode) + AND (1 < (SELECT COUNT(user_id) FROM active_administrators) + OR (1 = (SELECT COUNT(user_id) FROM active_administrators) + AND :userIdCode NOT IN (SELECT user_id FROM active_administrators)))), +delete_authority AS ( +INSERT +INTO user_authority (user_id, authority_name, created) +SELECT :userIdCode as users, ARRAY []::varchar[], :created::timestamp with time zone +FROM user_authority +WHERE 1 < (SELECT COUNT(user_id) FROM active_administrators) + OR (1 = (SELECT COUNT(user_id) FROM active_administrators) + AND :userIdCode NOT IN (SELECT user_id FROM active_administrators)) +GROUP BY users) +SELECT max(status) FROM "user" WHERE id_code = :userIdCode; diff --git a/DSL/Resql/global-classifier/POST/get-agencies.sql b/DSL/Resql/global-classifier/POST/get-agencies.sql new file mode 100644 index 00000000..514aed0b --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-agencies.sql @@ -0,0 +1,28 @@ +SELECT + agency_id, + agency_name, + group_key, + is_latest, + deployment_status, + is_enabled, + agency_data_hash, + enable_allowed, + last_model_trained, + last_updated_timestamp, + last_trained_timestamp, + sync_status, + created_at, + CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages +FROM + integrated_agencies +WHERE + (:agency_name = 'all' OR agency_name ILIKE '%' || :agency_name || '%') +ORDER BY + CASE WHEN :sort_by = 'agency_name' AND :sort_type = 'asc' THEN agency_name END ASC, + CASE WHEN :sort_by = 'agency_name' AND :sort_type = 'desc' THEN agency_name END DESC, + CASE WHEN :sort_by = 'created_at' AND :sort_type = 'asc' THEN created_at END ASC, + CASE WHEN :sort_by = 'created_at' AND :sort_type = 'desc' THEN created_at END DESC, + CASE WHEN :sort_by = 'last_updated_timestamp' AND :sort_type = 'asc' THEN last_updated_timestamp END ASC, + CASE WHEN :sort_by = 'last_updated_timestamp' AND :sort_type = 'desc' THEN last_updated_timestamp END DESC, + CASE WHEN :sort_by IS NULL OR :sort_by = '' THEN last_updated_timestamp END DESC +OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-agency-datahash.sql b/DSL/Resql/global-classifier/POST/get-agency-datahash.sql new file mode 100644 index 00000000..6357efe5 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-agency-datahash.sql @@ -0,0 +1 @@ +SELECT agency_data_hash FROM public.integrated_agencies WHERE agency_id = :agencyId; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-agency.sql b/DSL/Resql/global-classifier/POST/get-agency.sql new file mode 100644 index 00000000..a98fbc23 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-agency.sql @@ -0,0 +1,3 @@ +SELECT agency_id +FROM "integrated_agencies" +WHERE agency_id = :agencyId; diff --git a/DSL/Resql/global-classifier/POST/get-all-agencies.sql b/DSL/Resql/global-classifier/POST/get-all-agencies.sql new file mode 100644 index 00000000..87944520 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-all-agencies.sql @@ -0,0 +1,5 @@ +SELECT + agency_id, + agency_name +FROM + integrated_agencies; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-all-datamodel-versions.sql b/DSL/Resql/global-classifier/POST/get-all-datamodel-versions.sql new file mode 100644 index 00000000..7b5b6b7e --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-all-datamodel-versions.sql @@ -0,0 +1,7 @@ +SELECT + model_id as id, + model_name, + major, + minor +FROM public.data_models +ORDER BY model_id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-all-dataset-versions.sql b/DSL/Resql/global-classifier/POST/get-all-dataset-versions.sql new file mode 100644 index 00000000..78be3514 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-all-dataset-versions.sql @@ -0,0 +1,3 @@ +SELECT id, major, minor +FROM public.dataset_versions +ORDER BY id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-chunk-ids-by-agency.sql b/DSL/Resql/global-classifier/POST/get-chunk-ids-by-agency.sql new file mode 100644 index 00000000..4186f446 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-chunk-ids-by-agency.sql @@ -0,0 +1,7 @@ +SELECT chunk_id +FROM public.dataset_metadata +WHERE EXISTS ( + SELECT 1 + FROM jsonb_array_elements_text(included_agencies) AS elem + WHERE elem = :agencyId::text +); \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-chunks-and-agencies.sql b/DSL/Resql/global-classifier/POST/get-chunks-and-agencies.sql new file mode 100644 index 00000000..038c8edd --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-chunks-and-agencies.sql @@ -0,0 +1,4 @@ +SELECT chunk_id, included_agencies +FROM public.dataset_metadata +WHERE dataset_id = :datasetId +ORDER BY chunk_id::int \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-configuration.sql b/DSL/Resql/global-classifier/POST/get-configuration.sql new file mode 100644 index 00000000..f03b322e --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-configuration.sql @@ -0,0 +1,5 @@ +SELECT id, key, value +FROM configuration +WHERE key=:key +AND id IN (SELECT max(id) from configuration GROUP BY key) +AND NOT deleted; diff --git a/DSL/Resql/global-classifier/POST/get-data-model-by-id.sql b/DSL/Resql/global-classifier/POST/get-data-model-by-id.sql new file mode 100644 index 00000000..8b2deab7 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-data-model-by-id.sql @@ -0,0 +1,6 @@ +SELECT + * +FROM + data_models +WHERE + model_id = :model_id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-data-model-info-by-given-model-id.sql b/DSL/Resql/global-classifier/POST/get-data-model-info-by-given-model-id.sql new file mode 100644 index 00000000..4fec7683 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-data-model-info-by-given-model-id.sql @@ -0,0 +1,5 @@ +SELECT + connected_ds_id, + base_models +FROM data_models +WHERE model_id = :model_id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-data-model-options.sql b/DSL/Resql/global-classifier/POST/get-data-model-options.sql new file mode 100644 index 00000000..d339bd8c --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-data-model-options.sql @@ -0,0 +1,2 @@ +SELECT base_models, deployment_environments +FROM model_configurations; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-data-model-progress-sessions.sql b/DSL/Resql/global-classifier/POST/get-data-model-progress-sessions.sql new file mode 100644 index 00000000..7e3a38c6 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-data-model-progress-sessions.sql @@ -0,0 +1,18 @@ +SELECT + mps.id, + mps.model_id, + mps.model_name, + mps.major_version, + mps.minor_version, + mps.latest, + mps.process_complete, + mps.progress_percentage, + mps.training_progress_status as training_status, + mps.training_message, + mm.deployment_env +FROM model_progress_sessions mps +JOIN + data_models mm +ON + mps.model_id = mm.model_id +ORDER BY mps.created_time DESC; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-data-models.sql b/DSL/Resql/global-classifier/POST/get-data-models.sql new file mode 100644 index 00000000..cf6f80ac --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-data-models.sql @@ -0,0 +1,35 @@ +SELECT + model_id, + model_group_key, + model_name, + major, + minor, + latest, + connected_ds_id, + connected_ds_major_version, + connected_ds_minor_version, + base_models, + deployment_env, + created_timestamp, + updated_timestamp, + last_trained, + model_status, + training_status, + training_results, + CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages +FROM + data_models +WHERE + (:training_status = 'all' OR training_status = :training_status::training_status) + AND (:model_status = 'all' OR model_status = :model_status::model_status) + AND (:deployment_env = 'all' OR deployment_env = :deployment_env::deployment_environment) + AND deployment_env != 'production'::deployment_environment +ORDER BY + CASE WHEN :sort_by = 'createdAt' AND :sort_type = 'asc' THEN created_timestamp END ASC, + CASE WHEN :sort_by = 'createdAt' AND :sort_type = 'desc' THEN created_timestamp END DESC, + CASE WHEN :sort_by = 'lastTrained' AND :sort_type = 'asc' THEN last_trained END ASC, + CASE WHEN :sort_by = 'lastTrained' AND :sort_type = 'desc' THEN last_trained END DESC, + CASE WHEN :sort_by = 'modelName' AND :sort_type = 'asc' THEN model_name END ASC, + CASE WHEN :sort_by = 'modelName' AND :sort_type = 'desc' THEN model_name END DESC, + CASE WHEN :sort_by IS NULL OR :sort_by = '' THEN created_timestamp END DESC +OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-data-unavailable-agencies.sql b/DSL/Resql/global-classifier/POST/get-data-unavailable-agencies.sql new file mode 100644 index 00000000..8c1de65e --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-data-unavailable-agencies.sql @@ -0,0 +1,3 @@ +SELECT agency_id +FROM public.integrated_agencies +WHERE sync_status = 'Unavailable_in_CKB'; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-dataset-by-id.sql b/DSL/Resql/global-classifier/POST/get-dataset-by-id.sql new file mode 100644 index 00000000..1a0b245c --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-dataset-by-id.sql @@ -0,0 +1,3 @@ +SELECT id, major, minor +FROM public.dataset_versions +WHERE id = :id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-dataset-progress-sessions.sql b/DSL/Resql/global-classifier/POST/get-dataset-progress-sessions.sql new file mode 100644 index 00000000..ed66084d --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-dataset-progress-sessions.sql @@ -0,0 +1,12 @@ +SELECT + id, + dataset_id, + major_version, + minor_version, + latest, + process_complete, + progress_percentage, + generation_status, + generation_message +FROM dataset_progress_sessions +ORDER BY created_time DESC; diff --git a/DSL/Resql/global-classifier/POST/get-dataset-total-count.sql b/DSL/Resql/global-classifier/POST/get-dataset-total-count.sql new file mode 100644 index 00000000..054aff78 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-dataset-total-count.sql @@ -0,0 +1,3 @@ +SELECT COUNT(*) as total_count +FROM datasets +WHERE dataset_version_id = :datasetVersionId; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-datasets-connected-models.sql b/DSL/Resql/global-classifier/POST/get-datasets-connected-models.sql new file mode 100644 index 00000000..37f100c1 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-datasets-connected-models.sql @@ -0,0 +1,10 @@ +SELECT + dm.model_name, + dm.major, + dm.minor +FROM public.data_models dm +WHERE dm.model_id = ANY( + SELECT jsonb_array_elements_text(connected_models)::BIGINT + FROM public.dataset_versions + WHERE id = :datasetId +); \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-datasets-data.sql b/DSL/Resql/global-classifier/POST/get-datasets-data.sql new file mode 100644 index 00000000..365b7648 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-datasets-data.sql @@ -0,0 +1,15 @@ +SELECT + item_id, + agency_name, + agency_id, + data_item, + dataset_version_id, + CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages +FROM + public.datasets +WHERE + dataset_version_id = :dataset_version_id + AND (:client_id = 'all' OR agency_id = :client_id) +ORDER BY + item_id DESC +OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-datasets.sql b/DSL/Resql/global-classifier/POST/get-datasets.sql new file mode 100644 index 00000000..bd2febd8 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-datasets.sql @@ -0,0 +1,22 @@ +SELECT + id, + major, + minor, + created_at, + generation_status, + last_model_trained, + last_trained, + CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages +FROM + dataset_versions +WHERE + (:generation_status = 'all' OR generation_status ILIKE '%' || :generation_status || '%') +ORDER BY + CASE WHEN :sort_by = 'created_at' AND :sort_type = 'asc' THEN created_at END ASC, + CASE WHEN :sort_by = 'created_at' AND :sort_type = 'desc' THEN created_at END DESC, + -- CASE WHEN :sort_by = 'major' AND :sort_type = 'asc' THEN major END ASC, + -- CASE WHEN :sort_by = 'major' AND :sort_type = 'desc' THEN major END DESC, + -- CASE WHEN :sort_by = 'minor' AND :sort_type = 'asc' THEN minor END ASC, + -- CASE WHEN :sort_by = 'minor' AND :sort_type = 'desc' THEN minor END DESC, + CASE WHEN :sort_by IS NULL OR :sort_by = '' THEN created_at END DESC +OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-highest-major-version.sql b/DSL/Resql/global-classifier/POST/get-highest-major-version.sql new file mode 100644 index 00000000..63666f93 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-highest-major-version.sql @@ -0,0 +1,9 @@ +SELECT major, model_id +FROM public.data_models +WHERE model_group_key = :modelGroupKey + AND major = ( + SELECT MAX(major) + FROM public.data_models + WHERE model_group_key = :modelGroupKey + ) +LIMIT 1; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-latest-datamodel.sql b/DSL/Resql/global-classifier/POST/get-latest-datamodel.sql new file mode 100644 index 00000000..ba14d05f --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-latest-datamodel.sql @@ -0,0 +1,5 @@ +SELECT major, minor, model_id +FROM public.data_models +WHERE model_group_key = :modelGroupKey +ORDER BY created_timestamp DESC +LIMIT 1; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-latest-dataset.sql b/DSL/Resql/global-classifier/POST/get-latest-dataset.sql new file mode 100644 index 00000000..6bf4a871 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-latest-dataset.sql @@ -0,0 +1,4 @@ +SELECT id, major, minor +FROM public.dataset_versions +ORDER BY created_at DESC +LIMIT 1; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-production-data-model.sql b/DSL/Resql/global-classifier/POST/get-production-data-model.sql new file mode 100644 index 00000000..24d067ec --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-production-data-model.sql @@ -0,0 +1,5 @@ +SELECT model_id, model_group_key, model_name, major, minor, latest, training_status, deployment_env, last_trained, model_status, training_results, created_timestamp, updated_timestamp, connected_ds_id, connected_ds_major_version, connected_ds_minor_version +FROM public.data_models +WHERE deployment_env = 'production'::deployment_environment +ORDER BY updated_timestamp DESC +LIMIT 1; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-production-model-id.sql b/DSL/Resql/global-classifier/POST/get-production-model-id.sql new file mode 100644 index 00000000..0d293b48 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-production-model-id.sql @@ -0,0 +1,5 @@ +SELECT model_id +FROM public.data_models +WHERE deployment_env = 'production'::deployment_environment +ORDER BY updated_timestamp DESC +LIMIT 1; diff --git a/DSL/Resql/global-classifier/POST/get-queued-training-job.sql b/DSL/Resql/global-classifier/POST/get-queued-training-job.sql new file mode 100644 index 00000000..408643dd --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-queued-training-job.sql @@ -0,0 +1,14 @@ +SELECT + job_id, + model_id, + job_status, + model_name, + major_version, + minor_version, + latest, + deployment_environment, + created_at +FROM model_training_jobs +WHERE job_status = 'queued' +ORDER BY created_at ASC +LIMIT 1; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-synced-agency-ids.sql b/DSL/Resql/global-classifier/POST/get-synced-agency-ids.sql new file mode 100644 index 00000000..39d40700 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-synced-agency-ids.sql @@ -0,0 +1,3 @@ +SELECT agency_id, agency_data_hash +FROM public.integrated_agencies +WHERE sync_status = 'Synced_with_CKB'; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-training-job-status-in-progress.sql b/DSL/Resql/global-classifier/POST/get-training-job-status-in-progress.sql new file mode 100644 index 00000000..2d37f8d5 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-training-job-status-in-progress.sql @@ -0,0 +1,6 @@ +SELECT + EXISTS( + SELECT 1 + FROM model_training_jobs + WHERE job_status = 'training-in-progress' + ) AS has_training_in_progress; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-user-role.sql b/DSL/Resql/global-classifier/POST/get-user-role.sql new file mode 100644 index 00000000..39a51f4e --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-user-role.sql @@ -0,0 +1,10 @@ +SELECT ua.authority_name AS authorities +FROM "user" u + INNER JOIN (SELECT authority_name, user_id + FROM user_authority AS ua + WHERE ua.id IN (SELECT max(id) + FROM user_authority + GROUP BY user_id)) ua ON u.id_code = ua.user_id +WHERE u.id_code = :userIdCode + AND status <> 'deleted' + AND id IN (SELECT max(id) FROM "user" WHERE id_code = :userIdCode) diff --git a/DSL/Resql/global-classifier/POST/get-user-with-roles.sql b/DSL/Resql/global-classifier/POST/get-user-with-roles.sql new file mode 100644 index 00000000..8ef5044c --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-user-with-roles.sql @@ -0,0 +1,15 @@ +SELECT DISTINCT u.login, + u.first_name, + u.last_name, + u.id_code, + u.display_name, + u.csa_title, + u.csa_email, + ua.authority_name AS authorities +FROM "user" u + LEFT JOIN (SELECT authority_name, user_id + FROM user_authority AS ua + WHERE ua.id IN (SELECT max(id) + FROM user_authority + GROUP BY user_id)) ua ON u.id_code = ua.user_id +WHERE login = :login; diff --git a/DSL/Resql/global-classifier/POST/get-user.sql b/DSL/Resql/global-classifier/POST/get-user.sql new file mode 100644 index 00000000..18bef7ff --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-user.sql @@ -0,0 +1,5 @@ +SELECT id_code +FROM "user" +WHERE id_code = :userIdCode + AND status <> 'deleted' + AND id IN (SELECT max(id) FROM "user" WHERE id_code = :userIdCode) \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/get-users-with-roles-by-role.sql b/DSL/Resql/global-classifier/POST/get-users-with-roles-by-role.sql new file mode 100644 index 00000000..50ec5199 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/get-users-with-roles-by-role.sql @@ -0,0 +1,41 @@ +SELECT u.login, + u.first_name, + u.last_name, + u.id_code AS userIdCode, + u.display_name, + u.csa_title, + u.csa_email, + ua.authority_name AS authorities, + CEIL(COUNT(*) OVER() / :page_size::DECIMAL) AS total_pages +FROM "user" u +LEFT JOIN ( + SELECT authority_name, user_id, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id DESC) AS rn + FROM user_authority AS ua + WHERE authority_name && ARRAY [ :roles ]::character varying array + AND ua.id IN ( + SELECT max(id) + FROM user_authority + GROUP BY user_id + ) +) ua ON u.id_code = ua.user_id +WHERE u.status <> 'deleted' + AND array_length(authority_name, 1) > 0 + AND u.id IN ( + SELECT max(id) + FROM "user" + GROUP BY id_code + ) +ORDER BY + CASE WHEN :sorting = 'name asc' THEN u.first_name END ASC, + CASE WHEN :sorting = 'name desc' THEN u.first_name END DESC, + CASE WHEN :sorting = 'idCode asc' THEN u.id_code END ASC, + CASE WHEN :sorting = 'idCode desc' THEN u.id_code END DESC, + CASE WHEN :sorting = 'Role asc' THEN ua.authority_name END ASC, + CASE WHEN :sorting = 'Role desc' THEN ua.authority_name END DESC, + CASE WHEN :sorting = 'displayName asc' THEN u.display_name END ASC, + CASE WHEN :sorting = 'displayName desc' THEN u.display_name END DESC, + CASE WHEN :sorting = 'csaTitle asc' THEN u.csa_title END ASC, + CASE WHEN :sorting = 'csaTitle desc' THEN u.csa_title END DESC, + CASE WHEN :sorting = 'csaEmail asc' THEN u.csa_email END ASC, + CASE WHEN :sorting = 'csaEmail desc' THEN u.csa_email END DESC +OFFSET ((GREATEST(:page, 1) - 1) * :page_size) LIMIT :page_size; diff --git a/DSL/Resql/global-classifier/POST/insert-agency.sql b/DSL/Resql/global-classifier/POST/insert-agency.sql new file mode 100644 index 00000000..de8f4614 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-agency.sql @@ -0,0 +1,7 @@ +INSERT INTO "integrated_agencies" +(agency_id, agency_name, group_key, is_latest, deployment_status, is_enabled, agency_data_hash, enable_allowed, last_model_trained, last_updated_timestamp, last_trained_timestamp, sync_status, created_at) +VALUES +(:agencyId, :agencyName, :groupKey, :isLatest, :deploymentStatus::deployment_status, :isEnabled, :agencyDataHash, :enableAllowed, :lastModelTrained, +CASE WHEN :lastUpdatedTimestamp IS NOT NULL THEN :lastUpdatedTimestamp::timestamp with time zone ELSE NULL END, +CASE WHEN :lastTrainedTimestamp IS NOT NULL THEN :lastTrainedTimestamp::timestamp with time zone ELSE NULL END, +:syncStatus::sync_status, :createdAt::timestamp with time zone); diff --git a/DSL/Resql/global-classifier/POST/insert-data-chunk-metadata .sql b/DSL/Resql/global-classifier/POST/insert-data-chunk-metadata .sql new file mode 100644 index 00000000..769d0a56 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-data-chunk-metadata .sql @@ -0,0 +1,11 @@ +INSERT INTO public.dataset_metadata ( + dataset_id, + chunk_id, + included_agencies, + created_at +) VALUES ( + :datasetId, + :chunkId, + :includedAgencies::jsonb, + NOW() +) \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/insert-data-model-progress-session.sql b/DSL/Resql/global-classifier/POST/insert-data-model-progress-session.sql new file mode 100644 index 00000000..d038409f --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-data-model-progress-session.sql @@ -0,0 +1,17 @@ +INSERT INTO "model_progress_sessions" ( + model_id, + model_name, + major_version, + minor_version, + latest, + progress_percentage, + training_progress_status +) VALUES ( + :model_id, + :model_name, + :major_version, + :minor_version, + :latest, + :progress_percentage, + :training_progress_status::Training_Progress_Status +)RETURNING id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/insert-data-models.sql b/DSL/Resql/global-classifier/POST/insert-data-models.sql new file mode 100644 index 00000000..2a370b7d --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-data-models.sql @@ -0,0 +1,51 @@ +INSERT INTO public.data_models ( + model_group_key, + model_name, + major, + minor, + latest, + deployment_env, + training_status, + base_models, + last_trained, + connected_ds_id, + connected_ds_major_version, + connected_ds_minor_version, + model_status, + model_s3_location, + training_results, + created_timestamp, + updated_timestamp +) VALUES ( + (FLOOR(RANDOM() * 999999)::TEXT || '_' || EXTRACT(EPOCH FROM NOW())::BIGINT::TEXT), + :modelName, + 1, + 0, + true, + :deploymentEnv::deployment_environment, + 'initiating_training'::training_status, + :baseModels::JSONB, + NULL, + :connectedDsId, + :connectedDsMajorVersion, + :connectedDsMinorVersion, + 'active'::model_status, + NULL, + NULL, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP +) +RETURNING + model_id, + model_group_key, + model_name, + major, + minor, + latest, + deployment_env, + training_status, + base_models, + created_timestamp, + connected_ds_id, + connected_ds_major_version, + connected_ds_minor_version; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/insert-dataset-progress-session.sql b/DSL/Resql/global-classifier/POST/insert-dataset-progress-session.sql new file mode 100644 index 00000000..8c88b954 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-dataset-progress-session.sql @@ -0,0 +1,15 @@ +INSERT INTO public.dataset_progress_sessions ( + dataset_id, + major_version, + minor_version, + latest, + progress_percentage, + generation_status +) VALUES ( + :dataset_id, + :major_version, + :minor_version, + :latest, + :progressPercentage, + :generation_status::Generation_Progress_Status +)RETURNING id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/insert-dataset.sql b/DSL/Resql/global-classifier/POST/insert-dataset.sql new file mode 100644 index 00000000..7c86b971 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-dataset.sql @@ -0,0 +1,18 @@ +INSERT INTO public.dataset_versions ( + major, + minor, + created_at, + updated_at, + generation_status, + last_model_trained, + last_trained +) VALUES ( + :major, + :minor, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + :generationStatus, + :lastModelTrained, + CASE WHEN :lastTrained IS NOT NULL THEN :lastTrained::timestamp with time zone ELSE NULL END +) +RETURNING *; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/insert-grouped-datasets.sql b/DSL/Resql/global-classifier/POST/insert-grouped-datasets.sql new file mode 100644 index 00000000..ee072686 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-grouped-datasets.sql @@ -0,0 +1 @@ +SELECT import_csv_dynamic(:filePath); \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/insert-inference-log.sql b/DSL/Resql/global-classifier/POST/insert-inference-log.sql new file mode 100644 index 00000000..fc238075 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-inference-log.sql @@ -0,0 +1,13 @@ +INSERT INTO public.inference_logs ( + chat_id, + url, + model_id, + inference_time_ms, + parsed_output +) VALUES ( + :chatId, + :url, + :modelId, + :inferenceTimeMs, + :parsedOutput::jsonb +) RETURNING inference_id, chat_id, url, model_id, inference_time_ms, parsed_output, created_timestamp; diff --git a/DSL/Resql/global-classifier/POST/insert-testing-inference-log.sql b/DSL/Resql/global-classifier/POST/insert-testing-inference-log.sql new file mode 100644 index 00000000..aa3a51fa --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-testing-inference-log.sql @@ -0,0 +1,11 @@ +INSERT INTO public.testing_inference_logs ( + model_id, + message, + inference_time_ms, + parsed_output +) VALUES ( + :modelId, + :message, + :inferenceTimeMs, + :parsedOutput::jsonb +) RETURNING log_id, model_id, message, inference_time_ms, parsed_output, created_timestamp; diff --git a/DSL/Resql/global-classifier/POST/insert-training-job-to-queue.sql b/DSL/Resql/global-classifier/POST/insert-training-job-to-queue.sql new file mode 100644 index 00000000..0149e31a --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-training-job-to-queue.sql @@ -0,0 +1,30 @@ +INSERT INTO model_training_jobs ( + created_at, + model_id, + job_status, + model_name, + major_version, + minor_version, + latest, + deployment_environment +) VALUES ( + EXTRACT(EPOCH FROM NOW())::INTEGER, + :model_id, + 'queued'::training_job_status, + :model_name, + :major_version, + :minor_version, + :latest, + :deployment_environment +) +RETURNING + job_id, + model_id, + job_status, + created_at, + model_name, + major_version, + minor_version, + latest, + deployment_environment, + 'Training job successfully queued' AS message; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/insert-user-role.sql b/DSL/Resql/global-classifier/POST/insert-user-role.sql new file mode 100644 index 00000000..e2bfe3b4 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-user-role.sql @@ -0,0 +1,2 @@ +INSERT INTO user_authority (user_id, authority_name, created) +VALUES (:userIdCode, ARRAY [ :roles ], :created::timestamp with time zone); \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/insert-user.sql b/DSL/Resql/global-classifier/POST/insert-user.sql new file mode 100644 index 00000000..0fd7c12b --- /dev/null +++ b/DSL/Resql/global-classifier/POST/insert-user.sql @@ -0,0 +1,2 @@ +INSERT INTO "user" (login, first_name, last_name, display_name, password_hash, id_code, status, created, csa_title, csa_email) +VALUES (:userIdCode, :firstName, :lastName, :displayName, :displayName, :userIdCode, (:status)::user_status, :created::timestamp with time zone, :csaTitle, :csaEmail); diff --git a/DSL/Resql/global-classifier/POST/mock-get-agencies-from-centops.sql b/DSL/Resql/global-classifier/POST/mock-get-agencies-from-centops.sql new file mode 100644 index 00000000..c275ef70 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/mock-get-agencies-from-centops.sql @@ -0,0 +1,7 @@ +SELECT + mc.agency_id, + mc.agency_name + FROM + public.mock_centops mc + ORDER BY + mc.created_at DESC; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/mock-get-data-from-ckb.sql b/DSL/Resql/global-classifier/POST/mock-get-data-from-ckb.sql new file mode 100644 index 00000000..30b45be3 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/mock-get-data-from-ckb.sql @@ -0,0 +1,17 @@ +WITH parsed_ids AS ( + SELECT unnest(string_to_array(:agencyIds, ' ')) AS agency_id +) +SELECT + mock_ckb.agency_id, + ia.agency_name, + mock_ckb.agency_data_hash, + mock_ckb.data_url +FROM + public.mock_ckb +JOIN + parsed_ids ON mock_ckb.agency_id = parsed_ids.agency_id +JOIN + public.integrated_agencies ia ON mock_ckb.agency_id = ia.agency_id +WHERE + mock_ckb.agency_data_hash IS NOT NULL + AND mock_ckb.data_url IS NOT NULL; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-agency-data.sql b/DSL/Resql/global-classifier/POST/update-agency-data.sql new file mode 100644 index 00000000..0e45fb97 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-agency-data.sql @@ -0,0 +1,7 @@ +UPDATE public.integrated_agencies + SET + sync_status = :syncStatus::sync_status, + agency_data_hash = :agencyDataHash, + last_updated_timestamp = CURRENT_TIMESTAMP + WHERE + agency_id = :agencyId; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-agency-disabled.sql b/DSL/Resql/global-classifier/POST/update-agency-disabled.sql new file mode 100644 index 00000000..b80b8488 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-agency-disabled.sql @@ -0,0 +1,6 @@ +UPDATE public.integrated_agencies +SET + is_enabled = :isEnabled +WHERE + agency_id = :agencyId +RETURNING agency_id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-agency-enabled.sql b/DSL/Resql/global-classifier/POST/update-agency-enabled.sql new file mode 100644 index 00000000..6a540248 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-agency-enabled.sql @@ -0,0 +1,7 @@ +UPDATE public.integrated_agencies +SET + is_enabled = :isEnabled +WHERE + agency_id = :agencyId + AND sync_status IN ('Synced_with_CKB', 'Resync_needed_with_CKB') +RETURNING agency_id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-agency-sync-status.sql b/DSL/Resql/global-classifier/POST/update-agency-sync-status.sql new file mode 100644 index 00000000..72cf8b5a --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-agency-sync-status.sql @@ -0,0 +1,7 @@ +UPDATE public.integrated_agencies + SET + sync_status = :syncStatus::sync_status, + is_enabled = :isEnabled, + last_updated_timestamp = CURRENT_TIMESTAMP + WHERE + agency_id = :agencyId; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-agency.sql b/DSL/Resql/global-classifier/POST/update-agency.sql new file mode 100644 index 00000000..5e51a44a --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-agency.sql @@ -0,0 +1,29 @@ +UPDATE integrated_agencies +SET + agency_name = COALESCE(:agencyName, agency_name), + group_key = COALESCE(:groupKey, group_key), + is_latest = COALESCE(:isLatest, is_latest), + deployment_status = COALESCE(:deploymentStatus::deployment_status, deployment_status), + is_enabled = COALESCE(:isEnabled, is_enabled), + agency_data_hash = COALESCE(:agencyDataHash, agency_data_hash), + enable_allowed = COALESCE(:enableAllowed, enable_allowed), + last_model_trained = COALESCE(:lastModelTrained, last_model_trained), + last_updated_timestamp = CURRENT_TIMESTAMP, + last_trained_timestamp = COALESCE(:lastTrainedTimestamp::TIMESTAMP WITH TIME ZONE, last_trained_timestamp), + sync_status = COALESCE(:syncStatus::sync_status, sync_status) +WHERE + agency_id = :agencyId +RETURNING + agency_id, + agency_name, + group_key, + is_latest, + deployment_status, + is_enabled, + agency_data_hash, + enable_allowed, + last_model_trained, + last_updated_timestamp, + last_trained_timestamp, + sync_status, + created_at; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-data-generation-status.sql b/DSL/Resql/global-classifier/POST/update-data-generation-status.sql new file mode 100644 index 00000000..a2066e2b --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-data-generation-status.sql @@ -0,0 +1,3 @@ +UPDATE public.dataset_versions +SET generation_status = :generationStatus +WHERE id = :datasetId::bigint; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-data-model-progress-session.sql b/DSL/Resql/global-classifier/POST/update-data-model-progress-session.sql new file mode 100644 index 00000000..13b296f3 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-data-model-progress-session.sql @@ -0,0 +1,7 @@ +UPDATE model_progress_sessions +SET + training_progress_status = :training_progress_status::Training_Progress_Status, + training_message = :training_message, + progress_percentage = :progress_percentage, + process_complete = :process_complete +WHERE id = :id; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-datamodel-deployment-env.sql b/DSL/Resql/global-classifier/POST/update-datamodel-deployment-env.sql new file mode 100644 index 00000000..902f55bd --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-datamodel-deployment-env.sql @@ -0,0 +1,14 @@ +UPDATE public.data_models +SET + deployment_env = 'undeployed'::deployment_environment, + updated_timestamp = CURRENT_TIMESTAMP +WHERE + deployment_env = 'production'::deployment_environment +RETURNING + model_id, + model_group_key, + model_name, + major, + minor, + deployment_env, + updated_timestamp; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-datamodels-latest-state.sql b/DSL/Resql/global-classifier/POST/update-datamodels-latest-state.sql new file mode 100644 index 00000000..a39f93ed --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-datamodels-latest-state.sql @@ -0,0 +1,16 @@ +UPDATE public.data_models +SET + latest = false, + updated_timestamp = CURRENT_TIMESTAMP +WHERE + model_id = :modelId +RETURNING + model_id, + model_group_key, + model_name, + major, + minor, + latest, + deployment_env, + training_status, + updated_timestamp; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-datamodels-training-status.sql b/DSL/Resql/global-classifier/POST/update-datamodels-training-status.sql new file mode 100644 index 00000000..dbf8d4f8 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-datamodels-training-status.sql @@ -0,0 +1,18 @@ +UPDATE public.data_models +SET + training_status = :trainingStatus::training_status, + training_results = :trainingResults::jsonb, + model_s3_location = :modelS3Location::text, + last_trained = CURRENT_TIMESTAMP, + updated_timestamp = CURRENT_TIMESTAMP +WHERE + model_id = :modelId +RETURNING + model_id, + model_group_key, + model_name, + major, + minor, + training_status, + training_results, + updated_timestamp; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-dataset-progress-session.sql b/DSL/Resql/global-classifier/POST/update-dataset-progress-session.sql new file mode 100644 index 00000000..1b760738 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-dataset-progress-session.sql @@ -0,0 +1,7 @@ +UPDATE dataset_progress_sessions +SET + generation_status = :generation_status::Generation_Progress_Status, + generation_message = :generation_message, + progress_percentage = :progress_percentage, + process_complete = :process_complete +WHERE id = :id::bigint; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-dataset.sql b/DSL/Resql/global-classifier/POST/update-dataset.sql new file mode 100644 index 00000000..15332cca --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-dataset.sql @@ -0,0 +1,8 @@ +UPDATE public.datasets +SET + agency_name = :agencyName, + agency_id = :agencyId, + data_item = :dataItem +WHERE + item_id = :itemId +RETURNING *; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-datasets-connected-models.sql b/DSL/Resql/global-classifier/POST/update-datasets-connected-models.sql new file mode 100644 index 00000000..80939209 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-datasets-connected-models.sql @@ -0,0 +1,16 @@ +UPDATE public.dataset_versions +SET + connected_models = CASE + WHEN connected_models IS NULL THEN + jsonb_build_array(:modelId) + WHEN NOT (connected_models @> jsonb_build_array(:modelId)) THEN + connected_models || jsonb_build_array(:modelId) + ELSE + connected_models + END, + updated_at = CURRENT_TIMESTAMP +WHERE + id = :datasetId +RETURNING + id, + connected_models; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-training-job-status.sql b/DSL/Resql/global-classifier/POST/update-training-job-status.sql new file mode 100644 index 00000000..e2aa9eb4 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-training-job-status.sql @@ -0,0 +1,3 @@ +UPDATE public.model_training_jobs +SET job_status = :jobStatus::training_job_status +WHERE job_id = :jobId; \ No newline at end of file diff --git a/DSL/Resql/global-classifier/POST/update-user.sql b/DSL/Resql/global-classifier/POST/update-user.sql new file mode 100644 index 00000000..688e8df7 --- /dev/null +++ b/DSL/Resql/global-classifier/POST/update-user.sql @@ -0,0 +1,16 @@ +INSERT INTO "user" (id_code, login, password_hash, first_name, last_name, display_name, status, created, csa_title, csa_email) +SELECT + :userIdCode, + login, + password_hash, + :firstName, + :lastName, + :displayName, + :status::user_status, + :created::timestamp with time zone, + :csaTitle, + :csaEmail +FROM "user" +WHERE id = ( + SELECT MAX(id) FROM "user" WHERE id_code = :userIdCode +); diff --git a/DSL/Ruuter.private/global-classifier/GET/.guard b/DSL/Ruuter.private/global-classifier/GET/.guard new file mode 100644 index 00000000..60ad9726 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/.guard @@ -0,0 +1,33 @@ +check_for_cookie: + switch: + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +authenticate: + template: "[#GLOBAL_CLASSIFIER_PROJECT_LAYER]/check-user-authority" + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + next: log_cookie + +log_cookie: + log: "Cookie received: ${incoming.headers.cookie}" + next: check_authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: + return: "success" + status: 200 + next: end + +guard_fail: + return: "unauthorized" + status: 401 + next: end diff --git a/DSL/Ruuter.private/global-classifier/GET/accounts/logout.yml b/DSL/Ruuter.private/global-classifier/GET/accounts/logout.yml new file mode 100644 index 00000000..8284c80b --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/accounts/logout.yml @@ -0,0 +1,63 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'LOGOUT'" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + headers: + - field: cookie + type: string + description: "Cookie field" + +get_user_info: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TIM]/jwt/custom-jwt-userinfo" + contentType: plaintext + headers: + cookie: ${incoming.headers.cookie} + plaintext: "customJwtCookie" + result: res + next: check_user_info_response + +check_user_info_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: blacklistCustomJwt + next: return_bad_request + +blacklistCustomJwt: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TIM]/jwt/custom-jwt-blacklist" + contentType: plaintext + headers: + cookie: ${incoming.headers.cookie} + plaintext: "customJwtCookie" + result: blacklist_res + next: assign_cookie + +assign_cookie: + assign: + setCookie: + customJwtCookie: null + Domain: "[#DOMAIN]" + Max-Age: 0 + Secure: true + HttpOnly: true + SameSite: "Lax" + next: return_result + +return_result: + headers: + Set-Cookie: ${setCookie} + return: "Logged Out Successfully" + next: end + +return_bad_request: + return: "error: bad request" + status: 400 + next: end diff --git a/DSL/Ruuter.private/global-classifier/GET/accounts/user-role.yml b/DSL/Ruuter.private/global-classifier/GET/accounts/user-role.yml new file mode 100644 index 00000000..eb0ea7f2 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/accounts/user-role.yml @@ -0,0 +1,53 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'USER-ROLE'" + method: get + accepts: json + returns: json + namespace: global-classifier + allowlist: + headers: + - field: cookie + type: string + description: "Cookie field" + +get_user_info: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TIM]/jwt/custom-jwt-userinfo" + contentType: plaintext + headers: + cookie: ${incoming.headers.cookie} + plaintext: + "customJwtCookie" + result: res + next: check_user_info_response + +check_user_info_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assignIdCode + next: returnNotFound + +assignIdCode: + assign: + idCode: ${res.response.body.idCode} + next: getUserRole + +getUserRole: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-user-role" + body: + userIdCode: ${idCode} + result: roles_res + next: returnSuccess + +returnSuccess: + return: ${roles_res.response.body?.[0]?.authorities ?? []} + next: end + +returnNotFound: + return: "error: not found" + next: end diff --git a/DSL/Ruuter.private/global-classifier/GET/agencies/all.yml b/DSL/Ruuter.private/global-classifier/GET/agencies/all.yml new file mode 100644 index 00000000..2a2678dd --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/agencies/all.yml @@ -0,0 +1,19 @@ +declaration: + call: declare + version: 0.1 + description: "Get list of agencyId and agencyName from integrated_agencies" + method: get + accepts: json + returns: json + namespace: global-classifier + +getAllAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-all-agencies" + result: agencies_res + next: return_result + +return_result: + return: ${agencies_res.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/GET/agencies/list.yml b/DSL/Ruuter.private/global-classifier/GET/agencies/list.yml new file mode 100644 index 00000000..43f1ad02 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/agencies/list.yml @@ -0,0 +1,50 @@ +declaration: + call: declare + version: 0.1 + description: "Get all agencies from the integrated_agencies table with sorting and filtering" + method: get + accepts: json + returns: json + namespace: global-classifier + allowlist: + params: + - field: page + type: number + description: "Query parameter 'page' for pagination" + - field: pageSize + type: number + description: "Query parameter 'pageSize' for pagination" + - field: agencyName + type: string + description: "Query parameter 'agencyName' for filtering agencies by name" + - field: sortBy + type: string + description: "Query parameter 'sortBy' for sorting (agency_name, created_at, last_updated_timestamp)" + - field: sortType + type: string + description: "Query parameter 'sortType' for sort direction (asc, desc)" + +extractRequestData: + assign: + page: ${Number(incoming.params.page) || 1} + pageSize: ${Number(incoming.params.pageSize) || 10} + agencyName: ${incoming.params.agencyName || 'all'} + sortBy: ${incoming.params.sortBy || ''} + sortType: ${incoming.params.sortType || 'desc'} + +getAllAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-agencies" + body: + page: ${page} + page_size: ${pageSize} + agency_name: ${agencyName} + sort_by: ${sortBy} + sort_type: ${sortType} + result: agencies_res + next: return_result + +return_result: + return: ${agencies_res.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/GET/auth/jwt/extend.yml b/DSL/Ruuter.private/global-classifier/GET/auth/jwt/extend.yml new file mode 100644 index 00000000..c98c8cd9 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/auth/jwt/extend.yml @@ -0,0 +1,41 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'EXTEND'" + method: get + accepts: json + returns: json + namespace: global-classifier + allowlist: + headers: + - field: cookie + type: string + description: "Cookie field" + +extend_cookie: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TIM]/jwt/custom-jwt-extend" + contentType: plaintext + headers: + cookie: ${incoming.headers.cookie} + plaintext: + "customJwtCookie" + result: cookie_result + next: assign_cookie + +assign_cookie: + assign: + setCookie: + customJwtCookie: ${cookie_result.response.body.token} + Domain: "[#DOMAIN]" + Secure: true + HttpOnly: true + SameSite: "Lax" + next: return_value + +return_value: + headers: + Set-Cookie: ${setCookie} + return: ${cookie_result.response.body.token} + next: end diff --git a/DSL/Ruuter.private/global-classifier/GET/auth/jwt/userinfo.yml b/DSL/Ruuter.private/global-classifier/GET/auth/jwt/userinfo.yml new file mode 100644 index 00000000..ee4b5f3f --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/auth/jwt/userinfo.yml @@ -0,0 +1,27 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'USERINFO'" + method: get + accepts: json + returns: json + namespace: global-classifier + allowlist: + headers: + - field: cookie + type: string + description: "Cookie field" + +get_user_info: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TIM]/jwt/custom-jwt-userinfo" + contentType: plaintext + headers: + cookie: ${incoming.headers.cookie} + plaintext: + "customJwtCookie" + result: res + +return_result: + return: ${res.response.body} diff --git a/DSL/Ruuter.private/global-classifier/GET/datamodels/configs/environments.yml b/DSL/Ruuter.private/global-classifier/GET/datamodels/configs/environments.yml new file mode 100644 index 00000000..d0b8cd9b --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datamodels/configs/environments.yml @@ -0,0 +1,31 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'OPTIONS'" + method: get + accepts: json + returns: json + namespace: classifier + +get_data_model_options: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-data-model-options" + result: res_options + next: check_status + +check_status: + switch: + - condition: ${200 <= res_options.response.statusCodeValue && res_options.response.statusCodeValue < 300} + next: return_ok + next: return_bad_request + +return_ok: + status: 200 + return: ${res_options.response.body} + next: end + +return_bad_request: + status: 400 + return: "Bad Request" + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/GET/datamodels/list.yml b/DSL/Ruuter.private/global-classifier/GET/datamodels/list.yml new file mode 100644 index 00000000..fa4a5c85 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datamodels/list.yml @@ -0,0 +1,65 @@ +declaration: + call: declare + version: 0.1 + description: "Get all data models from the data_models table with sorting and filtering" + method: get + accepts: json + returns: json + namespace: global-classifier + allowlist: + params: + - field: trainingStatus + type: string + description: "Filter by training status" + - field: modelStatus + type: string + description: "Filter by model status" + - field: deploymentEnvironment + type: string + description: "Filter by deploymentEnvironment (optional, if your table supports it)" + - field: sortBy + type: string + description: "Sort by createdAt, lastTrained, or modelName" + - field: sortType + type: string + description: "Sort direction (asc, desc)" + - field: page + type: number + description: "Page number for pagination" + - field: pageSize + type: number + description: "Page size for pagination" + +extractRequestData: + assign: + trainingStatus: ${incoming.params.trainingStatus || 'all'} + modelStatus: ${incoming.params.modelStatus || 'all'} + deploymentEnvironment: ${incoming.params.deploymentEnvironment || 'all'} + sortBy: ${incoming.params.sortBy || 'createdAt'} + sortType: ${incoming.params.sortType || 'desc'} + page: ${Number(incoming.params.page) || 1} + pageSize: ${Number(incoming.params.pageSize) || 10} + +getAllDataModels: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-data-models" + body: + training_status: ${trainingStatus} + model_status: ${modelStatus} + deployment_env: ${deploymentEnvironment} + sort_by: ${sortBy} + sort_type: ${sortType} + page: ${page} + page_size: ${pageSize} + result: data_models_res + next: return_result + +assign_result: + assign: + dataModels: ${data_models_res.response.body} + next: return_result + +return_result: + return: ${data_models_res.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/GET/datamodels/metadata.yml b/DSL/Ruuter.private/global-classifier/GET/datamodels/metadata.yml new file mode 100644 index 00000000..152ffead --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datamodels/metadata.yml @@ -0,0 +1,30 @@ +declaration: + call: declare + version: 0.1 + description: "Get metadata for a specific data model by modelId" + method: get + accepts: json + returns: json + namespace: global-classifier + allowlist: + params: + - field: modelId + type: number + description: "ID of the data model" + +extractRequestData: + assign: + modelId: ${Number(incoming.params.modelId)} + +getDataModelMetadata: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-data-model-by-id" + body: + model_id: ${modelId} + result: data_model_metadata + next: return_result + +return_result: + return: ${data_model_metadata.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/GET/datamodels/production-model.yml b/DSL/Ruuter.private/global-classifier/GET/datamodels/production-model.yml new file mode 100644 index 00000000..13eb90a4 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datamodels/production-model.yml @@ -0,0 +1,20 @@ +declaration: + call: declare + version: 0.1 + description: "Get the current production data model" + method: get + accepts: json + returns: json + namespace: global-classifier + +getProductionModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-production-data-model" + body: {} + result: productionModelRes + next: return_result + +return_result: + return: ${productionModelRes.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/GET/datamodels/progress.yml b/DSL/Ruuter.private/global-classifier/GET/datamodels/progress.yml new file mode 100644 index 00000000..82953c85 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datamodels/progress.yml @@ -0,0 +1,48 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'PROGRESS'" + method: get + accepts: json + returns: json + namespace: classifier + +get_data_model_progress_sessions: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-data-model-progress-sessions" + result: res_sessions + next: check_status + +check_status: + switch: + - condition: ${200 <= res_sessions.response.statusCodeValue && res_sessions.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${res_sessions.response.body}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/DSL/Ruuter.private/global-classifier/GET/datamodels/versions.yml b/DSL/Ruuter.private/global-classifier/GET/datamodels/versions.yml new file mode 100644 index 00000000..95241408 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datamodels/versions.yml @@ -0,0 +1,66 @@ +declaration: + call: declare + version: 0.1 + description: "Get datamodel versions as [id, model_name version] array" + method: get + accepts: json + returns: json + namespace: global-classifier + +getAllDatamodelVersions: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-all-datamodel-versions" + result: datamodels_res + next: checkDatabaseResult + +checkDatabaseResult: + switch: + - condition: ${!datamodels_res || !datamodels_res.response || !datamodels_res.response.body} + next: return_database_error + - condition: ${datamodels_res.response.body.length === 0} + next: return_empty_result + next: format_versions + +format_versions: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/format_datamodel_versions" + headers: + type: json + body: + datamodels: ${datamodels_res.response.body} + result: formatted_versions + next: checkFormatResult + +checkFormatResult: + switch: + - condition: ${!formatted_versions || !formatted_versions.response} + next: return_format_error + next: return_result + +return_result: + return: ${formatted_versions.response.body} + status: 200 + next: end + +return_empty_result: + return: ${[]} + status: 200 + next: end + +return_database_error: + assign: + error_response: + error: "Failed to retrieve datamodel versions" + return: ${error_response} + status: 500 + next: end + +return_format_error: + assign: + error_response: + error: "Failed to format datamodel versions" + return: ${error_response} + status: 500 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/GET/datasets/data.yml b/DSL/Ruuter.private/global-classifier/GET/datasets/data.yml new file mode 100644 index 00000000..3df80785 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datasets/data.yml @@ -0,0 +1,45 @@ +declaration: + call: declare + version: 0.1 + description: "Get all datasets from the datasets table with pagination and optional client filtering" + method: get + accepts: json + returns: json + namespace: global-classifier + allowlist: + params: + - field: pageNum + type: number + description: "Page number for pagination" + - field: pageSize + type: number + description: "Page size for pagination" + - field: datasetVersionId + type: number + description: "Dataset version ID" + - field: clientId + type: string + description: "Client ID to filter results (optional)" + +extractRequestData: + assign: + page: ${Number(incoming.params.pageNum) || 1} + pageSize: ${Number(incoming.params.pageSize) || 10} + datasetVersionId: ${Number(incoming.params.datasetVersionId) || 0} + clientId: ${incoming.params.clientId || 'all'} + +getAllDatasets: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-datasets-data" + body: + page: ${page} + page_size: ${pageSize} + dataset_version_id: ${datasetVersionId} + client_id: ${clientId} + result: datasets_res + next: return_result + +return_result: + return: ${datasets_res.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/GET/datasets/list.yml b/DSL/Ruuter.private/global-classifier/GET/datasets/list.yml new file mode 100644 index 00000000..3790fd8d --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datasets/list.yml @@ -0,0 +1,50 @@ +declaration: + call: declare + version: 0.1 + description: "Get all datasets from the datasets table with sorting and filtering" + method: get + accepts: json + returns: json + namespace: global-classifier + allowlist: + params: + - field: page + type: number + description: "Query parameter 'page' for pagination" + - field: pageSize + type: number + description: "Query parameter 'pageSize' for pagination" + - field: generationStatus + type: string + description: "Query parameter 'generationStatus' for filtering datasets by status" + - field: sortBy + type: string + description: "Query parameter 'sortBy' for sorting (created_at, major, minor, generation_status)" + - field: sortType + type: string + description: "Query parameter 'sortType' for sort direction (asc, desc)" + +extractRequestData: + assign: + page: ${Number(incoming.params.page) || 1} + pageSize: ${Number(incoming.params.pageSize) || 10} + generationStatus: ${incoming.params.generationStatus || 'all'} + sortBy: ${incoming.params.sortBy || ''} + sortType: ${incoming.params.sortType || 'desc'} + +getAllDatasets: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-datasets" + body: + page: ${page} + page_size: ${pageSize} + generation_status: ${generationStatus} + sort_by: ${sortBy} + sort_type: ${sortType} + result: datasets_res + next: return_result + +return_result: + return: ${datasets_res.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/GET/datasets/metadata.yml b/DSL/Ruuter.private/global-classifier/GET/datasets/metadata.yml new file mode 100644 index 00000000..85187620 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datasets/metadata.yml @@ -0,0 +1,85 @@ +declaration: + call: declare + version: 0.1 + description: "Get dataset metadata by id with connected models and total data count" + method: get + accepts: json + returns: json + namespace: global-classifier + allowlist: + params: + - field: datasetId + type: number + description: "Dataset id" + +extractRequestData: + assign: + datasetId: ${Number(incoming.params.datasetId)} + next: getDatasetMetadata + +getDatasetMetadata: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-dataset-by-id" + body: + id: ${datasetId} + result: dataset_res + next: checkDatasetResult + +checkDatasetResult: + switch: + - condition: ${!dataset_res || !dataset_res.response.body || dataset_res.response.body.length === 0} + next: return_not_found + next: getDatasetTotalCount + +getDatasetTotalCount: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-dataset-total-count" + body: + datasetVersionId: ${datasetId} + result: total_count_res + next: getConnectedModels + +getConnectedModels: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-datasets-connected-models" + body: + datasetId: ${datasetId} + result: connected_models_res + next: formatConnectedModels + +formatConnectedModels: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/format_connected_models" + headers: + type: json + body: + connectedModels: ${connected_models_res.response.body} + result: formatted_models_res + next: buildFinalResponse + +buildFinalResponse: + assign: + dataset: ${dataset_res.response.body[0]} + totalCount: ${total_count_res.response.body[0]?.totalCount || 0} + finalResponse: + response: + - id: ${dataset.id} + major: ${dataset.major} + minor: ${dataset.minor} + totalDataCount: ${totalCount} + connectedModels: ${formatted_models_res.response.body} + next: return_result + +return_not_found: + return: '{"error": "Dataset not found"}' + status: 404 + next: end + +return_result: + return: ${finalResponse} + status: 200 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/GET/datasets/overview.yml b/DSL/Ruuter.private/global-classifier/GET/datasets/overview.yml new file mode 100644 index 00000000..91cbd460 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datasets/overview.yml @@ -0,0 +1,124 @@ +declaration: + call: declare + version: 0.1 + description: "Get chunk_ids from dataset_metadata where included_agencies contains the given agencyId" + method: get + accepts: json + returns: json + namespace: global-classifier + allowlist: + params: + - field: agencyId + type: string + description: "Agency ID to search for in included_agencies" + - field: pageNum + type: string + description: "Page number for pagination" + - field: datasetId + type: string + description: "datasetId to filter chunks by dataset" + +extractRequestData: + assign: + agencyId: ${incoming.params.agencyId} + pageNum: ${incoming.params.pageNum} + datasetId: ${incoming.params.datasetId} + +checkFilter: + switch: + - condition: ${agencyId === "all"} + next: download_single_chunk_data + next: getChunksAndAgencies + +getAllChunks: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/get_all_chunks" + headers: + type: json + body: + pageNum: ${pageNum} + datasetId: ${datasetId} + result: dataChunksResult + next: return_chunk_data + +getChunksAndAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-chunks-and-agencies" + body: + datasetId: ${datasetId} + result: chunksResult + next: getPaginatedChunkIds + +getPaginatedChunkIds: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/get_paginated_chunk_ids" + headers: + type: json + body: + chunks: ${chunksResult.response.body} + agencyId: ${agencyId} + pageNum: ${pageNum} + result: paginatedChunksResult + next: download_agency_chunks_data + +download_single_chunk_data: + call: http.post + args: + url: "http://dataset-file-handler:8000/download-chunk" + body: + datasetId: ${datasetId} + pageNum: ${pageNum} + result: singleChunkResult + next: processSingleChunkResult + +processSingleChunkResult: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/get_single_chunk_data" + headers: + type: json + body: + chunkData: ${singleChunkResult.response.body.chunk_data.data} + result: chunkDataResult + next: return_single_chunk_response + +download_agency_chunks_data: + call: http.post + args: + url: "http://dataset-file-handler:8000/download-multiple-chunks" + body: + datasetId: ${datasetId} + chunkIds: ${paginatedChunksResult.response.body.chunks} + result: aggregatedDataResult + next: filterChunkDataByAgency + +filterChunkDataByAgency: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/get_filtered_chunks" + headers: + type: json + body: + aggregatedData: ${aggregatedDataResult.response.body.aggregated_data} + startIndex: ${paginatedChunksResult.response.body.startIndex} + agencyId: ${agencyId} + result: filteredChunkDataResult + next: return_filtered_chunk_response + +return_chunk_data: + return: ${dataChunksResult.response.body} + status: 200 + next: end + +return_single_chunk_response: + return: ${chunkDataResult.response.body} + status: 200 + next: end + +return_filtered_chunk_response: + return: ${filteredChunkDataResult.response.body} + status: 200 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/GET/datasets/progress.yml b/DSL/Ruuter.private/global-classifier/GET/datasets/progress.yml new file mode 100644 index 00000000..0ba9b96a --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datasets/progress.yml @@ -0,0 +1,48 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'PROGRESS'" + method: get + accepts: json + returns: json + namespace: classifier + +get_dataset_progress_sessions: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-dataset-progress-sessions" + result: res_sessions + next: check_status + +check_status: + switch: + - condition: ${200 <= res_sessions.response.statusCodeValue && res_sessions.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + data: '${res_sessions.response.body}' + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + data: '${[]}' + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/DSL/Ruuter.private/global-classifier/GET/datasets/versions.yml b/DSL/Ruuter.private/global-classifier/GET/datasets/versions.yml new file mode 100644 index 00000000..c6741634 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/GET/datasets/versions.yml @@ -0,0 +1,31 @@ +declaration: + call: declare + version: 0.1 + description: "Get dataset versions as [id, version] array" + method: get + accepts: json + returns: json + namespace: global-classifier + +getAllDatasetVersions: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-all-dataset-versions" + result: datasets_res + next: format_versions + +format_versions: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/format_dataset_versions" + headers: + type: json + body: + datasets: ${datasets_res.response.body} + result: formatted_versions + next: return_result + +return_result: + return: ${formatted_versions.response.body} + status: 200 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/.guard b/DSL/Ruuter.private/global-classifier/POST/.guard new file mode 100644 index 00000000..9d21179b --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/.guard @@ -0,0 +1,28 @@ +check_for_cookie: + switch: + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +authenticate: + template: "[#GLOBAL_CLASSIFIER_PROJECT_LAYER]/check-user-authority" + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: + return: "success" + status: 200 + next: end + +guard_fail: + return: "unauthorized" + status: 401 + next: end diff --git a/DSL/Ruuter.private/global-classifier/POST/accounts/.guard b/DSL/Ruuter.private/global-classifier/POST/accounts/.guard new file mode 100644 index 00000000..be3aa511 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/accounts/.guard @@ -0,0 +1,28 @@ +check_for_cookie: + switch: + - condition: ${incoming.headers == null || incoming.headers.cookie == null} + next: guard_fail + next: authenticate + +authenticate: + template: "[#GLOBAL_CLASSIFIER_PROJECT_LAYER]/check-user-authority-admin" + requestType: templates + headers: + cookie: ${incoming.headers.cookie} + result: authority_result + +check_authority_result: + switch: + - condition: ${authority_result !== "false"} + next: guard_success + next: guard_fail + +guard_success: + return: "success" + status: 200 + next: end + +guard_fail: + return: "unauthorized" + status: 401 + next: end diff --git a/DSL/Ruuter.private/global-classifier/POST/accounts/add.yml b/DSL/Ruuter.private/global-classifier/POST/accounts/add.yml new file mode 100644 index 00000000..1959c935 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/accounts/add.yml @@ -0,0 +1,89 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'ADD'" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: csaTitle + type: string + description: "Body field 'csaTitle'" + - field: csa_email + type: string + description: "Body field 'csa_email'" + - field: firstName + type: string + description: "Body field 'firstName'" + - field: lastName + type: string + description: "Body field 'lastName'" + - field: roles + type: array + description: "Body field 'roles'" + - field: userIdCode + type: string + description: "Body field 'userIdCode'" + +extractRequestData: + assign: + firstName: ${incoming.body.firstName} + lastName: ${incoming.body.lastName} + userIdCode: ${incoming.body.userIdCode} + displayName: ${incoming.body.firstName} + csaTitle: ${incoming.body.csaTitle} + csa_email: ${incoming.body.csa_email} + roles: ${incoming.body.roles} + +getUser: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-user" + body: + userIdCode: ${userIdCode} + result: res + next: checkIfUserExists + +checkIfUserExists: + switch: + - condition: "${res.response.body.length > 0}" + next: return_exists + next: addUser + +addUser: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-user" + body: + created: ${new Date().toISOString()} + status: "active" + firstName: ${firstName} + lastName: ${lastName} + userIdCode: ${userIdCode} + displayName: ${displayName} + csaTitle: ${csaTitle} + csaEmail: ${csa_email} + result: add_user_res + next: addRoles + +addRoles: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-user-role" + body: + userIdCode: ${userIdCode} + roles: ${roles} + created: ${new Date().toISOString()} + result: add_roles_res + next: return_result + +return_result: + return: "User added successfully" + next: end + +return_exists: + return: "error: user already exists" + status: 400 + next: end diff --git a/DSL/Ruuter.private/global-classifier/POST/accounts/delete.yml b/DSL/Ruuter.private/global-classifier/POST/accounts/delete.yml new file mode 100644 index 00000000..e1579062 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/accounts/delete.yml @@ -0,0 +1,29 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'DELETE'" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: userIdCode + type: string + description: "Body field 'userIdCode'" + +extractRequestData: + assign: + userId: ${incoming.body.userIdCode} + +setConfigurationValue: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/delete-user" + body: + userIdCode: ${userId} + created: ${new Date().toISOString()} + result: res + +return_result: + return: ${res.response.body} diff --git a/DSL/Ruuter.private/global-classifier/POST/accounts/edit.yml b/DSL/Ruuter.private/global-classifier/POST/accounts/edit.yml new file mode 100644 index 00000000..455a5ab1 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/accounts/edit.yml @@ -0,0 +1,94 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'EDIT'" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: csaTitle + type: string + description: "Body field 'csaTitle'" + - field: csa_email + type: string + description: "Body field 'csa_email'" + - field: displayName + type: string + description: "Body field 'displayName'" + - field: firstName + type: string + description: "Body field 'firstName'" + - field: lastName + type: string + description: "Body field 'lastName'" + - field: roles + type: array + description: "Body field 'roles'" + - field: userIdCode + type: string + description: "Body field 'userIdCode'" + +extractRequestData: + assign: + firstName: ${incoming.body.firstName} + lastName: ${incoming.body.lastName} + userIdCode: ${incoming.body.userIdCode} + displayName: ${incoming.body.displayName} + csaTitle: ${incoming.body.csaTitle} + csa_email: ${incoming.body.csa_email} + roles: ${incoming.body.roles} + +getUser: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-user" + body: + userIdCode: ${userIdCode} + result: res + next: checkIfUserExists + +checkIfUserExists: + switch: + - condition: "${res.response.body.length > 0}" + next: updateUser + next: return_not_exists + +updateUser: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-user" + body: + created: ${new Date().toISOString()} + status: "active" + firstName: ${firstName} + lastName: ${lastName} + userIdCode: ${userIdCode} + displayName: ${displayName} + csaTitle: ${csaTitle} + csaEmail: ${csa_email} + result: add_user_res + next: updateRoles + +updateRoles: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-user-role" + body: + userIdCode: ${userIdCode} + roles: ${roles} + created: ${new Date().toISOString()} + result: add_roles_res + next: return_result + +return_result: + return: "User updated successfully" + status: 200 + next: end + +return_not_exists: + return: "error: user does not exist" + status: 400 + next: end + diff --git a/DSL/Ruuter.private/global-classifier/POST/accounts/exists.yml b/DSL/Ruuter.private/global-classifier/POST/accounts/exists.yml new file mode 100644 index 00000000..5998db49 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/accounts/exists.yml @@ -0,0 +1,40 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'EXISTS'" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: userIdCode + type: string + description: "Body field 'userIdCode'" + +extractRequestData: + assign: + userId: ${incoming.body.userIdCode} + +getUser: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-user" + body: + userIdCode: ${userId} + result: res + next: checkIfUserExists + +checkIfUserExists: + switch: + - condition: "${res.response.body.length > 0}" + next: return_exists + next: return_not_exists + +return_exists: + return: "true" + next: end + +return_not_exists: + return: "false" + next: end diff --git a/DSL/Ruuter.private/global-classifier/POST/accounts/users.yml b/DSL/Ruuter.private/global-classifier/POST/accounts/users.yml new file mode 100644 index 00000000..d9c8960f --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/accounts/users.yml @@ -0,0 +1,39 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'USERS'" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: page + type: number + description: "Body field 'page'" + - field: page_size + type: number + description: "Body field 'page_size'" + - field: sorting + type: string + description: "Body field 'sorting'" + +getUsers: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-users-with-roles-by-role" + body: + page: ${incoming.body.page} + page_size: ${incoming.body.page_size} + sorting: ${incoming.body.sorting} + roles: + [ + "ROLE_ADMINISTRATOR", + "ROLE_MODEL_TRAINER" + ] + result: res + next: return_result + +return_result: + return: ${res.response.body} + next: end diff --git a/DSL/Ruuter.private/global-classifier/POST/agencies/add.yml b/DSL/Ruuter.private/global-classifier/POST/agencies/add.yml new file mode 100644 index 00000000..2fb30a36 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/agencies/add.yml @@ -0,0 +1,92 @@ +declaration: + call: declare + version: 0.1 + description: "Add multiple agencies to the integrated_agencies table" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agencies + type: array + description: "Body field 'agencies' as an array of agency objects" + +extractRequestData: + assign: + agencies: ${incoming.body.agencies || []} + addedAgencies: 0 + currentIndex: 0 + +processAgencies: + switch: + - condition: ${currentIndex >= agencies.length} + next: return_final_result + next: processCurrentAgency + +processCurrentAgency: + assign: + currentAgency: ${agencies[currentIndex]} + agencyId: ${agencies[currentIndex].agencyId} + agencyName: ${agencies[currentIndex].agencyName} + next: checkAgencyExists + +checkAgencyExists: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-agency" + body: + agencyId: ${agencyId} + result: agency_res + next: evaluateAgencyExistence + +evaluateAgencyExistence: + switch: + - condition: "${agency_res.response.body.length > 0}" + next: moveToNextAgency + next: addAgency + +addAgency: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-agency" + body: + agencyId: ${agencyId} + agencyName: ${agencyName} + groupKey: "" + isLatest: true + deploymentStatus: "undeployed" + isEnabled: false + agencyDataHash: "" + enableAllowed: false + lastModelTrained: "" + lastUpdatedTimestamp: ${new Date().toISOString()} + lastTrainedTimestamp: "1970-01-01T00:00:00.000Z" + syncStatus: "Unavailable_in_CKB" + createdAt: ${new Date().toISOString()} + result: add_agency_res + next: addAgencySuccessResult + +addAgencySuccessResult: + assign: + addedAgencies: ${addedAgencies+1} + next: moveToNextAgency + +moveToNextAgency: + assign: + currentIndex: ${currentIndex + 1} + next: processAgencies + +return_final_result: + switch: + - condition: "${addedAgencies === 0}" + next: return_no_agencies_added + next: return_added_agencies_count + +return_added_agencies_count: + return: "${addedAgencies} agencies added successfully." + next: end + +return_no_agencies_added: + return: "No new agencies were added." + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/agencies/data/resync.yml b/DSL/Ruuter.private/global-classifier/POST/agencies/data/resync.yml new file mode 100644 index 00000000..dfa286e7 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/agencies/data/resync.yml @@ -0,0 +1,68 @@ +declaration: + call: declare + version: 0.1 + description: "Resync new data from CKB" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agencyId + type: string + description: "Agency ID" + +extractInput: + assign: + agencyId: ${incoming.body.agencyId} + +importAgencyData: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/ckb/agency-data-import" + headers: + cookie: ${incoming.headers.cookie} + body: + agencyIds: ${agencyId} + result: importResult + next: updateAgencySyncStatus + +updateAgencySyncStatus: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-agency-data" + body: + agencyId: ${agencyId} + syncStatus: "Resync_in_progress_with_CKB" + agencyDataHash: ${importResult.response.body.response.find(a => a.agencyId == agencyId)?.agencyDataHash || ""} + result: updateResult + next: CreateDatasetVersion + +CreateDatasetVersion: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/datasets/version/minor" + result: datasetResponse + next: forwardToFormatting + +forwardToFormatting: + switch: + - condition: ${datasetResponse.response.body.response.length === 0} + next: end + next: initiateDataGeneration + +initiateDataGeneration: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/data/generate" + body: + presignedUrls: ${importResult.response.body.response} + datasetId: ${datasetResponse.response.body.response[0].id} + majorVersion: ${datasetResponse.response.body.response[0].major} + minorVersion: ${datasetResponse.response.body.response[0].minor} + result: dataGenerationResponse + next: returnImportResult + +returnImportResult: + return: ${dataGenerationResponse.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/agencies/data/sync.yml b/DSL/Ruuter.private/global-classifier/POST/agencies/data/sync.yml new file mode 100644 index 00000000..fc56e652 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/agencies/data/sync.yml @@ -0,0 +1,200 @@ +declaration: + call: declare + version: 0.1 + description: "Fetch new agencies from CentOps, retrieve their data, and add them to the database" + method: post + accepts: json + returns: json + namespace: global-classifier + + +fetchAgenciesFromCentops: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/centops/new-agencies" + headers: + cookie: ${incoming.headers.cookie} + result: centopsResponse + log: "Fetched agencies from CentOps: ${centopsResponse}" + next: checkAgenciesExist + +checkAgenciesExist: + switch: + - condition: ${!centopsResponse.response.body.response || centopsResponse.response.body.response.length === 0} + next: returnNoAgenciesFound + next: getNewAgencies + +getNewAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-all-agencies" + result: allAgencies + next: compareAgencies + +compareAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/extract_unavailable_agencies" + headers: + type: json + body: + gcAgencies: ${allAgencies.response.body} + centopsAgencies: ${centopsResponse.response.body.response} + result: newAgencies + next: addAgencies + +checkNewAgenciesAvilability: + switch: + - condition: ${!newAgencies.response.body.agencies || newAgencies.response.body.agencies.length === 0} + next: returnNoAgenciesFound + next: addAgencies + +addAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/agencies/add" + headers: + cookie: ${incoming.headers.cookie} + body: + agencies: ${newAgencies.response.body.agencies} + result: addAgenciesResult + next: getUnavailableAgencyIds + +getUnavailableAgencyIds: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-data-unavailable-agencies" + result: unavailableAgencyIds + next: checkAgenciesWithNoData + +checkAgenciesWithNoData: + switch: + - condition: ${!unavailableAgencyIds || unavailableAgencyIds.length === 0} + next: returnNoAgenciesFound + next: checkAgencyData + +# extractAgencyIds: +# call: http.post +# args: +# url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/extract_agency_ids" +# headers: +# type: json +# body: +# agencies: ${centopsResponse.response.body.response} +# result: agencyIds +# next: checkAgencyData + +checkAgencyData: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/ckb/agency-data-exist" + headers: + cookie: ${incoming.headers.cookie} + body: + agencyIds: ${unavailableAgencyIds.response.body.map(a => a.agencyId).join(' ')} + result: agencyMetadata + next: filterAvailableAgencies + +filterAvailableAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/get-data-available-agencies" + headers: + type: json + body: + agencies: ${agencyMetadata.response.body.response.filter(a => a.isDataAvailable)} + result: availableAgencyIds + next: checkAvailableAgencies + +checkAvailableAgencies: + switch: + - condition: ${!availableAgencyIds.response.body || availableAgencyIds.response.body.length === 0} + next: formatResults + next: importAgencyData + +importAgencyData: + call: http.post + log: "Importing agency data for IDs: ${agencyIdsString.response.body}" + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/ckb/agency-data-import" + headers: + cookie: ${incoming.headers.cookie} + body: + agencyIds: ${availableAgencyIds.response.body.join(' ')} + result: importResult + next: initializeUpdateProcess + +# ++++ + +initializeUpdateProcess: + assign: + agencyUpdateIndex: 0 + totalAgencies: ${availableAgencyIds.response.body.length} + updatedAgencies: 0 + log: "Starting update for ${totalAgencies} agencies" + next: processNextAgency + +processNextAgency: + switch: + - condition: ${agencyUpdateIndex >= totalAgencies} + next: returnImportResult + next: updateCurrentAgency + +updateCurrentAgency: + assign: + currentAgencyId: ${availableAgencyIds.response.body[agencyUpdateIndex]} + log: "Updating agency ${agencyUpdateIndex + 1} of ${totalAgencies}: ${currentAgencyId}" + next: updateAgencySyncStatus + +updateAgencySyncStatus: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-agency-data" + body: + agencyId: ${currentAgencyId} + syncStatus: "Sync_in_progress_with_CKB" + agencyDataHash: ${importResult.response.body.response.find(a => a.agencyId == currentAgencyId)?.agencyDataHash || ""} + result: updateResult + next: incrementUpdateCounter + +incrementUpdateCounter: + assign: + agencyUpdateIndex: ${agencyUpdateIndex + 1} + updatedAgencies: ${updatedAgencies + 1} + next: processNextAgency + +# +++++ + +returnImportResult: + return: ${importResult.response.body.response} + next: end + +formatResults: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/agencies-sync-summary" + headers: + type: json + body: + totalFound: ${unavailableAgencyIds.response.body.length} + addResult: ${addAgenciesResult.response.body.response} + importResult: "No data imported" + result: finalResult + next: returnResult + +returnResult: + return: ${finalResult.response.body} + next: end + +returnNoAgenciesFound: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/no-new-agencies" + headers: + type: json + result: noAgenciesResult + next: returnEmptyResult + +returnEmptyResult: + return: ${noAgenciesResult.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/agencies/data/update.yml b/DSL/Ruuter.private/global-classifier/POST/agencies/data/update.yml new file mode 100644 index 00000000..9652b105 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/agencies/data/update.yml @@ -0,0 +1,97 @@ +declaration: + call: declare + version: 0.1 + description: "Check if new data_url is available in mock_ckb for agencies synced with CKB" + method: post + accepts: json + returns: json + namespace: global-classifier + +checkForNewMockCkbData: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-synced-agency-ids" + result: syncedAgencies + next: checkForSyncedAgencies + +checkForSyncedAgencies: + switch: + - condition: ${syncedAgencies.response.body.length > 0} + next: fetchMockCkbHashes + next: returnNoAgencies + +fetchMockCkbHashes: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/ckb/agency-data-import" + headers: + cookie: ${incoming.headers.cookie} + type: json + body: + agencyIds: ${syncedAgencies.response.body.map(a => a.agencyId).join(' ')} + result: ckbData + next: initializeResyncCheck + +initializeResyncCheck: + assign: + ckbIndex: 0 + resyncNeededAgencies: 0 + totalCkb: ${ckbData.response.body.response.length} + next: processNextCkbAgency + +processNextCkbAgency: + switch: + - condition: ${ckbIndex >= totalCkb} + next: returnResult + next: checkAgencyHash + +checkAgencyHash: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-agency-datahash" + body: + agencyId: ${ckbData.response.body.response[ckbIndex].agencyId} + result: integratedAgency + next: compareHashes + +compareHashes: + assign: + ckbAgency: ${ckbData.response.body.response[ckbIndex]} + integratedHash: ${integratedAgency.response.body[0].agencyDataHash} + ckbHash: ${ckbAgency.agencyDataHash} + needsResync: ${integratedHash !== ckbHash} + next: updateIfNeeded + +updateIfNeeded: + switch: + - condition: ${needsResync} + next: updateResyncStatus + next: incrementCkbIndex + +updateResyncStatus: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-agency-sync-status" + body: + agencyId: ${ckbAgency.agencyId} + syncStatus: "Resync_needed_with_CKB" + isEnabled: false + result: updateResult + assign: + resyncNeededAgencies: ${resyncNeededAgencies + 1} + next: incrementCkbIndex + +incrementCkbIndex: + assign: + ckbIndex: ${ckbIndex + 1} + next: processNextCkbAgency + +returnNoAgencies: + return: "No synced agencies found" + status: 404 + next: end + +returnResult: + return: "Agencies needing resync- ${resyncNeededAgencies}" + status: 200 + next: end diff --git a/DSL/Ruuter.private/global-classifier/POST/agencies/disable.yml b/DSL/Ruuter.private/global-classifier/POST/agencies/disable.yml new file mode 100644 index 00000000..481aa582 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/agencies/disable.yml @@ -0,0 +1,43 @@ +declaration: + call: declare + version: 0.1 + description: "Disable an agency in the integrated_agencies table" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agencyId + type: string + description: "Agency ID to disable" + +extractRequestData: + assign: + agencyId: ${incoming.body.agencyId} + +validateInput: + switch: + - condition: ${!agencyId} + next: returnMissingAgencyId + next: disableAgency + +disableAgency: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-agency-disabled" + body: + agencyId: ${agencyId} + isEnabled: false + result: disableResult + next: returnResult + +returnResult: + return: "Agency disabled successfully" + status: 200 + next: end + +returnMissingAgencyId: + return: "Missing agencyId" + status: 400 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/agencies/enable.yml b/DSL/Ruuter.private/global-classifier/POST/agencies/enable.yml new file mode 100644 index 00000000..585219ff --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/agencies/enable.yml @@ -0,0 +1,54 @@ +declaration: + call: declare + version: 0.1 + description: "Disable an agency in the integrated_agencies table" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agencyId + type: string + description: "Agency ID to disable" + +extractRequestData: + assign: + agencyId: ${incoming.body.agencyId} + +validateInput: + switch: + - condition: ${!agencyId} + next: returnMissingAgencyId + next: enableAgency + +enableAgency: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-agency-enabled" + body: + agencyId: ${agencyId} + isEnabled: true + result: enableResult + next: checkEnableResult + +checkEnableResult: + switch: + - condition: ${!enableResult || enableResult.response.body.length===0} + next: returnEnableFailed + next: returnResult + +returnResult: + return: ${enableResult.response.body} + status: 200 + next: end + +returnMissingAgencyId: + return: "missing agencyId" + status: 400 + next: end + +returnEnableFailed: + return: "unable to enable agency" + status: 400 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/agencies/fetch.yml b/DSL/Ruuter.private/global-classifier/POST/agencies/fetch.yml new file mode 100644 index 00000000..52b1738c --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/agencies/fetch.yml @@ -0,0 +1,87 @@ +declaration: + call: declare + version: 0.1 + description: "Fetch new agencies from CentOps, retrieve their data, and add them to the database" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: lastSyncedTimestamp + type: string + description: "Timestamp to fetch agencies updated after this time" + +extractInput: + assign: + lastSyncedTimestamp: ${incoming.body.lastSyncedTimestamp || (new Date()).toISOString()} + log: "Starting agency sync with timestamp: ${lastSyncedTimestamp}" + +fetchAgenciesFromCentops: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/centops/new-agencies" + headers: + cookie: ${incoming.headers.cookie} + body: + lastSyncedTimestamp: ${lastSyncedTimestamp} + result: centopsResponse + log: "Fetched agencies from CentOps: ${centopsResponse}" + next: extractAgencyIds + +extractAgencyIds: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/extract_agency_ids" + headers: + type: json + body: + agencies: ${centopsResponse.response.body.response.agencies} + result: agencyIds + next: fetchAgencyMetadata + +fetchAgencyMetadata: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/ckb/agency-data-exist" + body: + agencyIds: ${agencyIds} + result: agencyMetadata + next: prepareAgencies + +prepareAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifie/prepare_agencies_for_import" + headers: + type: json + body: + agencyMetadata: ${agencyMetadata.body} + result: preparedAgencies + next: addAgencies + +addAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/agencies/add" + body: + agencies: ${preparedAgencies.body} + result: addAgenciesResult + next: formatResults + +formatResults: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/format_agency_sync_results" + headers: + type: json + body: + inputTimestamp: ${lastSyncedTimestamp} + totalFound: ${agencyIds.body.length} + addResult: ${addAgenciesResult.body} + result: finalResult + next: returnResult + +returnResult: + return: ${finalResult.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/agencies/update.yml b/DSL/Ruuter.private/global-classifier/POST/agencies/update.yml new file mode 100644 index 00000000..3d756cef --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/agencies/update.yml @@ -0,0 +1,111 @@ +declaration: + call: declare + version: 0.1 + description: "Update an agency in the integrated_agencies table" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agency_id + type: string + description: "Agency ID (required)" + - field: agency_name + type: string + description: "Name of the agency" + - field: group_key + type: string + description: "Group key" + - field: is_latest + type: boolean + description: "Whether this is the latest version" + - field: deployment_status + type: string + description: "Deployment status (deployed, testing, undeployed)" + - field: is_enabled + type: boolean + description: "Whether the agency is enabled" + - field: agency_data_hash + type: string + description: "Hash of the agency data" + - field: enable_allowed + type: boolean + description: "Whether enabling is allowed" + - field: last_model_trained + type: string + description: "Name/ID of the last model trained" + - field: last_trained_timestamp + type: string + description: "When the model was last trained" + - field: sync_status + type: string + description: "Sync status with CKB" + +extractRequestData: + assign: + agencyId: ${incoming.body.agency_id} + agencyName: ${incoming.body.agency_name ?? null} + groupKey: ${incoming.body.group_key ?? null} + isLatest: ${incoming.body.is_latest ?? null} + deploymentStatus: ${incoming.body.deployment_status ?? null} + isEnabled: ${incoming.body.is_enabled ?? null} + agencyDataHash: ${incoming.body.agency_data_hash ?? null} + enableAllowed: ${incoming.body.enable_allowed ?? null} + lastModelTrained: ${incoming.body.last_model_trained ?? null} + lastTrainedTimestamp: ${incoming.body.last_trained_timestamp ?? null} + syncStatus: ${incoming.body.sync_status ?? null} + +validateRequiredFields: + switch: + - condition: ${!agencyId} + next: return_missing_agency_id + next: checkAgencyExists + +checkAgencyExists: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-agency" + body: + agencyId: ${agencyId} + result: agency_res + next: evaluateAgencyExistence + +evaluateAgencyExistence: + switch: + - condition: "${agency_res.response.body.length === 0}" + next: return_agency_not_found + next: updateAgency + +updateAgency: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-agency" + body: + agencyId: ${agencyId} + agencyName: ${agencyName} + groupKey: ${groupKey} + isLatest: ${isLatest} + deploymentStatus: ${deploymentStatus} + isEnabled: ${isEnabled} + agencyDataHash: ${agencyDataHash} + enableAllowed: ${enableAllowed} + lastModelTrained: ${lastModelTrained} + lastTrainedTimestamp: ${lastTrainedTimestamp} + syncStatus: ${syncStatus} + result: update_agency_res + next: return_result + +return_result: + return: ${update_agency_res.response.body} + next: end + +return_missing_agency_id: + return: "error: missing agency_id" + status: 400 + next: end + +return_agency_not_found: + return: "error: agency not found" + status: 404 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/centops/new-agencies.yml b/DSL/Ruuter.private/global-classifier/POST/centops/new-agencies.yml new file mode 100644 index 00000000..c3a265f8 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/centops/new-agencies.yml @@ -0,0 +1,19 @@ +declaration: + call: declare + version: 0.1 + description: "Get agencies synchronized after a specific timestamp" + method: post + accepts: json + returns: json + namespace: global-classifier + +fetch_agencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/mock-get-agencies-from-centops" + result: agency_data + next: return_result + +return_result: + return: ${agency_data.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/ckb/agency-data-exist.yml b/DSL/Ruuter.private/global-classifier/POST/ckb/agency-data-exist.yml new file mode 100644 index 00000000..94cf506f --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/ckb/agency-data-exist.yml @@ -0,0 +1,33 @@ +declaration: + call: declare + version: 0.1 + description: "Get agency data information by agency IDs" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agencyIds + type: array + description: "Array of unique institution IDs" + +extractRequestData: + assign: + agencyIds: ${incoming.body.agencyIds || []} + log: "Received request for agency data: ${agencyIds}" + +check_data_availability: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/check-agency-data-availability" + headers: + type: json + body: + agencyIds: ${agencyIds} + result: agency_data_info + next: return_result + +return_result: + return: ${agency_data_info.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/ckb/agency-data-import.yml b/DSL/Ruuter.private/global-classifier/POST/ckb/agency-data-import.yml new file mode 100644 index 00000000..864f8e00 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/ckb/agency-data-import.yml @@ -0,0 +1,33 @@ +declaration: + call: declare + version: 0.1 + description: "Get agency data information by agency IDs" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agencyIds + type: array + description: "Array of unique institution IDs" + +extractRequestData: + assign: + agencyIds: ${incoming.body.agencyIds || []} + log: "Received request for agency data: ${agencyIds}" + +get_agency_data: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/mock-get-data-from-ckb" + headers: + type: json + body: + agencyIds: ${agencyIds} + result: agency_data_info + next: return_result + +return_result: + return: ${agency_data_info.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/datamodels/create.yml b/DSL/Ruuter.private/global-classifier/POST/datamodels/create.yml new file mode 100644 index 00000000..2f9994b5 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/datamodels/create.yml @@ -0,0 +1,193 @@ +declaration: + call: declare + version: 0.1 + description: "Create a new data model record in data_models, update connected dataset and initiate training" + method: post + accepts: json + returns: json + namespace: global-classifier + # Input Validation Schema + allowlist: + body: + - field: modelName + type: string + description: "Name of the model (required)" + - field: deploymentEnv + type: string + description: "Deployment environment: production, testing, undeployed (required)" + - field: baseModels + type: array + description: "Array of base models: ['estbert', 'xlm-roberta', 'multilingual-distilbert'] (required)" + - field: connectedDsId + type: number + description: "Connected dataset ID (required)" + - field: connectedDsMajorVersion + type: number + description: "Connected dataset major version (required)" + - field: connectedDsMinorVersion + type: number + description: "Connected dataset minor version (required)" + next: extractRequestData + +# Data Extraction +extractRequestData: + assign: + modelName: ${incoming.body.modelName} + deploymentEnv: ${incoming.body.deploymentEnv} + baseModels: ${incoming.body.baseModels} + connectedDsId: ${incoming.body.connectedDsId} + connectedDsMajorVersion: ${incoming.body.connectedDsMajorVersion} + connectedDsMinorVersion: ${incoming.body.connectedDsMinorVersion} + next: validateRequiredFields + +# Required Field Validation +validateRequiredFields: + switch: + - condition: ${modelName === null || modelName === undefined || modelName === "" || deploymentEnv === null || deploymentEnv === undefined || deploymentEnv === "" || baseModels === null || baseModels === undefined || connectedDsId === null || connectedDsId === undefined} + next: return_missing_required_fields + - condition: ${connectedDsMajorVersion === null || connectedDsMajorVersion === undefined || connectedDsMinorVersion === null || connectedDsMinorVersion === undefined} + next: return_missing_required_fields + - condition: ${!baseModels || baseModels.length === 0} + next: return_missing_required_fields + next: validateEnumValues + +# Enum Value Validation +validateEnumValues: + switch: + - condition: ${!["production", "testing", "undeployed"].includes(deploymentEnv)} + next: return_invalid_deployment_env + - condition: ${!baseModels.every(model => ["estbert", "xlm-roberta", "multilingual-distilbert"].includes(model))} + next: return_invalid_base_model + next: checkProductionDeployment + +# Check if Production Deployment +checkProductionDeployment: + switch: + - condition: ${deploymentEnv === "production"} + next: updateExistingProductionModels + next: insertModelMetadata + +# Update Existing Production Models to Undeployed +updateExistingProductionModels: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-datamodel-deployment-env" + result: update_production_res + next: checkProductionUpdateResult + +# Check Production Update Result +checkProductionUpdateResult: + switch: + - condition: ${!update_production_res || !update_production_res.response} + next: return_production_update_failed + next: insertModelMetadata + +# Insert Model Metadata +insertModelMetadata: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-data-models" + body: + modelName: ${modelName} + deploymentEnv: ${deploymentEnv} + baseModels: ${JSON.stringify(baseModels)} + connectedDsId: ${connectedDsId} + connectedDsMajorVersion: ${connectedDsMajorVersion} + connectedDsMinorVersion: ${connectedDsMinorVersion} + result: insert_model_res + next: checkInsertResult + +# Check Insert Result +checkInsertResult: + switch: + - condition: ${!insert_model_res || !insert_model_res.response.body || insert_model_res.response.body.length === 0} + next: return_insert_failed + next: updateDatasetConnectedModels + +# Update Dataset Connected Models +updateDatasetConnectedModels: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-datasets-connected-models" + body: + modelId: ${insert_model_res.response.body[0].modelId} + datasetId: ${connectedDsId} + result: update_dataset_res + next: checkUpdateResult + +# Check Update Result +checkUpdateResult: + switch: + - condition: ${!update_dataset_res || !update_dataset_res.response.body || update_dataset_res.response.body.length === 0} + next: return_update_failed + next: initiateTraining + +# Initiate Training +initiateTraining: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/datamodels/train" + headers: + cookie: ${incoming.headers.cookie} + body: + modelId: ${insert_model_res.response.body[0].modelId} + modelName: ${modelName} + majorVersion: ${insert_model_res.response.body[0].major} + minorVersion: ${insert_model_res.response.body[0].minor} + latest: true + deploymentEnv: ${deploymentEnv} + result: training_res + next: logTrainingResult + +logTrainingResult: + log: "Initiate training response: ${training_res.response}" + next: checkTrainingResult + +# Check Training Result +checkTrainingResult: + switch: + - condition: ${!training_res || !training_res.response} + next: return_training_failed + - condition: ${training_res.response.body.response.operationSuccessful !== true} + next: return_training_failed + next: return_success + +return_success: + return: "Data model created successfully and training initiated. Model ID:${insert_model_res.response.body[0].modelId} Job ID: ${training_res.response.body.response.jobId}" + status: 200 + next: end + +return_missing_required_fields: + return: "error: missing required fields (modelName, deploymentEnv, baseModels as array, connectedDsId, connectedDsMajorVersion, connectedDsMinorVersion)" + status: 400 + next: end + +return_invalid_deployment_env: + return: "error: invalid deploymentEnv. Must be one of: production, testing, undeployed" + status: 400 + next: end + +return_invalid_base_model: + return: "error: invalid baseModels" + status: 400 + next: end + +return_production_update_failed: + return: "error: failed to update existing production models to undeployed before creating new production model" + status: 500 + next: end + +return_insert_failed: + return: "error: failed to create model metadata record" + status: 500 + next: end + +return_update_failed: + return: "error: model created but failed to update dataset connected_models" + status: 500 + next: end + +return_training_failed: + return: "error: model created and dataset updated but failed to initiate training" + status: 500 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/datamodels/delete.yml b/DSL/Ruuter.private/global-classifier/POST/datamodels/delete.yml new file mode 100644 index 00000000..aa44aa56 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/datamodels/delete.yml @@ -0,0 +1,54 @@ +declaration: + call: declare + version: 0.1 + description: "Delete a data model by modelId" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: modelId + type: number + description: "ID of the data model to delete" + +extractRequestData: + assign: + modelId: ${incoming.body.modelId} + next: validateModelId + +validateModelId: + switch: + - condition: ${modelId === null || modelId === undefined || !modelId} + next: return_missing_model_id + next: deleteDataModel + +deleteDataModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/delete-data-model-by-id" + body: + model_id: ${Number(modelId)} + result: deleteResult + next: checkDeleteResult + +checkDeleteResult: + switch: + - condition: ${!deleteResult || !deleteResult.response || !deleteResult.response.body} + next: return_delete_failed + next: return_success + +return_success: + return: "Data model deleted successfully. Model ID: ${modelId}" + status: 200 + next: end + +return_missing_model_id: + return: '{"error": "Missing required field: modelId"}' + status: 400 + next: end + +return_delete_failed: + return: '{"error": "Failed to delete data model"}' + status: 500 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/datamodels/major.yml b/DSL/Ruuter.private/global-classifier/POST/datamodels/major.yml new file mode 100644 index 00000000..c1bdd896 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/datamodels/major.yml @@ -0,0 +1,244 @@ +declaration: + call: declare + version: 0.1 + description: "Create a new data model with incremented major version" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: modelGroupKey + type: string + description: "Model group key to create new version for (required)" + - field: modelName + type: string + description: "Name of the model (required)" + - field: deploymentEnv + type: string + description: "Deployment environment: production, testing, undeployed (required)" + - field: baseModels + type: array + description: "Array of base models: ['bert', 'roberta', 'xlm'] (required)" + - field: connectedDsId + type: number + description: "Connected dataset ID (required)" + - field: connectedDsMajorVersion + type: number + description: "Connected dataset major version (required)" + - field: connectedDsMinorVersion + type: number + description: "Connected dataset minor version (required)" + next: extractRequestData + +# Data Extraction +extractRequestData: + assign: + modelGroupKey: ${incoming.body.modelGroupKey} + modelName: ${incoming.body.modelName} + deploymentEnv: ${incoming.body.deploymentEnv} + baseModels: ${incoming.body.baseModels} + connectedDsId: ${incoming.body.connectedDsId} + connectedDsMajorVersion: ${incoming.body.connectedDsMajorVersion} + connectedDsMinorVersion: ${incoming.body.connectedDsMinorVersion} + next: validateRequiredFields + +# Required Field Validation +validateRequiredFields: + switch: + - condition: ${modelGroupKey === null || modelGroupKey === undefined || modelGroupKey === "" || modelName === null || modelName === undefined || modelName === "" || deploymentEnv === null || deploymentEnv === undefined || deploymentEnv === "" || baseModels === null || baseModels === undefined || connectedDsId === null || connectedDsId === undefined} + next: return_missing_required_fields + - condition: ${connectedDsMajorVersion === null || connectedDsMajorVersion === undefined || connectedDsMinorVersion === null || connectedDsMinorVersion === undefined} + next: return_missing_required_fields + - condition: ${!baseModels || baseModels.length === 0} + next: return_missing_required_fields + next: validateEnumValues + +# Enum Value Validation +validateEnumValues: + switch: + - condition: ${!["production", "testing", "undeployed"].includes(deploymentEnv)} + next: return_invalid_deployment_env + - condition: ${!baseModels.every(model => ["estbert", "xlm-roberta", "multilingual-distilbert"].includes(model))} + next: return_invalid_base_model + next: checkProductionDeployment + +# Check if Production Deployment +checkProductionDeployment: + switch: + - condition: ${deploymentEnv === "production"} + next: updateExistingProductionModels + next: getLatestDataModel + +# Update Existing Production Models to Undeployed +updateExistingProductionModels: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-datamodel-deployment-env" + result: update_production_res + next: checkProductionUpdateResult + +# Check Production Update Result +checkProductionUpdateResult: + switch: + - condition: ${!update_production_res || !update_production_res.response || !update_production_res.response.body} + next: return_production_update_failed + next: getLatestDataModel + +# Get Latest Data Model +getLatestDataModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-highest-major-version" + body: + modelGroupKey: ${modelGroupKey} + result: latestDataModelResult + next: createDataModel + +# Create Data Model Version +createDataModel: + switch: + - condition: ${latestDataModelResult.response.body.length === 0} + next: computeInitialVersion + next: computeNewVersion + +# Compute Initial Version (First Model) +computeInitialVersion: + assign: + newMajor: 1 + newMinor: 0 + switchResult: null + next: insertDataModel + +# Compute New Version (Increment Major) +computeNewVersion: + assign: + latestDataModel: ${latestDataModelResult.response.body[0]} + newMajor: ${latestDataModel.major + 1} + newMinor: 0 + previousModelId: ${latestDataModel.modelId} + next: switchLatestFlag + +# Switch Latest Flag to False for Previous Model +switchLatestFlag: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-datamodels-latest-state" + body: + modelId: ${previousModelId} + result: switchResult + next: insertDataModel + +# Insert New Data Model +insertDataModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/create-datamodels-version" + body: + modelGroupKey: ${modelGroupKey} + modelName: ${modelName} + major: ${newMajor} + minor: ${newMinor} + deploymentEnv: ${deploymentEnv} + baseModels: ${JSON.stringify(baseModels)} + connectedDsId: ${connectedDsId} + connectedDsMajorVersion: ${connectedDsMajorVersion} + connectedDsMinorVersion: ${connectedDsMinorVersion} + result: insertResult + next: checkInsertResult + +# Check Insert Result +checkInsertResult: + switch: + - condition: ${!insertResult || !insertResult.response.body || insertResult.response.body.length === 0} + next: return_insert_failed + next: updateDatasetConnectedModels + +# Update Dataset Connected Models +updateDatasetConnectedModels: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-datasets-connected-models" + body: + modelId: ${insertResult.response.body[0].modelId} + datasetId: ${connectedDsId} + result: updateDatasetResult + next: checkDatasetUpdateResult + +# Check Dataset Update Result +checkDatasetUpdateResult: + switch: + - condition: ${!updateDatasetResult || !updateDatasetResult.response || !updateDatasetResult.response.body || updateDatasetResult.response.body.length === 0} + next: return_dataset_update_failed + next: initiateTraining + +# Initiate Training +initiateTraining: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/datamodels/train" + headers: + cookie: ${incoming.headers.cookie} + body: + modelId: ${insertResult.response.body[0].modelId} + modelName: ${modelName} + majorVersion: ${insertResult.response.body[0].major} + minorVersion: ${insertResult.response.body[0].minor} + latest: true + deploymentEnv: ${deploymentEnv} + result: training_res + next: logTrainingResult + +logTrainingResult: + log: "Initiate training response: ${training_res.response}" + next: checkTrainingResult + +# Check Training Result +checkTrainingResult: + switch: + - condition: ${!training_res || !training_res.response} + next: return_training_failed + - condition: ${training_res.response.body.response.operationSuccessful !== true} + next: return_training_failed + next: return_success + +return_success: + return: "Data model created successfully and training initiated. Model ID:${insertResult.response.body[0].modelId} Job ID: ${training_res.response.body.response.jobId}" + status: 200 + next: end + + +return_missing_required_fields: + return: "error: missing required fields (modelGroupKey, modelName, deploymentEnv, baseModels as array, connectedDsId, connectedDsMajorVersion, connectedDsMinorVersion)" + status: 400 + next: end + +return_invalid_deployment_env: + return: "error: invalid deploymentEnv. Must be one of: production, testing, undeployed" + status: 400 + next: end + +return_invalid_base_model: + return: "error: invalid baseModels" + status: 400 + next: end + +return_production_update_failed: + return: "error: failed to update existing production models to undeployed before creating new production model" + status: 500 + next: end + +return_insert_failed: + return: "error: failed to create new model version" + status: 500 + next: end + +return_dataset_update_failed: + return: "error: model created but failed to update dataset connected_models" + status: 500 + next: end + +return_training_failed: + return: "error: model created and dataset updated but failed to initiate training" + status: 500 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/datamodels/minor.yml b/DSL/Ruuter.private/global-classifier/POST/datamodels/minor.yml new file mode 100644 index 00000000..990c6269 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/datamodels/minor.yml @@ -0,0 +1,250 @@ +declaration: + call: declare + version: 0.1 + description: "Create a new data model with incremented minor version" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: modelGroupKey + type: string + description: "Model group key to create new version for (required)" + - field: modelName + type: string + description: "Name of the model (required)" + - field: deploymentEnv + type: string + description: "Deployment environment: production, testing, undeployed (required)" + - field: baseModels + type: array + description: "Array of base models: ['bert', 'roberta', 'xlm'] (required)" + - field: connectedDsId + type: number + description: "Connected dataset ID (required)" + - field: connectedDsMajorVersion + type: number + description: "Connected dataset major version (required)" + - field: connectedDsMinorVersion + type: number + description: "Connected dataset minor version (required)" + - field: isTrainingNeeded + type: boolean + description: "Whether to initiate training after model creation (optional, defaults to false)" + next: extractRequestData + +# Data Extraction +extractRequestData: + assign: + modelGroupKey: ${incoming.body.modelGroupKey} + modelName: ${incoming.body.modelName} + deploymentEnv: ${incoming.body.deploymentEnv} + baseModels: ${incoming.body.baseModels} + connectedDsId: ${incoming.body.connectedDsId} + connectedDsMajorVersion: ${incoming.body.connectedDsMajorVersion} + connectedDsMinorVersion: ${incoming.body.connectedDsMinorVersion} + isTrainingNeeded: ${incoming.body.isTrainingNeeded || false} + next: validateRequiredFields +# Required Field Validation +validateRequiredFields: + switch: + - condition: ${modelGroupKey === null || modelGroupKey === undefined || modelGroupKey === "" || modelName === null || modelName === undefined || modelName === "" || deploymentEnv === null || deploymentEnv === undefined || deploymentEnv === "" || baseModels === null || baseModels === undefined || connectedDsId === null || connectedDsId === undefined } + next: return_missing_required_fields + - condition: ${connectedDsMajorVersion === null || connectedDsMajorVersion === undefined || connectedDsMinorVersion === null || connectedDsMinorVersion === undefined} + next: return_missing_required_fields + - condition: ${!baseModels || baseModels.length === 0} + next: return_missing_required_fields + next: validateEnumValues + +# Enum Value Validation +validateEnumValues: + switch: + - condition: ${!["production", "testing", "undeployed"].includes(deploymentEnv)} + next: return_invalid_deployment_env + - condition: ${!baseModels.every(model => ["estbert", "xlm-roberta", "multilingual-distilbert"].includes(model))} + next: return_invalid_base_model + next: checkProductionDeployment + +# Check if Production Deployment +checkProductionDeployment: + switch: + - condition: ${deploymentEnv === "production"} + next: updateExistingProductionModels + next: getLatestDataModel + +# Update Existing Production Models to Undeployed +updateExistingProductionModels: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-datamodel-deployment-env" + result: update_production_res + next: checkProductionUpdateResult + +# Check Production Update Result +checkProductionUpdateResult: + switch: + - condition: ${!update_production_res || !update_production_res.response || !update_production_res.response.body} + next: return_production_update_failed + next: getLatestDataModel + +# Get Latest Data Model +getLatestDataModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-latest-datamodel" + body: + modelGroupKey: ${modelGroupKey} + result: latestDataModelResult + next: createDataModel + +# Create Data Model Version +createDataModel: + switch: + - condition: ${latestDataModelResult.response.body.length === 0} + next: end + next: computeNewVersion + +# Compute New Version (Increment Minor) +computeNewVersion: + assign: + latestDataModel: ${latestDataModelResult.response.body[0]} + newMajor: ${latestDataModel?.major} + newMinor: ${latestDataModel?.minor + 1} + previousModelId: ${latestDataModel?.modelId} + next: switchLatestFlag + +# Switch Latest Flag to False for Previous Model +switchLatestFlag: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-datamodels-latest-state" + body: + modelId: ${previousModelId} + result: switchResult + next: insertDataModel + +# Insert New Data Model +insertDataModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/create-datamodels-version" + body: + modelGroupKey: ${modelGroupKey} + modelName: ${modelName} + major: ${newMajor} + minor: ${newMinor} + deploymentEnv: ${deploymentEnv} + baseModels: ${JSON.stringify(baseModels)} + connectedDsId: ${connectedDsId} + connectedDsMajorVersion: ${connectedDsMajorVersion} + connectedDsMinorVersion: ${connectedDsMinorVersion} + result: insertResult + next: checkInsertResult + +# Check Insert Result +checkInsertResult: + switch: + - condition: ${!insertResult || !insertResult.response.body || insertResult.response.body.length === 0} + next: return_insert_failed + next: updateDatasetConnectedModels + +# Update Dataset Connected Models +updateDatasetConnectedModels: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-datasets-connected-models" + body: + modelId: ${insertResult.response.body[0].modelId} + datasetId: ${connectedDsId} + result: updateDatasetResult + next: checkDatasetUpdateResult + +# Check Dataset Update Result +checkDatasetUpdateResult: + switch: + - condition: ${!updateDatasetResult || !updateDatasetResult.response || !updateDatasetResult.response.body || updateDatasetResult.response.body.length === 0} + next: return_dataset_update_failed + next: checkTrainingRequired + +# Check if Training is Required +checkTrainingRequired: + switch: + - condition: ${isTrainingNeeded === true} + next: initiateTraining + next: return_success_no_training + +# Initiate Training +initiateTraining: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/datamodels/train" + headers: + cookie: ${incoming.headers.cookie} + body: + modelId: ${insertResult.response.body[0].modelId} + modelName: ${modelName} + majorVersion: ${insertResult.response.body[0].major} + minorVersion: ${insertResult.response.body[0].minor} + latest: true + deploymentEnv: ${deploymentEnv} + result: training_res + next: logTrainingResult + +logTrainingResult: + log: "Initiate training response: ${training_res.response}" + next: checkTrainingResult + +# Check Training Result +checkTrainingResult: + switch: + - condition: ${!training_res || !training_res.response} + next: return_training_failed + - condition: ${training_res.response.body.response.operationSuccessful !== true} + next: return_training_failed + next: return_success + +return_success: + return: "Data model created successfully and training initiated. Model ID: ${insertResult.response.body[0].modelId} Job ID: ${training_res.response.body.response.jobId}" + status: 200 + next: end + +return_success_no_training: + return: "Data model created successfully. Model ID: ${insertResult.response.body[0].modelId}" + status: 200 + next: end + +return_missing_required_fields: + return: "error: missing required fields (modelGroupKey, modelName, deploymentEnv, baseModels as array, connectedDsId, connectedDsMajorVersion, connectedDsMinorVersion)" + status: 400 + next: end + +return_invalid_deployment_env: + return: "error: invalid deploymentEnv. Must be one of: production, testing, undeployed" + status: 400 + next: end + +return_invalid_base_model: + return: "error: invalid baseModels" + status: 400 + next: end + +return_production_update_failed: + return: "error: failed to update existing production models to undeployed before creating new production model" + status: 500 + next: end + +return_insert_failed: + return: "error: failed to create new model version" + status: 500 + next: end + +return_dataset_update_failed: + return: "error: model created but failed to update dataset connected_models" + status: 500 + next: end + +return_training_failed: + return: "error: model created and dataset updated but failed to initiate training" + status: 500 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/datamodels/train.yml b/DSL/Ruuter.private/global-classifier/POST/datamodels/train.yml new file mode 100644 index 00000000..cb5272c6 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/datamodels/train.yml @@ -0,0 +1,95 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'RE-TRAIN'" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: modelId + type: number + description: "Body field 'modelId'" + - field: modelName + type: string + description: "Body field 'modelName'" + - field: majorVersion + type: number + description: "Body field 'majorVersion'" + - field: minorVersion + type: number + description: "Body field 'minorVersion'" + - field: latest + type: boolean + description: "Body field 'latest'" + - field: deploymentEnv + type: string + description: "Body field 'deploymentEnv'" + +extract_request_data: + assign: + model_id: ${incoming.body.modelId} + model_name: ${incoming.body.modelName} + major_version: ${incoming.body.majorVersion} + minor_version: ${incoming.body.minorVersion} + latest: ${incoming.body.latest} + deployment_environment: ${incoming.body.deploymentEnv} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${model_id !== null} + next: create_training_job_in_queue + next: return_incorrect_request + +create_training_job_in_queue: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-training-job-to-queue" + body: + model_id: ${model_id} + model_name: ${model_name} + major_version: ${major_version} + minor_version: ${minor_version} + latest: ${incoming.body.latest === true || incoming.body.latest === 'true'} + deployment_environment: ${deployment_environment} + result: res_model + next: log_result + +log_result: + log: "Training job creation response: ${res_model.response.statusCodeValue} - ${res_model.response.body}" + next: check_insert_result + +check_insert_result: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + success_format_res: + modelId: ${model_id} + operationSuccessful: true + message: "Training job successfully queued" + jobId: ${res_model.response.body[0].jobId} + next: return_ok + +assign_fail_response: + assign: + fail_format_res: + modelId: ${model_id} + operationSuccessful: false + message: "Failed to queue training job" + next: return_bad_request + +return_ok: + status: 200 + return: ${success_format_res} + next: end + +return_bad_request: + status: 400 + return: ${fail_format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/datasets/delete.yml b/DSL/Ruuter.private/global-classifier/POST/datasets/delete.yml new file mode 100644 index 00000000..cec2b627 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/datasets/delete.yml @@ -0,0 +1,48 @@ +declaration: + call: declare + version: 0.1 + description: "Delete all datasets with a dataset_version_id and then delete the dataset version" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: datasetVersionId + type: number + description: "dataset_version_id to delete (required)" + next: deleteDatasets + +deleteDatasets: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/delete-datasets-by-version" + body: + datasetVersionId: ${Number(incoming.body.datasetVersionId)} + result: deleteDatasetsResult + next: deleteDatasetVersion + +deleteDatasetVersion: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/delete-dataset-version" + body: + datasetVersionId: ${Number(incoming.body.datasetVersionId)} + result: deleteVersionResult + next: checkDeleteResult + +checkDeleteResult: + switch: + - condition: ${!deleteVersionResult || !deleteVersionResult.response.body || deleteVersionResult.response.body.length === 0} + next: return_delete_failed + next: return_success + +return_success: + return: ${deleteVersionResult.response.body[0]} + status: 200 + next: end + +return_delete_failed: + return: " Error : Delete failed or dataset_version_id not found" + status: 400 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/datasets/update.yml b/DSL/Ruuter.private/global-classifier/POST/datasets/update.yml new file mode 100644 index 00000000..bb852c1b --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/datasets/update.yml @@ -0,0 +1,84 @@ +declaration: + call: declare + version: 0.1 + description: "Bulk update and delete datasets" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: updatedRowsLength + type: number + description: "Number of rows to update (required)" + - field: deletedRowsLength + type: number + description: "Number of rows to delete (required)" + - field: updatedDataItems + type: array + description: "Array of dataset rows to update" + - field: deletedRows + type: array + description: "Array of item_ids to delete" + next: extractRequestedData + +extractRequestedData: + assign: + updated_rows_length: ${incoming.body.updatedRowsLength} + deleted_rows_length: ${incoming.body.deletedRowsLength} + next: checkIfUpdatesNeeded + +checkIfUpdatesNeeded: + switch: + - condition: ${updated_rows_length > 0} + then: setUpdateIndex + next: checkIfDeletesNeeded + +setUpdateIndex: + assign: + updateIndex: 0 + next: updateStep + +updateStep: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-dataset" + body: + agencyName: ${incoming.body.updatedDataItems[updateIndex].agencyName} + agencyId: ${incoming.body.updatedDataItems[updateIndex].agencyId} + dataItem: ${incoming.body.updatedDataItems[updateIndex].dataItem} + itemId: ${incoming.body.updatedDataItems[updateIndex].itemId} + maxRecursions: 1000 + next: checkIfMoreUpdates + result: updateResult + +checkIfMoreUpdates: + switch: + - condition: ${updateIndex + 1 < updated_rows_length} + then: incrementUpdateIndex + next: checkIfDeletesNeeded + +incrementUpdateIndex: + assign: + updateIndex: ${updateIndex + 1} + next: updateStep + +checkIfDeletesNeeded: + switch: + - condition: ${deleted_rows_length > 0} + then: deleteRows + next: returnResult + +deleteRows: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/delete-dataset-records" + body: + itemIds: ${incoming.body.deletedRows} + result: deleteResult + next: returnResult + +returnResult: + return: "Datasets updated and deleted successfully" + status: 200 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/testmodel/classify.yml b/DSL/Ruuter.private/global-classifier/POST/testmodel/classify.yml new file mode 100644 index 00000000..5dfb7001 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/testmodel/classify.yml @@ -0,0 +1,93 @@ +declaration: + call: declare + version: 0.1 + description: "Test model classification endpoint for development and testing" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: modelId + type: string + description: "Identifier of the model to use for classification" + - field: text + type: string + description: "Text text to be classified" + +extractClassificationData: + assign: + modelId: ${incoming.body.modelId} + text: ${incoming.body.text} + startTime: ${Date.now()} + next: validate_input + +validate_input: + switch: + - condition: ${!modelId} + next: return_validation_error + - condition: ${!text} + next: return_validation_error + next: prepare_inference_payload + +prepare_inference_payload: + assign: + ensembleModelName: ${modelId}-classifier-ensemble + next: call_triton_inference + +call_triton_inference: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER]/v2/models/${ensembleModelName}/infer" + body: + inputs: + - name: "TEXT" + datatype: "BYTES" + shape: [1, 1] + data: + - ${text} + headers: + Content-Type: "application/json" + result: inference_result + next: process_classification_result + error: return_inference_error + +process_classification_result: + assign: + # Parse the JSON string output from Triton to get agency classification array + rawOutput: ${inference_result.response.body.outputs[0].data[0]} + parsedOutput: ${JSON.parse(rawOutput)} + next: log_testing_inference_result + +log_testing_inference_result: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-testing-inference-log" + body: + modelId: ${modelId} + text: ${text} + inferenceTimeMs: ${Date.now() - startTime} + parsedOutput: ${JSON.stringify(parsedOutput)} + result: log_result + next: return_classification_success + error: return_log_inference_error + +return_classification_success: + status: 200 + return: ${parsedOutput} + next: end + +return_log_inference_error: + status: 500 + return: "Error logging testing inference result in PostgresDB" + next: end + +return_validation_error: + status: 400 + return: "Invalid payload. modelId and text are required." + next: end + +return_inference_error: + status: 500 + return: "Classification service temporarily unavailable" + next: end \ No newline at end of file diff --git a/DSL/Ruuter.private/global-classifier/POST/testmodel/load.yml b/DSL/Ruuter.private/global-classifier/POST/testmodel/load.yml new file mode 100644 index 00000000..bb608fef --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/POST/testmodel/load.yml @@ -0,0 +1,133 @@ +declaration: + call: declare + version: 0.1 + description: "Load a model into NVIDIA Triton test server for testing" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: modelId + type: string + description: "Identifier of the model to load into Triton server" + +extractRequestData: + assign: + modelId: ${incoming.body.modelId} + next: validateInput + +validateInput: + switch: + - condition: ${!modelId} + next: return_validation_error + next: prepareModelNames + +prepareModelNames: + assign: + # Generate the model names based on the modelId + preProcessingModel: ${modelId}-pre-processing + textClassifierModel: ${modelId}-text-classifier + postProcessingModel: ${modelId}-post-processing + ensembleModel: ${modelId}-classifier-ensemble + next: loadPreProcessingModel + +loadPreProcessingModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER]/v2/repository/models/${preProcessingModel}/load" + timeout: 600000 + result: preProcessingLoadResult + next: checkPreProcessingLoad + error: return_model_load_error + +checkPreProcessingLoad: + switch: + - condition: ${preProcessingLoadResult.response.statusCodeValue !== 200 && preProcessingLoadResult.response.statusCodeValue !== 201} + next: return_model_load_error + next: loadTextClassifierModel + +loadTextClassifierModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER]/v2/repository/models/${textClassifierModel}/load" + timeout: 600000 + result: textClassifierLoadResult + next: checkTextClassifierLoad + error: return_model_load_error + +checkTextClassifierLoad: + switch: + - condition: ${textClassifierLoadResult.response.statusCodeValue !== 200 && textClassifierLoadResult.response.statusCodeValue !== 201} + next: return_model_load_error + next: loadPostProcessingModel + +loadPostProcessingModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER]/v2/repository/models/${postProcessingModel}/load" + timeout: 600000 + result: postProcessingLoadResult + next: checkPostProcessingLoad + error: return_model_load_error + +checkPostProcessingLoad: + switch: + - condition: ${postProcessingLoadResult.response.statusCodeValue !== 200 && postProcessingLoadResult.response.statusCodeValue !== 201} + next: return_model_load_error + next: loadEnsembleModel + +loadEnsembleModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER]/v2/repository/models/${ensembleModel}/load" + timeout: 600000 + result: ensembleLoadResult + next: checkEnsembleLoad + error: return_model_load_error + +checkEnsembleLoad: + switch: + - condition: ${ensembleLoadResult.response.statusCodeValue !== 200 && ensembleLoadResult.response.statusCodeValue !== 201} + next: return_model_load_error + next: verifyModelReady + +verifyModelReady: + call: http.get + args: + url: "[#GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER]/v2/models/${ensembleModel}/ready" + timeout: 600000 + result: readyCheck + next: checkModelReady + error: return_model_verification_error + +checkModelReady: + switch: + - condition: ${readyCheck.response.statusCodeValue !== 200} + next: return_model_verification_error + next: return_success + +return_success: + status: 200 + return: "Model loaded successfully into Triton test server" + next: end + +return_validation_error: + status: 400 + return: "Invalid request - modelId is required" + next: end + +return_server_unavailable: + status: 503 + return: "Server unavailable - Triton test server is not ready or unavailable" + next: end + +return_model_load_error: + status: 500 + return: "Model load failed - Failed to load one or more model components into Triton server" + next: end + +return_model_verification_error: + status: 500 + return: "Model verification failed - Model loaded but verification failed, ensemble model not ready" + next: end diff --git a/DSL/Ruuter.private/global-classifier/TEMPLATES/check-user-authority-admin.yml b/DSL/Ruuter.private/global-classifier/TEMPLATES/check-user-authority-admin.yml new file mode 100644 index 00000000..8864c184 --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/TEMPLATES/check-user-authority-admin.yml @@ -0,0 +1,52 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'CHECK-USER-AUTHORITY'" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + headers: + - field: cookie + type: string + description: "Cookie field" + +get_cookie_info: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TIM]/jwt/custom-jwt-userinfo" + contentType: plaintext + headers: + cookie: ${incoming.headers.cookie} + plaintext: "customJwtCookie" + result: res + next: check_cookie_info_response + +check_cookie_info_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_user_authority + next: return_bad_request + +check_user_authority: + switch: + - condition: ${res.response.body === null} + next: return_unauthorized + - condition: ${res.response.body.authorities.includes("ROLE_ADMINISTRATOR")} + next: return_authorized + next: return_unauthorized + +return_authorized: + return: ${res.response.body} + next: end + +return_unauthorized: + status: 401 + return: false + next: end + +return_bad_request: + status: 400 + return: false + next: end diff --git a/DSL/Ruuter.private/global-classifier/TEMPLATES/check-user-authority.yml b/DSL/Ruuter.private/global-classifier/TEMPLATES/check-user-authority.yml new file mode 100644 index 00000000..80708e9b --- /dev/null +++ b/DSL/Ruuter.private/global-classifier/TEMPLATES/check-user-authority.yml @@ -0,0 +1,52 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'CHECK-USER-AUTHORITY'" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + headers: + - field: cookie + type: string + description: "Cookie field" + +get_cookie_info: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TIM]/jwt/custom-jwt-userinfo" + contentType: plaintext + headers: + cookie: ${incoming.headers.cookie} + plaintext: "customJwtCookie" + result: res + next: check_cookie_info_response + +check_cookie_info_response: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: check_user_authority + next: return_bad_request + +check_user_authority: + switch: + - condition: ${res.response.body === null} + next: return_unauthorized + - condition: ${res.response.body.authorities.includes("ROLE_ADMINISTRATOR") || res.response.body.authorities.includes("ROLE_MODEL_TRAINER")} + next: return_authorized + next: return_unauthorized + +return_authorized: + return: ${res.response.body} + next: end + +return_unauthorized: + status: 401 + return: false + next: end + +return_bad_request: + status: 400 + return: false + next: end diff --git a/DSL/Ruuter.public/global-classifier/POST/agencies/add.yml b/DSL/Ruuter.public/global-classifier/POST/agencies/add.yml new file mode 100644 index 00000000..2fb30a36 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/agencies/add.yml @@ -0,0 +1,92 @@ +declaration: + call: declare + version: 0.1 + description: "Add multiple agencies to the integrated_agencies table" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agencies + type: array + description: "Body field 'agencies' as an array of agency objects" + +extractRequestData: + assign: + agencies: ${incoming.body.agencies || []} + addedAgencies: 0 + currentIndex: 0 + +processAgencies: + switch: + - condition: ${currentIndex >= agencies.length} + next: return_final_result + next: processCurrentAgency + +processCurrentAgency: + assign: + currentAgency: ${agencies[currentIndex]} + agencyId: ${agencies[currentIndex].agencyId} + agencyName: ${agencies[currentIndex].agencyName} + next: checkAgencyExists + +checkAgencyExists: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-agency" + body: + agencyId: ${agencyId} + result: agency_res + next: evaluateAgencyExistence + +evaluateAgencyExistence: + switch: + - condition: "${agency_res.response.body.length > 0}" + next: moveToNextAgency + next: addAgency + +addAgency: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-agency" + body: + agencyId: ${agencyId} + agencyName: ${agencyName} + groupKey: "" + isLatest: true + deploymentStatus: "undeployed" + isEnabled: false + agencyDataHash: "" + enableAllowed: false + lastModelTrained: "" + lastUpdatedTimestamp: ${new Date().toISOString()} + lastTrainedTimestamp: "1970-01-01T00:00:00.000Z" + syncStatus: "Unavailable_in_CKB" + createdAt: ${new Date().toISOString()} + result: add_agency_res + next: addAgencySuccessResult + +addAgencySuccessResult: + assign: + addedAgencies: ${addedAgencies+1} + next: moveToNextAgency + +moveToNextAgency: + assign: + currentIndex: ${currentIndex + 1} + next: processAgencies + +return_final_result: + switch: + - condition: "${addedAgencies === 0}" + next: return_no_agencies_added + next: return_added_agencies_count + +return_added_agencies_count: + return: "${addedAgencies} agencies added successfully." + next: end + +return_no_agencies_added: + return: "No new agencies were added." + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/agencies/data/generation.yml b/DSL/Ruuter.public/global-classifier/POST/agencies/data/generation.yml new file mode 100644 index 00000000..049fbe86 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/agencies/data/generation.yml @@ -0,0 +1,70 @@ +declaration: + call: declare + version: 0.1 + description: "Update sync_status and is_enabled for each agency in the array" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agencies + type: array + description: "Array of agencies with agencyId and syncStatus" + - field: datasetId + type: string + - field: generationStatus + type: string + +extractRequestData: + assign: + agencies: ${incoming.body.agencies || []} + datasetId: ${incoming.body.datasetId || ''} + generationStatus: ${incoming.body.generationStatus || ''} + currentIndex: 0 + updatedAgencies: [] + +processAgencies: + switch: + - condition: ${currentIndex >= agencies.length} + next: updateDatasetStatus + next: updateCurrentAgency + +updateCurrentAgency: + assign: + currentAgency: ${agencies[currentIndex]} + agencyId: ${currentAgency.agencyId} + syncStatus: ${currentAgency.syncStatus} + isEnabled: ${syncStatus === 'Synced_with_CKB'} + next: updateAgency + +updateAgency: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-agency-sync-status" + body: + agencyId: ${String(agencyId)} + syncStatus: ${syncStatus} + isEnabled: ${isEnabled} + result: updateResult + next: processNextAgency + +processNextAgency: + assign: + updatedAgencies: ${updatedAgencies.concat([agencyId])} + currentIndex: ${currentIndex + 1} + next: processAgencies + +updateDatasetStatus: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-data-generation-status" + body: + datasetId: ${String(datasetId)} + generationStatus: ${generationStatus} + result: datasetUpdateResult + next: return_result + +return_result: + return: "${agencies.length} agencies updated successfully." + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/agencies/data/resync.yml b/DSL/Ruuter.public/global-classifier/POST/agencies/data/resync.yml new file mode 100644 index 00000000..ef9b822f --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/agencies/data/resync.yml @@ -0,0 +1,68 @@ +declaration: + call: declare + version: 0.1 + description: "Resync new data from CKB" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agencyId + type: string + description: "Agency ID" + +extractInput: + assign: + agencyId: ${incoming.body.agencyId} + +importAgencyData: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PRIVATE]/ckb/agency-data-import" + headers: + cookie: ${incoming.headers.cookie} + body: + agencyIds: ${agencyId} + result: importResult + next: updateAgencySyncStatus + +updateAgencySyncStatus: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-agency-data" + body: + agencyId: ${agencyId} + syncStatus: "Resync_in_progress_with_CKB" + agencyDataHash: ${importResult.response.body.response.find(a => a.agencyId == agencyId)?.agencyDataHash || ""} + result: updateResult + next: CreateDatasetVersion + +CreateDatasetVersion: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/datasets/version/minor" + result: datasetResponse + next: forwardToFormatting + +forwardToFormatting: + switch: + - condition: ${datasetResponse.response.body.response.length === 0} + next: end + next: initiateDataGeneration + +initiateDataGeneration: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/data/generate" + body: + presignedUrls: ${importResult.response.body.response} + datasetId: ${datasetResponse.response.body.response[0].id} + majorVersion: ${datasetResponse.response.body.response[0].major} + minorVersion: ${datasetResponse.response.body.response[0].minor} + result: dataGenerationResponse + next: returnImportResult + +returnImportResult: + return: ${dataGenerationResponse.response.body} + next: end diff --git a/DSL/Ruuter.public/global-classifier/POST/agencies/data/sync.yml b/DSL/Ruuter.public/global-classifier/POST/agencies/data/sync.yml new file mode 100644 index 00000000..e21128f9 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/agencies/data/sync.yml @@ -0,0 +1,202 @@ +declaration: + call: declare + version: 0.1 + description: "Fetch new agencies from CentOps, retrieve their data, and add them to the database" + method: post + accepts: json + returns: json + namespace: global-classifier + + +fetchAgenciesFromCentops: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/centops/new-agencies" + result: centopsResponse + log: "Fetched agencies from CentOps: ${centopsResponse}" + next: checkAgenciesExist + +checkAgenciesExist: + switch: + - condition: ${!centopsResponse.response.body.response || centopsResponse.response.body.response.length === 0} + next: returnNoAgenciesFound + next: getNewAgencies + +getNewAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-all-agencies" + result: allAgencies + next: compareAgencies + +compareAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/extract_unavailable_agencies" + headers: + type: json + body: + gcAgencies: ${allAgencies.response.body} + centopsAgencies: ${centopsResponse.response.body.response} + result: newAgencies + next: addAgencies + +checkNewAgenciesAvilability: + switch: + - condition: ${!newAgencies.response.body.agencies || newAgencies.response.body.agencies.length === 0} + next: returnNoAgenciesFound + next: addAgencies + +addAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/agencies/add" + body: + agencies: ${newAgencies.response.body.agencies} + result: addAgenciesResult + next: getUnavailableAgencyIds + +getUnavailableAgencyIds: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-data-unavailable-agencies" + result: unavailableAgencyIds + next: checkAgenciesWithNoData + +checkAgenciesWithNoData: + switch: + - condition: ${!unavailableAgencyIds || unavailableAgencyIds.length === 0} + next: returnNoAgenciesFound + next: checkAgencyData + +checkAgencyData: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/ckb/agency-data-exist" + body: + agencyIds: ${unavailableAgencyIds.response.body.map(a => a.agencyId).join(' ')} + result: agencyMetadata + next: filterAvailableAgencies + +filterAvailableAgencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/get-data-available-agencies" + headers: + type: json + body: + agencies: ${agencyMetadata.response.body.response.filter(a => a.isDataAvailable)} + result: availableAgencyIds + next: checkAvailableAgencies + +checkAvailableAgencies: + switch: + - condition: ${!availableAgencyIds.response.body || availableAgencyIds.response.body.length === 0} + next: formatNoAgenciesResults + next: importAgencyData + +importAgencyData: + call: http.post + log: "Importing agency data for IDs: ${agencyIdsString.response.body}" + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/ckb/agency-data-import" + body: + agencyIds: ${availableAgencyIds.response.body.join(' ')} + result: importResult + next: initializeUpdateProcess + +initializeUpdateProcess: + assign: + agencyUpdateIndex: 0 + totalAgencies: ${availableAgencyIds.response.body.length} + updatedAgencies: 0 + log: "Starting update for ${totalAgencies} agencies" + next: processNextAgency + +processNextAgency: + switch: + - condition: ${agencyUpdateIndex >= totalAgencies} + next: CreateDatasetVersion + next: updateCurrentAgency + +updateCurrentAgency: + assign: + currentAgencyId: ${availableAgencyIds.response.body[agencyUpdateIndex]} + log: "Updating agency ${agencyUpdateIndex + 1} of ${totalAgencies}: ${currentAgencyId}" + next: updateAgencySyncStatus + +updateAgencySyncStatus: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-agency-data" + body: + agencyId: ${currentAgencyId} + syncStatus: "Sync_in_progress_with_CKB" + agencyDataHash: ${importResult.response.body.response.find(a => a.agencyId == currentAgencyId)?.agencyDataHash || ""} + result: updateResult + next: incrementUpdateCounter + +incrementUpdateCounter: + assign: + agencyUpdateIndex: ${agencyUpdateIndex + 1} + updatedAgencies: ${updatedAgencies + 1} + next: processNextAgency + +CreateDatasetVersion: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/datasets/version/major" + result: datasetResponse + next: forwardToFormatting + +forwardToFormatting: + switch: + - condition: ${datasetResponse.response.body.response.length === 0} + next: end + next: initiateDataGeneration + +initiateDataGeneration: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/data/generate" + body: + datasetId: ${datasetResponse.response.body.response[0].id} + majorVersion: ${datasetResponse.response.body.response[0].major} + minorVersion: ${datasetResponse.response.body.response[0].minor} + presignedUrls: ${importResult.response.body.response} + result: dataGenerationResponse + next: returnImportResult + +returnImportResult: + return: ${dataGenerationResponse.response.body} + next: end + +formatNoAgenciesResults: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/agencies-sync-summary" + headers: + type: json + body: + totalFound: ${unavailableAgencyIds.response.body.length} + addResult: ${addAgenciesResult.response.body.response} + importResult: "No data imported" + result: finalResult + next: returnResult + +returnResult: + return: ${finalResult.response.body} + next: end + +returnNoAgenciesFound: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/no-new-agencies" + headers: + type: json + result: noAgenciesResult + next: returnEmptyResult + +returnEmptyResult: + return: ${noAgenciesResult.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/agencies/data/update.yml b/DSL/Ruuter.public/global-classifier/POST/agencies/data/update.yml new file mode 100644 index 00000000..4f338d9f --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/agencies/data/update.yml @@ -0,0 +1,94 @@ +declaration: + call: declare + version: 0.1 + description: "Check if new data_url is available in mock_ckb for agencies synced with CKB" + method: post + accepts: json + returns: json + namespace: global-classifier + +checkForNewMockCkbData: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-synced-agency-ids" + result: syncedAgencies + next: checkForSyncedAgencies + +checkForSyncedAgencies: + switch: + - condition: ${syncedAgencies.response.body.length > 0} + next: fetchMockCkbHashes + next: returnNoAgencies + +fetchMockCkbHashes: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RUUTER_PUBLIC]/ckb/agency-data-import" + body: + agencyIds: ${syncedAgencies.response.body.map(a => a.agencyId).join(' ')} + result: ckbData + next: initializeResyncCheck + +initializeResyncCheck: + assign: + ckbIndex: 0 + resyncNeededAgencies: 0 + totalCkb: ${ckbData.response.body.response.length} + next: processNextCkbAgency + +processNextCkbAgency: + switch: + - condition: ${ckbIndex >= totalCkb} + next: returnResult + next: checkAgencyHash + +checkAgencyHash: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-agency-datahash" + body: + agencyId: ${ckbData.response.body.response[ckbIndex].agencyId} + result: integratedAgency + next: compareHashes + +compareHashes: + assign: + ckbAgency: ${ckbData.response.body.response[ckbIndex]} + integratedHash: ${integratedAgency.response.body[0].agencyDataHash} + ckbHash: ${ckbAgency.agencyDataHash} + needsResync: ${integratedHash !== ckbHash} + next: updateIfNeeded + +updateIfNeeded: + switch: + - condition: ${needsResync} + next: updateResyncStatus + next: incrementCkbIndex + +updateResyncStatus: + call: http.post + assign: + resyncNeededAgencies: ${resyncNeededAgencies + 1} + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-agency-sync-status" + body: + agencyId: ${ckbAgency.agencyId} + syncStatus: "Resync_needed_with_CKB" + isEnabled: false + result: updateResult + next: incrementCkbIndex + +incrementCkbIndex: + assign: + ckbIndex: ${ckbIndex + 1} + next: processNextCkbAgency + +returnNoAgencies: + return: "No eligible agencies found for resync" + status: 404 + next: end + +returnResult: + return: "Agencies updated with status - Resync_needed_with_CKB " + status: 200 + next: end diff --git a/DSL/Ruuter.public/global-classifier/POST/auth/login.yml b/DSL/Ruuter.public/global-classifier/POST/auth/login.yml new file mode 100644 index 00000000..d0809827 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/auth/login.yml @@ -0,0 +1,79 @@ +declaration: + call: declare + version: 0.1 + description: "Decription placeholder for 'LOGIN'" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: login + type: string + description: "Body field 'login'" + - field: password + type: string + description: "Body field 'password'" + +extractRequestData: + assign: + login: ${incoming.body.login} + password: ${incoming.body.password} + next: getUserWithRole + +getUserWithRole: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-user-with-roles" + body: + login: ${login} + password: ${password} + result: user_result + next: check_user_result + +check_user_result: + switch: + - condition: "${user_result.response.body.length > 0}" + next: get_session_length + next: return_user_not_found + +get_session_length: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-configuration" + body: + key: "session_length" + result: session_result + next: generate_cookie + +generate_cookie: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TIM]/jwt/custom-jwt-generate" + body: + JWTName: "customJwtCookie" + expirationInMinutes: ${session_result.response.body[0]?.value ?? '120'} + content: ${user_result.response.body[0]} + result: cookie_result + next: assign_cookie + +assign_cookie: + assign: + setCookie: + customJwtCookie: ${cookie_result.response.body.token} + Domain: "[#DOMAIN]" + Secure: true + HttpOnly: true + SameSite: "Lax" + next: return_value + +return_value: + headers: + Set-Cookie: ${setCookie} + return: ${cookie_result.response.body.token} + next: end + +return_user_not_found: + status: 400 + return: "User Not Found" + next: end diff --git a/DSL/Ruuter.public/global-classifier/POST/centops/new-agencies.yml b/DSL/Ruuter.public/global-classifier/POST/centops/new-agencies.yml new file mode 100644 index 00000000..c3a265f8 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/centops/new-agencies.yml @@ -0,0 +1,19 @@ +declaration: + call: declare + version: 0.1 + description: "Get agencies synchronized after a specific timestamp" + method: post + accepts: json + returns: json + namespace: global-classifier + +fetch_agencies: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/mock-get-agencies-from-centops" + result: agency_data + next: return_result + +return_result: + return: ${agency_data.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/chats/classify.yml b/DSL/Ruuter.public/global-classifier/POST/chats/classify.yml index bcbb9725..b71cfb28 100644 --- a/DSL/Ruuter.public/global-classifier/POST/chats/classify.yml +++ b/DSL/Ruuter.public/global-classifier/POST/chats/classify.yml @@ -1,7 +1,7 @@ declaration: call: declare version: 0.1 - description: "Callback endpoint for Global Classifier to indicate routing decisions" + description: "Primary text classification endpoint for Buerokratt chatbot services" method: post accepts: json returns: json @@ -10,64 +10,118 @@ declaration: body: - field: chatId type: string - description: "Unique identifier of the chat session" - - field: inferenceId + description: "Unique identifier for the chat session" + - field: message type: string - description: "Unique identifier of the prediction attempt" - - field: targetAgencies - type: json - description: "Array of agency IDs with confidence scores for routing" + description: "Text message to be classified" + - field: url + type: string + description: "URL context for the chat session" -extractDecisionData: +extractClassificationData: assign: chatId: ${incoming.body.chatId} - inferenceId: ${incoming.body.inferenceId} - targetAgencies: ${incoming.body.targetAgencies ?? []} - next: validate_decision + message: ${incoming.body.message} + url: ${incoming.body.url ?? ""} + startTime: ${Date.now()} + next: validate_input -validate_decision: +validate_input: switch: - condition: ${!chatId} next: return_validation_error - next: log_classification + - condition: ${!message} + next: return_validation_error + next: get_production_model -return_validation_error: - status: 400 - return: "Invalid classification payload. Chat ID is required." - next: end +get_production_model: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-production-model-id" + body: {} + result: model_result + next: check_model_availability + error: return_model_error -log_classification: - log: "Classification received for the chat ${chatId}: ${JSON.stringify(targetAgencies)}" - next: store_classification_data +check_model_availability: + switch: + - condition: ${!model_result.response.body || model_result.response.body.length == 0} + next: return_no_model_error + assign: + modelId: ${model_result.response.body[0].modelId} + next: prepare_inference_payload + +prepare_inference_payload: + assign: + ensembleModelName: ${modelId}-classifier-ensemble + next: call_triton_inference + +call_triton_inference: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_PRODUCTION_ENV_MODEL_SERVER]/v2/models/${ensembleModelName}/infer" + body: + inputs: + - name: "TEXT" + datatype: "BYTES" + shape: [1, 1] + data: + - ${message} + headers: + Content-Type: "application/json" + result: inference_result + next: process_classification_result + error: return_inference_error + +process_classification_result: + assign: + # Parse the JSON string output from Triton to get agency classification array + rawOutput: ${inference_result.response.body.outputs[0].data[0]} + parsedOutput: ${JSON.parse(rawOutput)} + next: log_inference_result -store_classification_data: +log_inference_result: call: http.post args: - url: "[#GLOBAL_CLASSIFIER_RESQL]/store-classification" + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-inference-log" body: chatId: ${chatId} - inferenceId: ${inferenceId} - targetAgencies: ${JSON.stringify(targetAgencies)} - result: store_result - error: handle_storage_error - next: check_storage_result + url: ${url} + modelId: ${modelId} + inferenceTimeMs: ${Date.now() - startTime} + parsedOutput: ${JSON.stringify(parsedOutput)} + result: log_result + next: return_classification_success + error: return_log_inference_error -check_storage_result: - switch: - - condition: ${200 <= store_result.response.statusCodeValue && store_result.response.statusCodeValue < 300} - next: return_success - next: handle_storage_error +return_classification_success: + status: 200 + return: ${parsedOutput} + next: end -handle_storage_error: - log: "Error storing classification data: ${store_result.error}" - next: return_storage_error -return_storage_error: +return_log_inference_error: status: 500 - return: "Failed to store classification data. Please try again later." + return: "Error logging inference result in PostgresDB" next: end - -return_success: - status: 200 - return: "Classification received and stored successfully" + + +return_validation_error: + status: 400 + return: "Invalid payload. chatId and message are required." + next: end + +return_no_model_error: + status: 503 + return: "No production model available for classification" + next: end + +return_model_error: + status: 500 + return: "Failed to retrieve production model configuration" + next: end + +return_inference_error: + status: 500 + return: "Classification service temporarily unavailable" next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/ckb/agency-data-exist.yml b/DSL/Ruuter.public/global-classifier/POST/ckb/agency-data-exist.yml new file mode 100644 index 00000000..94cf506f --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/ckb/agency-data-exist.yml @@ -0,0 +1,33 @@ +declaration: + call: declare + version: 0.1 + description: "Get agency data information by agency IDs" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agencyIds + type: array + description: "Array of unique institution IDs" + +extractRequestData: + assign: + agencyIds: ${incoming.body.agencyIds || []} + log: "Received request for agency data: ${agencyIds}" + +check_data_availability: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/check-agency-data-availability" + headers: + type: json + body: + agencyIds: ${agencyIds} + result: agency_data_info + next: return_result + +return_result: + return: ${agency_data_info.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/ckb/agency-data-import.yml b/DSL/Ruuter.public/global-classifier/POST/ckb/agency-data-import.yml new file mode 100644 index 00000000..864f8e00 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/ckb/agency-data-import.yml @@ -0,0 +1,33 @@ +declaration: + call: declare + version: 0.1 + description: "Get agency data information by agency IDs" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: agencyIds + type: array + description: "Array of unique institution IDs" + +extractRequestData: + assign: + agencyIds: ${incoming.body.agencyIds || []} + log: "Received request for agency data: ${agencyIds}" + +get_agency_data: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/mock-get-data-from-ckb" + headers: + type: json + body: + agencyIds: ${agencyIds} + result: agency_data_info + next: return_result + +return_result: + return: ${agency_data_info.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/data/callback.yml b/DSL/Ruuter.public/global-classifier/POST/data/callback.yml new file mode 100644 index 00000000..e069deab --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/data/callback.yml @@ -0,0 +1,120 @@ +declare: + call: declare + version: 0.1 + description: "Callback endpoint for dataset generation status logging" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: task_id + type: string + description: "UUID string identifying the task" + - field: status + type: string + description: "Task completion status (success, failed, etc.)" + - field: message + type: string + description: "Status message or debug information" + - field: filePath + type: string + description: "Path to the generated dataset file" + - field: results + type: array + description: "List of agency IDs for which the dataset generation was completed" + +log_callback_received: + log: "📞 Dataset generation callback received - Task ID: ${incoming.body.task_id}, Status: ${incoming.body.status}, File Path: ${incoming.body.filePath}" + next: extract_callback_data + +extract_callback_data: + assign: + task_id: ${incoming.body.task_id} + status: ${incoming.body.status} + message: ${incoming.body.message} + file_path: ${incoming.body.filePath} + results: ${encodeURIComponent(JSON.stringify(incoming.body.results))} + next: log_detailed_info + +log_detailed_info: + log: "📋 Callback Details - Task: ${task_id}, Status: ${status}, Message: ${message}, filePath: ${file_path}, results: ${results}" + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${task_id !== null && status !== null && message !== null && file_path !== null && results !== null} + next: determine_status_response + next: return_incorrect_request + +determine_status_response: + switch: + - condition: ${status == "success"} + next: handle_success_callback + - condition: ${status == "failed" || status == "error"} + next: handle_failure_callback + - condition: true + next: handle_unknown_status + +handle_success_callback: + assign: + format_res: + task_id: ${task_id} + status: "received" + message: "Success callback processed successfully" + next: log_cron_manager_execution + +log_cron_manager_execution: + log: "Executing Cron Manager for callback formatting - Task ID: ${task_id}, File Path: ${file_path}, Results: ${results}" + next: execute_cron_manager + +execute_cron_manager: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_CRON_MANAGER]/execute/callback_formatter/callback_format" + query: + taskId: ${task_id} + filePath: ${file_path} + results: ${results} + result: cron_res + next: assign_success_cron_response + +assign_success_cron_response: + assign: + format_success_cron_response_res: + task_id: ${task_id} + message: "Callback formatted started" + operationSuccessful: true + next: return_success + +handle_failure_callback: + assign: + format_failure_callback_res: + task_id: ${task_id} + status: "received" + message: "Failure callback processed" + next: return_failure_details + +handle_unknown_status: + assign: + format_unknown_status_res: + task_id: ${task_id} + status: "received" + message: "Unknown status callback processed" + next: return_unknown_status + +return_failure_details: + status: 500 + return: ${format_failure_callback_res} + +return_unknown_status: + status: 200 + return: ${format_unknown_status_res} + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + +return_success: + status: 200 + return: ${format_success_cron_response_res} \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/data/generate.yml b/DSL/Ruuter.public/global-classifier/POST/data/generate.yml new file mode 100644 index 00000000..a8de46fe --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/data/generate.yml @@ -0,0 +1,152 @@ +declaration: + call: declare + version: 0.1 + description: "Dataset Generation for Global Classifier" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: datasetId + type: string + - field: majorVersion + description: "Major version number for the dataset generation request" + - field: minorVersion + description: "Minor version number for the dataset generation request" + - field: presignedUrls + type: json + description: "Request body containing dataset generation details with set of signed URLs and agencyId" + +log_request_start: + log: "Received dataset generation request{ datasetId: ${incoming.body.datasetId}, URLs count: ${incoming.body.presignedUrls?.length || 0} }" + next: validate_input + +validate_input: + switch: + - condition: ${incoming.body.datasetId == null} + next: handle_missing_dataset_id + - condition: ${incoming.body.presignedUrls == null} + next: handle_missing_urls + - condition: ${Array.isArray(incoming.body.presignedUrls) && incoming.body.presignedUrls.length == 0} + next: handle_empty_urls + next: extract_request_data + +handle_missing_dataset_id: + assign: + error_response: + operationSuccessful: false + message: "datasetId field is required" + errorCode: "MISSING_REQUIRED_FIELD" + status: 400 + return: ${error_response} + +handle_missing_urls: + assign: + error_response: + operationSuccessful: false + message: "presignedUrls field is required" + errorCode: "MISSING_REQUIRED_FIELD" + status: 400 + return: ${error_response} + +handle_empty_urls: + assign: + error_response: + operationSuccessful: false + message: "presignedUrls cannot be empty" + errorCode: "EMPTY_URL_LIST" + status: 400 + return: ${error_response} + +extract_request_data: + assign: + dataset_id: ${JSON.stringify(incoming.body.datasetId)} + major_version: ${incoming.body.majorVersion} + minor_version: ${incoming.body.minorVersion} + url_list: ${incoming.body.presignedUrls} + encoded_urls: ${encodeURIComponent(JSON.stringify(url_list))} + next: log_request + +log_request: + log: "Dataset generation request received - Dataset ID: ${dataset_id}, URLs count: ${url_list.length}" + next: execute_cron_manager + +execute_cron_manager: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_CRON_MANAGER]/execute/dataset_pipeline_scheduler/dataset_pipeline_schedule" + query: + signedUrls: ${encoded_urls} + datasetId: ${dataset_id} + majorVersion: ${major_version} + minorVersion: ${minor_version} + result: cron_res + error: handle_cron_error + next: validate_cron_response + +handle_cron_error: + assign: + error_response: + operationSuccessful: false + message: "Failed to communicate with cron manager service" + errorCode: "CRON_SERVICE_UNAVAILABLE" + status: 503 + return: ${error_response} + +validate_cron_response: + log: "Cron manager response - Status: ${cron_res.response.statusCodeValue}" + switch: + - condition: ${cron_res.response.statusCodeValue >= 200 && cron_res.response.statusCodeValue < 300} + next: cron_job_format_res + - condition: ${cron_res.response.statusCodeValue >= 400 && cron_res.response.statusCodeValue < 500} + next: handle_client_error + - condition: ${cron_res.response.statusCodeValue >= 500} + next: handle_server_error + next: handle_unexpected_error + +handle_client_error: + assign: + error_response: + operationSuccessful: false + message: "Invalid request to cron manager" + errorCode: "INVALID_REQUEST" + statusCode: ${cron_res.response.statusCodeValue} + return: ${error_response} + status: 400 + next: end + +handle_server_error: + assign: + error_response: + operationSuccessful: false + message: "Cron manager service temporarily unavailable" + errorCode: "SERVICE_UNAVAILABLE" + statusCode: ${cron_res.response.statusCodeValue} + return: ${error_response} + status: 502 + next: end + +handle_unexpected_error: + assign: + error_response: + operationSuccessful: false + message: "Unexpected response from cron manager" + errorCode: "UNEXPECTED_ERROR" + return: ${error_response} + status: 500 + next: end + +cron_job_format_res: + assign: + format_res: + operationSuccessful: true + message: "Dataset generation job scheduled successfully" + next: return_success + +return_success: + status: 200 + headers: + Content-Type: application/json + return: ${format_res} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/datamodels/progress/create.yml b/DSL/Ruuter.public/global-classifier/POST/datamodels/progress/create.yml new file mode 100644 index 00000000..bc2ed525 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/datamodels/progress/create.yml @@ -0,0 +1,184 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CREATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: modelId + type: number + description: "Body field 'modelId'" + - field: modelName + type: string + description: "Body field 'modelName'" + - field: majorVersion + type: number + description: "Body field 'majorVersion'" + - field: minorVersion + type: number + description: "Body field 'minorVersion'" + - field: latest + type: boolean + description: "Body field 'latest'" + +extract_request_data: + assign: + model_id: ${incoming.body.modelId} + model_name: ${incoming.body.modelName} + major_version: ${incoming.body.majorVersion} + minor_version: ${incoming.body.minorVersion} + latest: ${incoming.body.latest} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${model_id !== null || major_version !=null || minor_version !==null} + next: get_data_model_by_id + next: return_incorrect_request + +get_data_model_by_id: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-data-model-by-id" + body: + model_id: ${model_id} + result: res_model + next: check_data_model_status + +check_data_model_status: + switch: + - condition: ${200 <= res_model.response.statusCodeValue && res_model.response.statusCodeValue < 300} + next: check_data_model_exist + next: assign_fail_response + +check_data_model_exist: + switch: + - condition: ${res_model.response.body.length>0} + next: get_random_string_cookie + next: return_model_not_found + +get_random_string_cookie: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/return_random_string" + headers: + type: json + result: res_cookie + next: check_random_string_status + +check_random_string_status: + switch: + - condition: ${200 <= res_cookie.response.statusCodeValue && res_cookie.response.statusCodeValue < 300} + next: assign_csrf_cookie + next: assign_fail_response + +assign_csrf_cookie: + assign: + csrf_cookie: ${'_csrf=' + res_cookie.response.body.randomHexString+';'} + next: create_data_model_progress_session + +create_data_model_progress_session: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-data-model-progress-session" + body: + model_id: ${model_id} + model_name: ${model_name} + major_version: ${major_version} + minor_version: ${minor_version} + latest: ${latest} + progress_percentage: 0 + training_progress_status: 'Initiating Training' + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_session_id + next: assign_fail_response + +assign_session_id: + assign: + session_id: ${res.response.body[0].id} + next: get_csrf_token + +get_csrf_token: + call: http.get + args: + url: "[#GLOBAL_CLASSIFIER_NOTIFICATIONS]/csrf-token" + headers: + cookie: ${csrf_cookie} + result: res_token + next: check_token_status + +check_token_status: + switch: + - condition: ${200 <= res_token.response.statusCodeValue && res_token.response.statusCodeValue < 300} + next: assign_csrf_token + next: assign_fail_response + +assign_csrf_token: + assign: + token: ${res_token.response.body.csrfToken} + next: update_progress + +update_progress: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_NOTIFICATIONS]/model/progress" + headers: + X-CSRF-Token: ${token} + cookie: ${csrf_cookie} + body: + sessionId: ${session_id} + progressPercentage: 0 + trainingStatus: 'Initiating Training' + trainingMessage: '' + result: res_node + next: check_node_server_status + +check_node_server_status: + switch: + - condition: ${200 <= res_node.response.statusCodeValue && res_node.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + sessionId: '${session_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + sessionId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end + +return_model_not_found: + status: 404 + return: "Model Not Found" + next: end diff --git a/DSL/Ruuter.public/global-classifier/POST/datamodels/progress/delete.yml b/DSL/Ruuter.public/global-classifier/POST/datamodels/progress/delete.yml new file mode 100644 index 00000000..50428fa3 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/datamodels/progress/delete.yml @@ -0,0 +1,46 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'DELETE'" + method: post + accepts: json + returns: json + namespace: classifier + +delete_dataset_progress_sessions: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/delete-completed-data-model-progress-sessions" + result: res_sessions + next: check_status + +check_status: + switch: + - condition: ${200 <= res_sessions.response.statusCodeValue && res_sessions.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/DSL/Ruuter.public/global-classifier/POST/datamodels/progress/update.yml b/DSL/Ruuter.public/global-classifier/POST/datamodels/progress/update.yml new file mode 100644 index 00000000..52906592 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/datamodels/progress/update.yml @@ -0,0 +1,151 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'UPDATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: sessionId + type: number + description: "Body field 'sessionId'" + - field: trainingStatus + type: string + description: "Body field 'trainingStatus'" + - field: trainingMessage + type: string + description: "Body field 'trainingMessage'" + - field: progressPercentage + type: number + description: "Body field 'progressPercentage'" + - field: processComplete + type: boolean + description: "Body field 'processComplete'" + +extract_request_data: + assign: + session_id: ${incoming.body.sessionId} + training_status: ${incoming.body.trainingStatus} + training_message: ${incoming.body.trainingMessage} + progress_percentage: ${incoming.body.progressPercentage} + process_complete: ${incoming.body.processComplete} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${session_id !== null || !training_status || progress_percentage !==null} + next: get_random_string_cookie + next: return_incorrect_request + +get_random_string_cookie: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/return_random_string" + headers: + type: json + result: res_cookie + next: check_random_string_status + +check_random_string_status: + switch: + - condition: ${200 <= res_cookie.response.statusCodeValue && res_cookie.response.statusCodeValue < 300} + next: assign_csrf_cookie + next: assign_fail_response + +assign_csrf_cookie: + assign: + csrf_cookie: ${'_csrf=' + res_cookie.response.body.randomHexString+';'} + next: update_data_model_progress_session + +update_data_model_progress_session: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-data-model-progress-session" + body: + id: ${session_id} + training_progress_status: ${training_status} + progress_percentage: ${progress_percentage} + training_message: ${training_message} + process_complete: ${process_complete} + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: get_csrf_token + next: assign_fail_response + +get_csrf_token: + call: http.get + args: + url: "[#GLOBAL_CLASSIFIER_NOTIFICATIONS]/csrf-token" + headers: + cookie: ${csrf_cookie} + result: res_token + next: check_token_status + +check_token_status: + switch: + - condition: ${200 <= res_token.response.statusCodeValue && res_token.response.statusCodeValue < 300} + next: assign_csrf_token + next: assign_fail_response + +assign_csrf_token: + assign: + token: ${res_token.response.body.csrfToken} + next: update_progress + +update_progress: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_NOTIFICATIONS]/model/progress" + headers: + X-CSRF-Token: ${token} + cookie: ${csrf_cookie} + body: + sessionId: ${session_id} + progressPercentage: ${progress_percentage} + trainingStatus: ${training_status} + trainingMessage: ${training_message} + result: res_node + next: check_node_server_status + +check_node_server_status: + switch: + - condition: ${200 <= res_node.response.statusCodeValue && res_node.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + sessionId: '${session_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + sessionId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end diff --git a/DSL/Ruuter.public/global-classifier/POST/datamodels/training/status/update.yml b/DSL/Ruuter.public/global-classifier/POST/datamodels/training/status/update.yml new file mode 100644 index 00000000..ffa2f330 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/datamodels/training/status/update.yml @@ -0,0 +1,93 @@ +declaration: + call: declare + version: 0.1 + description: "Update data model training status to trained and set training results" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: modelId + type: number + description: "Model ID to update (required)" + - field: trainingResults + type: object + description: "Training results object (required)" + - field: modelS3Location + type: string + description: "S3 location/key of the trained model (required)" + next: extractRequestData + +# Data Extraction +extractRequestData: + assign: + modelId: ${incoming.body.modelId} + trainingResults: ${incoming.body.trainingResults} + modelS3Location: ${incoming.body.modelS3Location} + next: validateRequiredFields + +# Required Field Validation +validateRequiredFields: + switch: + - condition: ${modelId === null || modelId === undefined || !modelId} + next: return_missing_model_id + - condition: ${trainingResults === null || trainingResults === undefined} + next: return_missing_training_results + - condition: ${modelS3Location === null || modelS3Location === undefined || modelS3Location === ""} + next: return_missing_model_s3_location + next: updateTrainingStatus + +# Update Training Status +updateTrainingStatus: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-datamodels-training-status" + body: + modelId: ${modelId} + trainingResults: ${JSON.stringify(trainingResults)} + modelS3Location: ${modelS3Location} + trainingStatus: "trained" + result: update_result + next: checkUpdateResult + +# Check Update Result +checkUpdateResult: + switch: + - condition: ${!update_result || !update_result.response || !update_result.response.body} + next: return_update_failed + - condition: ${update_result.response.body.length === 0} + next: return_model_not_found + next: return_success + +# Success Response +return_success: + return: "Training status updated successfully model ID : ${update_result.response.body[0].modelId}" + status: 200 + next: end + +# Error Responses +return_missing_model_id: + return: '{"error": "Missing required field: modelId"}' + status: 400 + next: end + +return_missing_training_results: + return: '{"error": "Missing required field: trainingResults"}' + status: 400 + next: end + +return_missing_model_s3_location: + return: '{"error": "Missing required field: modelS3Location"}' + status: 400 + next: end + +return_model_not_found: + return: '{"error": "Model not found with the provided modelId"}' + status: 404 + next: end + +return_update_failed: + return: '{"error": "Failed to update training status"}' + status: 500 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/datasets/progress/create.yml b/DSL/Ruuter.public/global-classifier/POST/datasets/progress/create.yml new file mode 100644 index 00000000..86254854 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/datasets/progress/create.yml @@ -0,0 +1,154 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'CREATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: datasetId + type: number + description: "Body field 'dgId'" + description: "Body field 'groupName'" + - field: majorVersion + type: number + description: "Body field 'majorVersion'" + - field: minorVersion + type: number + description: "Body field 'minorVersion'" + # - field: latest + # type: boolean + # description: "Body field 'latest'" + +extract_request_data: + assign: + dataset_id: ${incoming.body.datasetId} + major_version: ${incoming.body.majorVersion} + minor_version: ${incoming.body.minorVersion} + # latest: ${incoming.body.latest} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${dataset_id !== null || major_version !=null || minor_version !==null || patch_version !==null } + next: get_random_string_cookie + next: return_incorrect_request + +get_random_string_cookie: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/return_random_string" + headers: + type: json + result: res_cookie + next: check_random_string_status + +check_random_string_status: + switch: + - condition: ${200 <= res_cookie.response.statusCodeValue && res_cookie.response.statusCodeValue < 300} + next: assign_csrf_cookie + next: assign_fail_response + +assign_csrf_cookie: + assign: + csrf_cookie: ${'_csrf=' + res_cookie.response.body.randomHexString+';'} + next: create_dataset_progress_session + +create_dataset_progress_session: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-dataset-progress-session" + body: + dataset_id: ${dataset_id} + major_version: ${major_version} + minor_version: ${minor_version} + latest: true + progressPercentage: 0 + generation_status: 'Initiating Dataset Generation' + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: assign_session_id + next: assign_fail_response + +assign_session_id: + assign: + session_id: ${res.response.body[0].id} + next: get_csrf_token + +get_csrf_token: + call: http.get + args: + url: "[#GLOBAL_CLASSIFIER_NOTIFICATIONS]/csrf-token" + headers: + cookie: ${csrf_cookie} + result: res_token + next: check_token_status + +check_token_status: + switch: + - condition: ${200 <= res_token.response.statusCodeValue && res_token.response.statusCodeValue < 300} + next: assign_csrf_token + next: assign_fail_response + +assign_csrf_token: + assign: + token: ${res_token.response.body.csrfToken} + next: update_progress + +update_progress: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_NOTIFICATIONS]/dataset/progress" + headers: + X-CSRF-Token: ${token} + cookie: ${csrf_cookie} + body: + sessionId: ${session_id} + progressPercentage: 0 + generationStatus: 'Initiating Dataset Generation' + generationMessage: '' + result: res_node + next: check_node_server_status + +check_node_server_status: + switch: + - condition: ${200 <= res_node.response.statusCodeValue && res_node.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + sessionId: '${session_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + sessionId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end diff --git a/DSL/Ruuter.public/global-classifier/POST/datasets/progress/delete.yml b/DSL/Ruuter.public/global-classifier/POST/datasets/progress/delete.yml new file mode 100644 index 00000000..ba98e406 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/datasets/progress/delete.yml @@ -0,0 +1,46 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'DELETE'" + method: post + accepts: json + returns: json + namespace: classifier + +delete_dataset_progress_sessions: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/delete-completed-dataset-progress-sessions" + result: res_sessions + next: check_status + +check_status: + switch: + - condition: ${200 <= res_sessions.response.statusCodeValue && res_sessions.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + diff --git a/DSL/Ruuter.public/global-classifier/POST/datasets/progress/update.yml b/DSL/Ruuter.public/global-classifier/POST/datasets/progress/update.yml new file mode 100644 index 00000000..008225d9 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/datasets/progress/update.yml @@ -0,0 +1,151 @@ +declaration: + call: declare + version: 0.1 + description: "Description placeholder for 'UPDATE'" + method: post + accepts: json + returns: json + namespace: classifier + allowlist: + body: + - field: sessionId + type: number + description: "Body field 'sessionId'" + - field: generationStatus + type: string + description: "Body field 'generationStatus'" + - field: generationMessage + type: string + description: "Body field 'generationMessage'" + - field: progressPercentage + type: number + description: "Body field 'progressPercentage'" + - field: processComplete + type: boolean + description: "Body field 'processComplete'" + +extract_request_data: + assign: + session_id: ${incoming.body.sessionId} + generation_status: ${incoming.body.generationStatus} + generation_message: ${incoming.body.generationMessage} + progress_percentage: ${incoming.body.progressPercentage} + process_complete: ${incoming.body.processComplete} + next: check_for_request_data + +check_for_request_data: + switch: + - condition: ${session_id !== null || !generation_status || progress_percentage !==null} + next: get_random_string_cookie + next: return_incorrect_request + +get_random_string_cookie: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_DMAPPER]/hbs/global-classifier/return_random_string" + headers: + type: json + result: res_cookie + next: check_random_string_status + +check_random_string_status: + switch: + - condition: ${200 <= res_cookie.response.statusCodeValue && res_cookie.response.statusCodeValue < 300} + next: assign_csrf_cookie + next: assign_fail_response + +assign_csrf_cookie: + assign: + csrf_cookie: ${'_csrf=' + res_cookie.response.body.randomHexString+';'} + next: update_dataset_progress_session + +update_dataset_progress_session: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/update-dataset-progress-session" + body: + id: ${session_id} + generation_status: ${generation_status} + progress_percentage: ${progress_percentage} + generation_message: ${generation_message} + process_complete: ${process_complete} + result: res + next: check_status + +check_status: + switch: + - condition: ${200 <= res.response.statusCodeValue && res.response.statusCodeValue < 300} + next: get_csrf_token + next: assign_fail_response + +get_csrf_token: + call: http.get + args: + url: "[#GLOBAL_CLASSIFIER_NOTIFICATIONS]/csrf-token" + headers: + cookie: ${csrf_cookie} + result: res_token + next: check_token_status + +check_token_status: + switch: + - condition: ${200 <= res_token.response.statusCodeValue && res_token.response.statusCodeValue < 300} + next: assign_csrf_token + next: assign_fail_response + +assign_csrf_token: + assign: + token: ${res_token.response.body.csrfToken} + next: update_progress + +update_progress: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_NOTIFICATIONS]/dataset/progress" + headers: + X-CSRF-Token: ${token} + cookie: ${csrf_cookie} + body: + sessionId: ${session_id} + progressPercentage: ${progress_percentage} + generationStatus: ${generation_status} + generationMessage: ${generation_message} + result: res_node + next: check_node_server_status + +check_node_server_status: + switch: + - condition: ${200 <= res_node.response.statusCodeValue && res_node.response.statusCodeValue < 300} + next: assign_success_response + next: assign_fail_response + +assign_success_response: + assign: + format_res: { + sessionId: '${session_id}', + operationSuccessful: true, + } + next: return_ok + +assign_fail_response: + assign: + format_res: { + sessionId: '', + operationSuccessful: false, + } + next: return_bad_request + +return_ok: + status: 200 + return: ${format_res} + next: end + +return_bad_request: + status: 400 + return: ${format_res} + next: end + +return_incorrect_request: + status: 400 + return: 'Missing Required Fields' + next: end diff --git a/DSL/Ruuter.public/global-classifier/POST/datasets/update.yml b/DSL/Ruuter.public/global-classifier/POST/datasets/update.yml new file mode 100644 index 00000000..94515169 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/datasets/update.yml @@ -0,0 +1,55 @@ +declaration: + call: declare + version: 0.1 + description: "Bulk insert datasets from CSV file" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: filePath + type: string + description: "Path to the CSV file (required)" + next: extractRequestData + +extractRequestData: + assign: + filePath: ${incoming.body.filePath} + next: validateRequiredFields + +validateRequiredFields: + switch: + - condition: ${filePath === null || filePath === undefined || filePath === ""} + next: return_missing_required_fields + next: insertDatasets + +insertDatasets: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-grouped-datasets" + body: + filePath: ${filePath} + result: insert_result + next: checkInsertResult + +checkInsertResult: + switch: + - condition: ${!insert_result || !insert_result.response} + next: return_insert_failed + next: return_success + +return_success: + return: "Datasets inserted successfully from file: ${filePath}" + status: 200 + next: end + +return_missing_required_fields: + return: "error: missing required field (filePath)" + status: 400 + next: end + +return_insert_failed: + return: "error: failed to insert datasets from CSV file" + status: 500 + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/datasets/version/major.yml b/DSL/Ruuter.public/global-classifier/POST/datasets/version/major.yml new file mode 100644 index 00000000..99533b18 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/datasets/version/major.yml @@ -0,0 +1,51 @@ +declaration: + call: declare + version: 0.1 + description: "Create a new dataset with incremented major/minor version" + method: post + accepts: json + returns: json + namespace: global-classifier + +getLatestDataset: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-latest-dataset" + result: latestDatasetResult + next: createDataset + +createDataset: + switch: + - condition: ${latestDatasetResult.response.body.length === 0} + next: computeInitialVersion + next: computeNewVersion + +computeInitialVersion: + assign: + newMajor: 1 + newMinor: 0 + next: insertDataset + +computeNewVersion: + assign: + latestDataset: ${latestDatasetResult.response.body[0]} + newMajor: ${latestDataset?.major+1} + newMinor: 0 + next: insertDataset + +insertDataset: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-dataset" + body: + major: ${newMajor} + minor: ${newMinor} + generationStatus: "Generation_in_Progress" + lastModelTrained: "" + lastTrained: "1970-01-01T00:00:00.000Z" + result: insertResult + next: returnResult + +returnResult: + return: ${insertResult.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/datasets/version/minor.yml b/DSL/Ruuter.public/global-classifier/POST/datasets/version/minor.yml new file mode 100644 index 00000000..3e40f1bb --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/datasets/version/minor.yml @@ -0,0 +1,45 @@ +declaration: + call: declare + version: 0.1 + description: "Create a new dataset with incremented major/minor version" + method: post + accepts: json + returns: json + namespace: global-classifier + +getLatestDataset: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/get-latest-dataset" + result: latestDatasetResult + next: createDataset + +createDataset: + switch: + - condition: ${latestDatasetResult.response.body.length === 0} + next: end + next: computeNewVersion + +computeNewVersion: + assign: + latestDataset: ${latestDatasetResult.response.body[0]} + newMajor: ${latestDataset?.major} + newMinor: ${latestDataset?.minor+1} + next: insertDataset + +insertDataset: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_RESQL]/insert-dataset" + body: + major: ${newMajor} + minor: ${newMinor} + generationStatus: "Generation_in_Progress" + lastModelTrained: "" + lastTrained: "1970-01-01T00:00:00.000Z" + result: insertResult + next: returnResult + +returnResult: + return: ${insertResult.response.body} + next: end \ No newline at end of file diff --git a/DSL/Ruuter.public/global-classifier/POST/inference/deploy.yml b/DSL/Ruuter.public/global-classifier/POST/inference/deploy.yml new file mode 100644 index 00000000..2572e241 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/inference/deploy.yml @@ -0,0 +1,171 @@ +declaration: + call: declare + version: 0.1 + description: "Deploy a data model in a specified environment" + method: post + accepts: json + returns: json + namespace: global-classifier + # Input Validation Schema + allowlist: + body: + - field: modelId + type: number + description: "Model identifier (integer)" + - field: currentEnv + type: string + description: "Current environment: undeployed, testing, or production" + - field: targetEnv + type: string + description: "Target environment: undeployed, testing, or production" + - field: firstDeployment + type: boolean + description: "Indicates if this is the first deployment of the model" + next: extractRequestData + +# Data Extraction +extractRequestData: + assign: + modelId: ${incoming.body.modelId} + firstDeployment: ${incoming.body.firstDeployment} + currentEnv: ${incoming.body.currentEnv} + targetEnv: ${incoming.body.targetEnv} + deploymentType: ${currentEnv + "_to_" + targetEnv} + next: validate_input + +# Required Field Validation +validate_input: + switch: + - condition: ${!modelId} + next: validation_error + - condition: ${firstDeployment !== true && firstDeployment !== false} + next: validation_error + - condition: ${!currentEnv || !['undeployed', 'testing', 'production'].includes(currentEnv)} + next: validation_error + - condition: ${!targetEnv || !['undeployed', 'testing', 'production'].includes(targetEnv)} + next: validation_error + - condition: ${currentEnv === targetEnv && currentEnv !== 'undeployed'} + next: same_environment_error + next: checkFirstDeployment + +checkFirstDeployment: + switch: + - condition: ${firstDeployment} + next: handleFirstDeployment + next: selectDeploymentPath + +handleFirstDeployment: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_CRON_MANAGER]/execute/deploy/deploy_model" + query: + modelId: ${modelId} + currentEnv: ${currentEnv} + targetEnv: ${targetEnv} + firstDeployment: ${firstDeployment} + result: res + next: selectDeploymentPath + + +selectDeploymentPath: + switch: + + - condition: ${deploymentType === "undeployed_to_testing"} + next: pushUndeployedModelToTesting + + - condition: ${deploymentType === "testing_to_production"} + next: pushTestingModelToProduction + + - condition: ${deploymentType === "production_to_testing"} + next: pushProductionModelToTesting + + - condition: ${deploymentType === "testing_to_undeployed"} + next: pushTestingModelToUndeployed + + - condition: ${deploymentType === "production_to_undeployed"} + next: pushProductionModelToUndeployed + + - condition: ${deploymentType === "undeployed_to_production"} + next: pushUndeployedModelToProduction + + next: return_invalid_deployment_path_error + +pushUndeployedModelToTesting: + call: reflect.mock + args: + response: + success: true + message: "Model successfully pushed from undeployed to testing environment" + result: deployment_status + next: return_deployment_status + + +pushUndeployedModelToProduction: + call: reflect.mock + args: + response: + success: true + message: "Model successfully pushed from undeployed to production environment" + result: deployment_status + next: return_deployment_status + +pushTestingModelToProduction: + call: reflect.mock + args: + response: + success: true + message: "Model successfully pushed from testing to production environment" + result: deployment_status + next: return_deployment_status + +pushProductionModelToTesting: + call: reflect.mock + args: + response: + success: true + message: "Model successfully pushed from production to testing environment" + result: deployment_status + next: return_deployment_status + +pushTestingModelToUndeployed: + call: reflect.mock + args: + response: + success: true + message: "Model successfully pushed from testing to undeployed environment" + result: deployment_status + next: return_deployment_status + +pushProductionModelToUndeployed: + call: reflect.mock + args: + response: + success: true + message: "Model successfully pushed from production to undeployed environment" + result: deployment_status + next: return_deployment_status + + + +return_invalid_deployment_path_error: + return: "Invalid deployment path. Supported transitions - undeployed to testing, testing to production, production to testing, testing to undeployed, production to undeployed, undeployed to production" + status: 400 + next: end + + +validation_error: + return: "Invalid input. Required - model_id (number), current_env or target_env (undeployed|testing|production)" + status: 400 + next: end + +same_environment_error: + return: "Current environment and target environment cannot be the same" + status: 400 + next: end + +return_deployment_status: + return: ${deployment_status.response} + status: 200 + next: end + + diff --git a/DSL/Ruuter.public/global-classifier/POST/inference/load-model.yml b/DSL/Ruuter.public/global-classifier/POST/inference/load-model.yml new file mode 100644 index 00000000..789c5887 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/inference/load-model.yml @@ -0,0 +1,43 @@ +declaration: + call: declare + version: 0.1 + description: "Deploy a data model in a specified environment" + method: post + accepts: json + returns: json + namespace: global-classifier + # Input Validation Schema + allowlist: + body: + - field: modelId + type: number + description: "Model identifier (integer)" + next: extractRequestData + +# Data Extraction +extractRequestData: + assign: + modelId: ${incoming.body.modelId} + next: validate_input + +# Required Field Validation +validate_input: + switch: + - condition: ${!modelId} + next: validation_error + next: load_model + +load_model: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_CRON_MANAGER]/execute/deploy/deploy_model" + query: + modelId: ${modelId} + result: res + next: return_ok + + +return_ok: + status: 200 + return: "load-model initiated successfully" + next: end diff --git a/DSL/Ruuter.public/global-classifier/POST/testmodel/unload.yml b/DSL/Ruuter.public/global-classifier/POST/testmodel/unload.yml new file mode 100644 index 00000000..481312e4 --- /dev/null +++ b/DSL/Ruuter.public/global-classifier/POST/testmodel/unload.yml @@ -0,0 +1,110 @@ +declaration: + call: declare + version: 0.1 + description: "Unload a model from NVIDIA Triton test server" + method: post + accepts: json + returns: json + namespace: global-classifier + allowlist: + body: + - field: modelId + type: string + description: "Identifier of the model to unload from Triton server" + +extractRequestData: + assign: + modelId: ${incoming.body.modelId} + next: validateInput + +validateInput: + switch: + - condition: ${!modelId} + next: return_validation_error + next: prepareModelNames + +prepareModelNames: + assign: + # Generate the model names based on the modelId + preProcessingModel: ${modelId}-pre-processing + textClassifierModel: ${modelId}-text-classifier + postProcessingModel: ${modelId}-post-processing + ensembleModel: ${modelId}-classifier-ensemble + next: unloadEnsembleModel + +unloadEnsembleModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER]/v2/repository/models/${ensembleModel}/unload" + timeout: 600000 + body: + unload_dependents: true + result: ensembleUnloadResult + next: checkEnsembleUnload + error: unloadPostProcessingModel + +checkEnsembleUnload: + switch: + - condition: ${ensembleUnloadResult.response.statusCodeValue !== 200 && ensembleUnloadResult.response.statusCodeValue !== 201} + next: unloadPostProcessingModel + next: unloadPostProcessingModel + +unloadPostProcessingModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER]/v2/repository/models/${postProcessingModel}/unload" + timeout: 600000 + result: postProcessingUnloadResult + next: checkPostProcessingUnload + error: unloadTextClassifierModel + +checkPostProcessingUnload: + switch: + - condition: ${postProcessingUnloadResult.response.statusCodeValue !== 200 && postProcessingUnloadResult.response.statusCodeValue !== 201} + next: unloadTextClassifierModel + next: unloadTextClassifierModel + +unloadTextClassifierModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER]/v2/repository/models/${textClassifierModel}/unload" + timeout: 600000 + result: textClassifierUnloadResult + next: checkTextClassifierUnload + error: unloadPreProcessingModel + +checkTextClassifierUnload: + switch: + - condition: ${textClassifierUnloadResult.response.statusCodeValue !== 200 && textClassifierUnloadResult.response.statusCodeValue !== 201} + next: unloadPreProcessingModel + next: unloadPreProcessingModel + +unloadPreProcessingModel: + call: http.post + args: + url: "[#GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER]/v2/repository/models/${preProcessingModel}/unload" + timeout: 600000 + result: preProcessingUnloadResult + next: checkPreProcessingUnload + error: return_success + +checkPreProcessingUnload: + switch: + - condition: ${preProcessingUnloadResult.response.statusCodeValue !== 200 && preProcessingUnloadResult.response.statusCodeValue !== 201} + next: return_success + next: return_success + +return_success: + status: 200 + return: "Model unloaded successfully from Triton test server" + next: end + +return_validation_error: + status: 400 + return: "Invalid request - modelId is required" + next: end + +return_server_unavailable: + status: 503 + return: "Server unavailable - Triton test server is not ready or unavailable" + next: end diff --git a/GUI/.dockerignore b/GUI/.dockerignore new file mode 100644 index 00000000..ab4f96a1 --- /dev/null +++ b/GUI/.dockerignore @@ -0,0 +1,7 @@ +node_modules +npm-debug.log +build +.git +*.md +.gitignore +.env.development diff --git a/GUI/.env.development b/GUI/.env.development new file mode 100644 index 00000000..7ff4d8bb --- /dev/null +++ b/GUI/.env.development @@ -0,0 +1,8 @@ +REACT_APP_RUUTER_API_URL=http://localhost:8086 +REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 +REACT_APP_EXTERNAL_API_URL=http://localhost:8000 +REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth +REACT_APP_SERVICE_ID=conversations,settings,monitoring +REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 +REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040; +REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE \ No newline at end of file diff --git a/GUI/.eslintrc.json b/GUI/.eslintrc.json new file mode 100644 index 00000000..5e603ecd --- /dev/null +++ b/GUI/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "react-app" +} diff --git a/GUI/.gitignore b/GUI/.gitignore new file mode 100644 index 00000000..d79b5ca1 --- /dev/null +++ b/GUI/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# testing +/coverage + +# production +/build + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/GUI/.prettierignore b/GUI/.prettierignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/GUI/.prettierignore @@ -0,0 +1 @@ +node_modules diff --git a/GUI/.prettierrc b/GUI/.prettierrc new file mode 100644 index 00000000..0a725205 --- /dev/null +++ b/GUI/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/GUI/Dockerfile.dev b/GUI/Dockerfile.dev new file mode 100644 index 00000000..48b7890e --- /dev/null +++ b/GUI/Dockerfile.dev @@ -0,0 +1,14 @@ +FROM node:22.0.0-alpine AS image +WORKDIR /app +COPY ./package.json . + +FROM image AS build +RUN npm install --legacy-peer-deps --mode=development +COPY . . +RUN ./node_modules/.bin/vite build --mode=development + +EXPOSE 3001 + +ENV REACT_APP_ENABLE_HIDDEN_FEATURES TRUE + +CMD ["npm", "run", "dev"] diff --git a/GUI/docker-compose.yml b/GUI/docker-compose.yml new file mode 100644 index 00000000..87d6970c --- /dev/null +++ b/GUI/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.9" +services: + buerokratt_chatbot: + container_name: buerokratt_classifier + build: + context: . + target: web + entrypoint: "/opt/buerokratt-chatbot/rebuild.sh" + ports: + - '3001:3001' diff --git a/GUI/entrypoint.sh b/GUI/entrypoint.sh new file mode 100644 index 00000000..636848f7 --- /dev/null +++ b/GUI/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Replace environment variables in the Nginx configuration template +envsubst '$BASE_URL $REACT_APP_RUUTER_API_URL $REACT_APP_RUUTER_V1_PRIVATE_API_URL $REACT_APP_RUUTER_V2_PRIVATE_API_URL $REACT_APP_CUSTOMER_SERVICE_LOGIN $CHOKIDAR_USEPOLLING $PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf + +# Start the Nginx server +nginx -g "daemon off;" diff --git a/GUI/i18n.ts b/GUI/i18n.ts new file mode 100644 index 00000000..6a4593d0 --- /dev/null +++ b/GUI/i18n.ts @@ -0,0 +1,26 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import commonEN from './translations/en/common.json'; +import commonET from './translations/et/common.json'; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + debug: import.meta.env.NODE_ENV === 'development', + fallbackLng: 'et', + supportedLngs: ['et','en'], + resources: { + en: { + common: commonEN, + }, + et: { + common: commonET, + }, + }, + defaultNS: 'common', + }); + +export default i18n; diff --git a/GUI/index.html b/GUI/index.html new file mode 100644 index 00000000..047cff35 --- /dev/null +++ b/GUI/index.html @@ -0,0 +1,14 @@ + + + + + + + Bürokratt + + +
+
+ + + diff --git a/GUI/package-lock.json b/GUI/package-lock.json new file mode 100644 index 00000000..436ec9c4 --- /dev/null +++ b/GUI/package-lock.json @@ -0,0 +1,15860 @@ +{ + "name": "byk-training-module-gui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "byk-training-module-gui", + "version": "0.0.0", + "dependencies": { + "@buerokratt-ria/styles": "^0.0.1", + "@fontsource/roboto": "^4.5.8", + "@formkit/auto-animate": "^1.0.0-beta.5", + "@fortaine/fetch-event-source": "^3.0.6", + "@radix-ui/react-accessible-icon": "^1.0.1", + "@radix-ui/react-collapsible": "^1.0.1", + "@radix-ui/react-dialog": "^1.0.2", + "@radix-ui/react-popover": "^1.0.2", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-select": "^1.1.2", + "@radix-ui/react-switch": "^1.0.1", + "@radix-ui/react-tabs": "^1.0.1", + "@radix-ui/react-toast": "^1.1.2", + "@radix-ui/react-tooltip": "^1.0.2", + "@tanstack/match-sorter-utils": "^8.7.2", + "@tanstack/react-query": "^4.36.1", + "@tanstack/react-table": "^8.7.4", + "axios": "^1.2.1", + "clsx": "^1.2.1", + "date-fns": "^2.29.3", + "downshift": "^7.0.5", + "esbuild": "^0.19.5", + "formik": "^2.4.6", + "framer-motion": "^8.5.5", + "howler": "^2.2.4", + "i18next": "^22.4.5", + "i18next-browser-languagedetector": "^7.0.1", + "linkify-react": "^4.1.1", + "linkifyjs": "^4.1.1", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "react": "^18.2.0", + "react-color": "^2.19.3", + "react-cookie": "^4.1.1", + "react-datepicker": "^4.8.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dom": "^18.2.0", + "react-hook-form": "^7.52.1", + "react-i18next": "^12.1.1", + "react-icons": "^4.10.1", + "react-idle-timer": "^5.5.2", + "react-modal": "^3.16.1", + "react-redux": "^8.1.1", + "react-router-dom": "^6.5.0", + "react-select": "^5.7.4", + "react-text-selection-popover": "^2.0.2", + "react-textarea-autosize": "^8.4.0", + "reactflow": "^11.4.0", + "regexify-string": "^1.0.19", + "rxjs": "^7.8.1", + "timeago.js": "^4.0.2", + "usehooks-ts": "^2.9.1", + "uuid": "^9.0.0", + "yup": "^1.4.0", + "zustand": "^4.4.4" + }, + "devDependencies": { + "@types/howler": "^2.2.11", + "@types/lodash": "^4.14.191", + "@types/lodash.debounce": "^4.0.7", + "@types/node": "^18.11.17", + "@types/react": "^18.0.26", + "@types/react-color": "^3.0.6", + "@types/react-datepicker": "^4.8.0", + "@types/react-dom": "^18.0.9", + "@types/uuid": "^9.0.2", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", + "@vitejs/plugin-react": "^3.0.0", + "eslint": "^8.57.1", + "eslint-config-react-app": "^7.0.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-typescript": "^0.14.0", + "mocksse": "^1.0.4", + "msw": "^0.49.2", + "prettier": "^2.8.1", + "sass": "^1.57.0", + "typescript": "^4.9.3", + "vite": "^4.0.0", + "vite-plugin-env-compatible": "^1.1.1", + "vite-plugin-svgr": "^2.4.0", + "vite-plugin-transform": "^2.0.1", + "vite-tsconfig-paths": "^4.0.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.7.tgz", + "integrity": "sha512-SO5E3bVxDuxyNxM5agFv480YA2HO6ohZbGxbazZdIk3KQOPOGVNw6q78I9/lbviIf95eq6tPozeYnJLbjnC8IA==", + "dev": true, + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dependencies": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", + "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", + "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.7.tgz", + "integrity": "sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-decorators": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.7.tgz", + "integrity": "sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.7.tgz", + "integrity": "sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", + "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", + "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", + "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.7.tgz", + "integrity": "sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-flow": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", + "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", + "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz", + "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", + "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", + "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz", + "integrity": "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", + "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-typescript": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", + "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", + "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.24.7", + "@babel/plugin-transform-react-jsx-development": "^7.24.7", + "@babel/plugin-transform-react-pure-annotations": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz", + "integrity": "sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@buerokratt-ria/styles": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@buerokratt-ria/styles/-/styles-0.0.1.tgz", + "integrity": "sha512-bSj7WsdQO4P/43mRgsa5sDEwBuOebXcl3+Peur8NwToqczqsTMbXSO5P6xyXHoTnHWt082PhT8ht7OAgtFSzfw==" + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz", + "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" + }, + "node_modules/@fontsource/roboto": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz", + "integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA==" + }, + "node_modules/@formkit/auto-animate": { + "version": "1.0.0-pre-alpha.3", + "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-1.0.0-pre-alpha.3.tgz", + "integrity": "sha512-lMVZ3LFUIu0RIxCEwmV8nUUJQ46M2bv2NDU3hrhZivViuR1EheC8Mj5sx/ACqK5QLK8XB8z7GDIZBUGdU/9OZQ==", + "peerDependencies": { + "react": "^16.8.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@fortaine/fetch-event-source": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@fortaine/fetch-event-source/-/fetch-event-source-3.0.6.tgz", + "integrity": "sha512-621GAuLMvKtyZQ3IA6nlDWhV1V/7PGOTNIGLUifxt0KzM+dZIweJ6F3XvQF3QnqeNfS1N7WQ0Kil1Di/lhChEw==", + "engines": { + "node": ">=16.15" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "node_modules/@lezer/lr": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", + "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.8.5.tgz", + "integrity": "sha512-KPDeVScZgA1oq0CiPBcOa3kHIqU+pTOwRFDIhxvmf8CTNvqdZQYp5cCKW0bUk69VygB2PuTiINFWbY78aR2pQw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-2.8.5.tgz", + "integrity": "sha512-w/sLhN4T7MW1nB3R/U8WK5BgQLz904wh+/SmA2jD8NnF7BLLoUgflCNxOeSPOWp8geP6nP/+VjWzZVip7rZ1ug==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-2.8.5.tgz", + "integrity": "sha512-c0TGMbm2M55pwTDIfkDLB6BpIsgxV4PjYck2HiOX+cy/JWiBXz32lYbarPqejKs9Flm7YVAKSILUducU9g2RVg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-2.8.5.tgz", + "integrity": "sha512-vtbZRHH5UDlL01TT5jB576Zox3+hdyogvpcbvVJlmU5PdL3c5V7cj1EODdh1CHPksRl+cws/58ugEHi8bcj4Ww==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-2.8.5.tgz", + "integrity": "sha512-Xkc8IUx9aEhP0zvgeKy7IQ3ReX2N8N1L0WPcQwnZweWmOuKfwpS3GRIYqLtK5za/w3E60zhFfNdS+3pBZPytqQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.8.5.tgz", + "integrity": "sha512-4wvrf5BgnR8RpogHhtpCPJMKBmvyZPhhUtEwMJbXh0ni2BucpfF07jlmyM11zRqQ2XIq6PbC2j7W7UCCcm1rRQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@mischnic/json-sourcemap": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@mischnic/json-sourcemap/-/json-sourcemap-0.1.1.tgz", + "integrity": "sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/lr": "^1.0.0", + "json5": "^2.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@motionone/animation": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz", + "integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==", + "dependencies": { + "@motionone/easing": "^10.18.0", + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/dom": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.18.0.tgz", + "integrity": "sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A==", + "dependencies": { + "@motionone/animation": "^10.18.0", + "@motionone/generators": "^10.18.0", + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/easing": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz", + "integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==", + "dependencies": { + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/generators": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz", + "integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==", + "dependencies": { + "@motionone/types": "^10.17.1", + "@motionone/utils": "^10.18.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@motionone/types": { + "version": "10.17.1", + "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz", + "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==" + }, + "node_modules/@motionone/utils": { + "version": "10.18.0", + "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz", + "integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==", + "dependencies": { + "@motionone/types": "^10.17.1", + "hey-listen": "^1.0.8", + "tslib": "^2.3.1" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@mswjs/cookies": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.2.2.tgz", + "integrity": "sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g==", + "dev": true, + "dependencies": { + "@types/set-cookie-parser": "^2.4.0", + "set-cookie-parser": "^2.4.6" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.17.10.tgz", + "integrity": "sha512-N8x7eSLGcmUFNWZRxT1vsHvypzIRgQYdG0rJey/rZCy6zT/30qDt8Joj7FxzGNLSwXbeZqJOMqDurp7ra4hgbw==", + "dev": true, + "dependencies": { + "@open-draft/until": "^1.0.3", + "@types/debug": "^4.1.7", + "@xmldom/xmldom": "^0.8.3", + "debug": "^4.3.3", + "headers-polyfill": "3.2.5", + "outvariant": "^1.2.1", + "strict-event-emitter": "^0.2.4", + "web-encoding": "^1.1.5" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@mswjs/interceptors/node_modules/headers-polyfill": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.2.5.tgz", + "integrity": "sha512-tUCGvt191vNSQgttSyJoibR+VO+I6+iCHIUdhzEMJKE+EAL8BwCN7fUOZlY4ofOelNHsK+gEjxB/B+9N3EWtdA==", + "dev": true + }, + "node_modules/@mswjs/interceptors/node_modules/strict-event-emitter": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz", + "integrity": "sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==", + "dev": true, + "dependencies": { + "events": "^3.3.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-draft/until": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz", + "integrity": "sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==", + "dev": true + }, + "node_modules/@parcel/bundler-default": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/bundler-default/-/bundler-default-2.12.0.tgz", + "integrity": "sha512-3ybN74oYNMKyjD6V20c9Gerdbh7teeNvVMwIoHIQMzuIFT6IGX53PyOLlOKRLbjxMc0TMimQQxIt2eQqxR5LsA==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/graph": "3.2.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/cache": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/cache/-/cache-2.12.0.tgz", + "integrity": "sha512-FX5ZpTEkxvq/yvWklRHDESVRz+c7sLTXgFuzz6uEnBcXV38j6dMSikflNpHA6q/L4GKkCqRywm9R6XQwhwIMyw==", + "dependencies": { + "@parcel/fs": "2.12.0", + "@parcel/logger": "2.12.0", + "@parcel/utils": "2.12.0", + "lmdb": "2.8.5" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/codeframe": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/codeframe/-/codeframe-2.12.0.tgz", + "integrity": "sha512-v2VmneILFiHZJTxPiR7GEF1wey1/IXPdZMcUlNXBiPZyWDfcuNgGGVQkx/xW561rULLIvDPharOMdxz5oHOKQg==", + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/codeframe/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@parcel/codeframe/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@parcel/codeframe/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@parcel/codeframe/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@parcel/codeframe/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/codeframe/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/compressor-raw": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/compressor-raw/-/compressor-raw-2.12.0.tgz", + "integrity": "sha512-h41Q3X7ZAQ9wbQ2csP8QGrwepasLZdXiuEdpUryDce6rF9ZiHoJ97MRpdLxOhOPyASTw/xDgE1xyaPQr0Q3f5A==", + "dependencies": { + "@parcel/plugin": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/config-default": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/config-default/-/config-default-2.12.0.tgz", + "integrity": "sha512-dPNe2n9eEsKRc1soWIY0yToMUPirPIa2QhxcCB3Z5RjpDGIXm0pds+BaiqY6uGLEEzsjhRO0ujd4v2Rmm0vuFg==", + "dependencies": { + "@parcel/bundler-default": "2.12.0", + "@parcel/compressor-raw": "2.12.0", + "@parcel/namer-default": "2.12.0", + "@parcel/optimizer-css": "2.12.0", + "@parcel/optimizer-htmlnano": "2.12.0", + "@parcel/optimizer-image": "2.12.0", + "@parcel/optimizer-svgo": "2.12.0", + "@parcel/optimizer-swc": "2.12.0", + "@parcel/packager-css": "2.12.0", + "@parcel/packager-html": "2.12.0", + "@parcel/packager-js": "2.12.0", + "@parcel/packager-raw": "2.12.0", + "@parcel/packager-svg": "2.12.0", + "@parcel/packager-wasm": "2.12.0", + "@parcel/reporter-dev-server": "2.12.0", + "@parcel/resolver-default": "2.12.0", + "@parcel/runtime-browser-hmr": "2.12.0", + "@parcel/runtime-js": "2.12.0", + "@parcel/runtime-react-refresh": "2.12.0", + "@parcel/runtime-service-worker": "2.12.0", + "@parcel/transformer-babel": "2.12.0", + "@parcel/transformer-css": "2.12.0", + "@parcel/transformer-html": "2.12.0", + "@parcel/transformer-image": "2.12.0", + "@parcel/transformer-js": "2.12.0", + "@parcel/transformer-json": "2.12.0", + "@parcel/transformer-postcss": "2.12.0", + "@parcel/transformer-posthtml": "2.12.0", + "@parcel/transformer-raw": "2.12.0", + "@parcel/transformer-react-refresh-wrap": "2.12.0", + "@parcel/transformer-svg": "2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/core": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.12.0.tgz", + "integrity": "sha512-s+6pwEj+GfKf7vqGUzN9iSEPueUssCCQrCBUlcAfKrJe0a22hTUCjewpB0I7lNrCIULt8dkndD+sMdOrXsRl6Q==", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "@parcel/cache": "2.12.0", + "@parcel/diagnostic": "2.12.0", + "@parcel/events": "2.12.0", + "@parcel/fs": "2.12.0", + "@parcel/graph": "3.2.0", + "@parcel/logger": "2.12.0", + "@parcel/package-manager": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/profiler": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "@parcel/workers": "2.12.0", + "abortcontroller-polyfill": "^1.1.9", + "base-x": "^3.0.8", + "browserslist": "^4.6.6", + "clone": "^2.1.1", + "dotenv": "^7.0.0", + "dotenv-expand": "^5.1.0", + "json5": "^2.2.0", + "msgpackr": "^1.9.9", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/core/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/diagnostic": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.12.0.tgz", + "integrity": "sha512-8f1NOsSFK+F4AwFCKynyIu9Kr/uWHC+SywAv4oS6Bv3Acig0gtwUjugk0C9UaB8ztBZiW5TQZhw+uPZn9T/lJA==", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/events": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/events/-/events-2.12.0.tgz", + "integrity": "sha512-nmAAEIKLjW1kB2cUbCYSmZOGbnGj8wCzhqnK727zCCWaA25ogzAtt657GPOeFyqW77KyosU728Tl63Fc8hphIA==", + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/fs": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.12.0.tgz", + "integrity": "sha512-NnFkuvou1YBtPOhTdZr44WN7I60cGyly2wpHzqRl62yhObyi1KvW0SjwOMa0QGNcBOIzp4G0CapoZ93hD0RG5Q==", + "dependencies": { + "@parcel/rust": "2.12.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "@parcel/watcher": "^2.0.7", + "@parcel/workers": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/graph": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.2.0.tgz", + "integrity": "sha512-xlrmCPqy58D4Fg5umV7bpwDx5Vyt7MlnQPxW68vae5+BA4GSWetfZt+Cs5dtotMG2oCHzZxhIPt7YZ7NRyQzLA==", + "dependencies": { + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/logger": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.12.0.tgz", + "integrity": "sha512-cJ7Paqa7/9VJ7C+KwgJlwMqTQBOjjn71FbKk0G07hydUEBISU2aDfmc/52o60ErL9l+vXB26zTrIBanbxS8rVg==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/events": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/markdown-ansi": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/markdown-ansi/-/markdown-ansi-2.12.0.tgz", + "integrity": "sha512-WZz3rzL8k0H3WR4qTHX6Ic8DlEs17keO9gtD4MNGyMNQbqQEvQ61lWJaIH0nAtgEetu0SOITiVqdZrb8zx/M7w==", + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/markdown-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@parcel/markdown-ansi/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@parcel/markdown-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@parcel/markdown-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@parcel/markdown-ansi/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/markdown-ansi/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/namer-default": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/namer-default/-/namer-default-2.12.0.tgz", + "integrity": "sha512-9DNKPDHWgMnMtqqZIMiEj/R9PNWW16lpnlHjwK3ciRlMPgjPJ8+UNc255teZODhX0T17GOzPdGbU/O/xbxVPzA==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/node-resolver-core": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.3.0.tgz", + "integrity": "sha512-rhPW9DYPEIqQBSlYzz3S0AjXxjN6Ub2yS6tzzsW/4S3Gpsgk/uEq4ZfxPvoPf/6TgZndVxmKwpmxaKtGMmf3cA==", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "@parcel/diagnostic": "2.12.0", + "@parcel/fs": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/node-resolver-core/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/optimizer-css": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-css/-/optimizer-css-2.12.0.tgz", + "integrity": "sha512-ifbcC97fRzpruTjaa8axIFeX4MjjSIlQfem3EJug3L2AVqQUXnM1XO8L0NaXGNLTW2qnh1ZjIJ7vXT/QhsphsA==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "browserslist": "^4.6.6", + "lightningcss": "^1.22.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-htmlnano": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.12.0.tgz", + "integrity": "sha512-MfPMeCrT8FYiOrpFHVR+NcZQlXAptK2r4nGJjfT+ndPBhEEZp4yyL7n1y7HfX9geg5altc4WTb4Gug7rCoW8VQ==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "htmlnano": "^2.0.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "svgo": "^2.4.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-image": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-image/-/optimizer-image-2.12.0.tgz", + "integrity": "sha512-bo1O7raeAIbRU5nmNVtx8divLW9Xqn0c57GVNGeAK4mygnQoqHqRZ0mR9uboh64pxv6ijXZHPhKvU9HEpjPjBQ==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/utils": "2.12.0", + "@parcel/workers": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/optimizer-svgo": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-svgo/-/optimizer-svgo-2.12.0.tgz", + "integrity": "sha512-Kyli+ZZXnoonnbeRQdoWwee9Bk2jm/49xvnfb+2OO8NN0d41lblBoRhOyFiScRnJrw7eVl1Xrz7NTkXCIO7XFQ==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "svgo": "^2.4.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-swc": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-swc/-/optimizer-swc-2.12.0.tgz", + "integrity": "sha512-iBi6LZB3lm6WmbXfzi8J3DCVPmn4FN2lw7DGXxUXu7MouDPVWfTsM6U/5TkSHJRNRogZ2gqy5q9g34NPxHbJcw==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "@swc/core": "^1.3.36", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/package-manager": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.12.0.tgz", + "integrity": "sha512-0nvAezcjPx9FT+hIL+LS1jb0aohwLZXct7jAh7i0MLMtehOi0z1Sau+QpgMlA9rfEZZ1LIeFdnZZwqSy7Ccspw==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/fs": "2.12.0", + "@parcel/logger": "2.12.0", + "@parcel/node-resolver-core": "3.3.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "@parcel/workers": "2.12.0", + "@swc/core": "^1.3.36", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/package-manager/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/packager-css": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-css/-/packager-css-2.12.0.tgz", + "integrity": "sha512-j3a/ODciaNKD19IYdWJT+TP+tnhhn5koBGBWWtrKSu0UxWpnezIGZetit3eE+Y9+NTePalMkvpIlit2eDhvfJA==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "lightningcss": "^1.22.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-html": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-html/-/packager-html-2.12.0.tgz", + "integrity": "sha512-PpvGB9hFFe+19NXGz2ApvPrkA9GwEqaDAninT+3pJD57OVBaxB8U+HN4a5LICKxjUppPPqmrLb6YPbD65IX4RA==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-js": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-js/-/packager-js-2.12.0.tgz", + "integrity": "sha512-viMF+FszITRRr8+2iJyk+4ruGiL27Y6AF7hQ3xbJfzqnmbOhGFtLTQwuwhOLqN/mWR2VKdgbLpZSarWaO3yAMg==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "globals": "^13.2.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-js/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@parcel/packager-js/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@parcel/packager-raw": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-raw/-/packager-raw-2.12.0.tgz", + "integrity": "sha512-tJZqFbHqP24aq1F+OojFbQIc09P/u8HAW5xfndCrFnXpW4wTgM3p03P0xfw3gnNq+TtxHJ8c3UFE5LnXNNKhYA==", + "dependencies": { + "@parcel/plugin": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-svg": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.12.0.tgz", + "integrity": "sha512-ldaGiacGb2lLqcXas97k8JiZRbAnNREmcvoY2W2dvW4loVuDT9B9fU777mbV6zODpcgcHWsLL3lYbJ5Lt3y9cg==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "posthtml": "^0.16.4" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-wasm": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/packager-wasm/-/packager-wasm-2.12.0.tgz", + "integrity": "sha512-fYqZzIqO9fGYveeImzF8ll6KRo2LrOXfD+2Y5U3BiX/wp9wv17dz50QLDQm9hmTcKGWxK4yWqKQh+Evp/fae7A==", + "dependencies": { + "@parcel/plugin": "2.12.0" + }, + "engines": { + "node": ">=12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/plugin": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.12.0.tgz", + "integrity": "sha512-nc/uRA8DiMoe4neBbzV6kDndh/58a4wQuGKw5oEoIwBCHUvE2W8ZFSu7ollSXUGRzfacTt4NdY8TwS73ScWZ+g==", + "dependencies": { + "@parcel/types": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/profiler": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/profiler/-/profiler-2.12.0.tgz", + "integrity": "sha512-q53fvl5LDcFYzMUtSusUBZSjQrKjMlLEBgKeQHFwkimwR1mgoseaDBDuNz0XvmzDzF1UelJ02TUKCGacU8W2qA==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/events": "2.12.0", + "chrome-trace-event": "^1.0.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-cli": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.12.0.tgz", + "integrity": "sha512-TqKsH4GVOLPSCanZ6tcTPj+rdVHERnt5y4bwTM82cajM21bCX1Ruwp8xOKU+03091oV2pv5ieB18pJyRF7IpIw==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "chalk": "^4.1.0", + "term-size": "^2.2.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@parcel/reporter-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@parcel/reporter-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@parcel/reporter-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@parcel/reporter-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/reporter-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/reporter-dev-server": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/reporter-dev-server/-/reporter-dev-server-2.12.0.tgz", + "integrity": "sha512-tIcDqRvAPAttRlTV28dHcbWT5K2r/MBFks7nM4nrEDHWtnrCwimkDmZTc1kD8QOCCjGVwRHcQybpHvxfwol6GA==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-tracer": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/reporter-tracer/-/reporter-tracer-2.12.0.tgz", + "integrity": "sha512-g8rlu9GxB8Ut/F8WGx4zidIPQ4pcYFjU9bZO+fyRIPrSUFH2bKijCnbZcr4ntqzDGx74hwD6cCG4DBoleq2UlQ==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "chrome-trace-event": "^1.0.3", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/resolver-default": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/resolver-default/-/resolver-default-2.12.0.tgz", + "integrity": "sha512-uuhbajTax37TwCxu7V98JtRLiT6hzE4VYSu5B7Qkauy14/WFt2dz6GOUXPgVsED569/hkxebPx3KCMtZW6cHHA==", + "dependencies": { + "@parcel/node-resolver-core": "3.3.0", + "@parcel/plugin": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-browser-hmr": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.12.0.tgz", + "integrity": "sha512-4ZLp2FWyD32r0GlTulO3+jxgsA3oO1P1b5oO2IWuWilfhcJH5LTiazpL5YdusUjtNn9PGN6QLAWfxmzRIfM+Ow==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-js": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/runtime-js/-/runtime-js-2.12.0.tgz", + "integrity": "sha512-sBerP32Z1crX5PfLNGDSXSdqzlllM++GVnVQVeM7DgMKS8JIFG3VLi28YkX+dYYGtPypm01JoIHCkvwiZEcQJg==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-react-refresh": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.12.0.tgz", + "integrity": "sha512-SCHkcczJIDFTFdLTzrHTkQ0aTrX3xH6jrA4UsCBL6ji61+w+ohy4jEEe9qCgJVXhnJfGLE43HNXek+0MStX+Mw==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "react-error-overlay": "6.0.9", + "react-refresh": "^0.9.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-react-refresh/node_modules/react-refresh": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", + "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@parcel/runtime-service-worker": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/runtime-service-worker/-/runtime-service-worker-2.12.0.tgz", + "integrity": "sha512-BXuMBsfiwpIEnssn+jqfC3jkgbS8oxeo3C7xhSQsuSv+AF2FwY3O3AO1c1RBskEW3XrBLNINOJujroNw80VTKA==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/rust/-/rust-2.12.0.tgz", + "integrity": "sha512-005cldMdFZFDPOjbDVEXcINQ3wT4vrxvSavRWI3Az0e3E18exO/x/mW9f648KtXugOXMAqCEqhFHcXECL9nmMw==", + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/source-map": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", + "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": "^12.18.3 || >=14" + } + }, + "node_modules/@parcel/transformer-babel": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-babel/-/transformer-babel-2.12.0.tgz", + "integrity": "sha512-zQaBfOnf/l8rPxYGnsk/ufh/0EuqvmnxafjBIpKZ//j6rGylw5JCqXSb1QvvAqRYruKeccxGv7+HrxpqKU6V4A==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "browserslist": "^4.6.6", + "json5": "^2.2.0", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-babel/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-css": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-css/-/transformer-css-2.12.0.tgz", + "integrity": "sha512-vXhOqoAlQGATYyQ433Z1DXKmiKmzOAUmKysbYH3FD+LKEKLMEl/pA14goqp00TW+A/EjtSKKyeMyHlMIIUqj4Q==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "browserslist": "^4.6.6", + "lightningcss": "^1.22.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-html": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-html/-/transformer-html-2.12.0.tgz", + "integrity": "sha512-5jW4dFFBlYBvIQk4nrH62rfA/G/KzVzEDa6S+Nne0xXhglLjkm64Ci9b/d4tKZfuGWUbpm2ASAq8skti/nfpXw==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "posthtml-parser": "^0.10.1", + "posthtml-render": "^3.0.0", + "semver": "^7.5.2", + "srcset": "4" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-html/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-image": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-image/-/transformer-image-2.12.0.tgz", + "integrity": "sha512-8hXrGm2IRII49R7lZ0RpmNk27EhcsH+uNKsvxuMpXPuEnWgC/ha/IrjaI29xCng1uGur74bJF43NUSQhR4aTdw==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "@parcel/workers": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/transformer-js": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-js/-/transformer-js-2.12.0.tgz", + "integrity": "sha512-OSZpOu+FGDbC/xivu24v092D9w6EGytB3vidwbdiJ2FaPgfV7rxS0WIUjH4I0OcvHAcitArRXL0a3+HrNTdQQw==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.12.0", + "@parcel/workers": "2.12.0", + "@swc/helpers": "^0.5.0", + "browserslist": "^4.6.6", + "nullthrows": "^1.1.1", + "regenerator-runtime": "^0.13.7", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@parcel/transformer-js/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/@parcel/transformer-js/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-json": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-json/-/transformer-json-2.12.0.tgz", + "integrity": "sha512-Utv64GLRCQILK5r0KFs4o7I41ixMPllwOLOhkdjJKvf1hZmN6WqfOmB1YLbWS/y5Zb/iB52DU2pWZm96vLFQZQ==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "json5": "^2.2.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-postcss": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-postcss/-/transformer-postcss-2.12.0.tgz", + "integrity": "sha512-FZqn+oUtiLfPOn67EZxPpBkfdFiTnF4iwiXPqvst3XI8H+iC+yNgzmtJkunOOuylpYY6NOU5jT8d7saqWSDv2Q==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/utils": "2.12.0", + "clone": "^2.1.1", + "nullthrows": "^1.1.1", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-postcss/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-posthtml": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-posthtml/-/transformer-posthtml-2.12.0.tgz", + "integrity": "sha512-z6Z7rav/pcaWdeD+2sDUcd0mmNZRUvtHaUGa50Y2mr+poxrKilpsnFMSiWBT+oOqPt7j71jzDvrdnAF4XkCljg==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "posthtml-parser": "^0.10.1", + "posthtml-render": "^3.0.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-posthtml/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/transformer-raw": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-raw/-/transformer-raw-2.12.0.tgz", + "integrity": "sha512-Ht1fQvXxix0NncdnmnXZsa6hra20RXYh1VqhBYZLsDfkvGGFnXIgO03Jqn4Z8MkKoa0tiNbDhpKIeTjyclbBxQ==", + "dependencies": { + "@parcel/plugin": "2.12.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-react-refresh-wrap": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.12.0.tgz", + "integrity": "sha512-GE8gmP2AZtkpBIV5vSCVhewgOFRhqwdM5Q9jNPOY5PKcM3/Ff0qCqDiTzzGLhk0/VMBrdjssrfZkVx6S/lHdJw==", + "dependencies": { + "@parcel/plugin": "2.12.0", + "@parcel/utils": "2.12.0", + "react-refresh": "^0.9.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-react-refresh-wrap/node_modules/react-refresh": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", + "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@parcel/transformer-svg": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.12.0.tgz", + "integrity": "sha512-cZJqGRJ4JNdYcb+vj94J7PdOuTnwyy45dM9xqbIMH+HSiiIkfrMsdEwYft0GTyFTdsnf+hdHn3tau7Qa5hhX+A==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/plugin": "2.12.0", + "@parcel/rust": "2.12.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "posthtml-parser": "^0.10.1", + "posthtml-render": "^3.0.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-svg/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@parcel/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.12.0.tgz", + "integrity": "sha512-8zAFiYNCwNTQcglIObyNwKfRYQK5ELlL13GuBOrSMxueUiI5ylgsGbTS1N7J3dAGZixHO8KhHGv5a71FILn9rQ==", + "dependencies": { + "@parcel/cache": "2.12.0", + "@parcel/diagnostic": "2.12.0", + "@parcel/fs": "2.12.0", + "@parcel/package-manager": "2.12.0", + "@parcel/source-map": "^2.1.1", + "@parcel/workers": "2.12.0", + "utility-types": "^3.10.0" + } + }, + "node_modules/@parcel/utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.12.0.tgz", + "integrity": "sha512-z1JhLuZ8QmDaYoEIuUCVZlhcFrS7LMfHrb2OCRui5SQFntRWBH2fNM6H/fXXUkT9SkxcuFP2DUA6/m4+Gkz72g==", + "dependencies": { + "@parcel/codeframe": "2.12.0", + "@parcel/diagnostic": "2.12.0", + "@parcel/logger": "2.12.0", + "@parcel/markdown-ansi": "2.12.0", + "@parcel/rust": "2.12.0", + "@parcel/source-map": "^2.1.1", + "chalk": "^4.1.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@parcel/utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@parcel/utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@parcel/utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@parcel/utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/workers": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-2.12.0.tgz", + "integrity": "sha512-zv5We5Jmb+ZWXlU6A+AufyjY4oZckkxsZ8J4dvyWL0W8IQvGO1JB4FGeryyttzQv3RM3OxcN/BpTGPiDG6keBw==", + "dependencies": { + "@parcel/diagnostic": "2.12.0", + "@parcel/logger": "2.12.0", + "@parcel/profiler": "2.12.0", + "@parcel/types": "2.12.0", + "@parcel/utils": "2.12.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.12.0" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.0.3.tgz", + "integrity": "sha512-duVGKeWPSUILr/MdlPxV+GeULTc2rS1aihGdQ3N2qCUPMgxYLxvAsHJM3mCVLF8d5eK+ympmB22mb1F3a5biNw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", + "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz", + "integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", + "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", + "integrity": "sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", + "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-rect": "1.0.1", + "@radix-ui/react-use-size": "1.0.1", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz", + "integrity": "sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==", + "dependencies": { + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", + "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz", + "integrity": "sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.4", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.3", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.2", + "@radix-ui/react-portal": "1.0.3", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", + "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", + "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", + "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-use-rect": "1.0.1", + "@radix-ui/react-use-size": "1.0.1", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", + "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz", + "integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", + "integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", + "integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", + "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", + "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", + "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", + "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", + "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, + "node_modules/@reactflow/background": { + "version": "11.3.13", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.13.tgz", + "integrity": "sha512-hkvpVEhgvfTDyCvdlitw4ioKCYLaaiRXnuEG+1QM3Np+7N1DiWF1XOv5I8AFyNoJL07yXEkbECUTsHvkBvcG5A==", + "dependencies": { + "@reactflow/core": "11.11.3", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.13", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.13.tgz", + "integrity": "sha512-3xgEg6ALIVkAQCS4NiBjb7ad8Cb3D8CtA7Vvl4Hf5Ar2PIVs6FOaeft9s2iDZGtsWP35ECDYId1rIFVhQL8r+A==", + "dependencies": { + "@reactflow/core": "11.11.3", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.3.tgz", + "integrity": "sha512-+adHdUa7fJSEM93fWfjQwyWXeI92a1eLKwWbIstoCakHpL8UjzwhEh6sn+mN2h/59MlVI7Ehr1iGTt3MsfcIFA==", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.13", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.13.tgz", + "integrity": "sha512-m2MvdiGSyOu44LEcERDEl1Aj6x//UQRWo3HEAejNU4HQTlJnYrSN8tgrYF8TxC1+c/9UdyzQY5VYgrTwW4QWdg==", + "dependencies": { + "@reactflow/core": "11.11.3", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.13.tgz", + "integrity": "sha512-X7ceQ2s3jFLgbkg03n2RYr4hm3jTVrzkW2W/8ANv/SZfuVmF8XJxlERuD8Eka5voKqLda0ywIZGAbw9GoHLfUQ==", + "dependencies": { + "@reactflow/core": "11.11.3", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.13.tgz", + "integrity": "sha512-aknvNICO10uWdthFSpgD6ctY/CTBeJUMV9co8T9Ilugr08Nb89IQ4uD0dPmr031ewMQxixtYIkw+sSDDzd2aaQ==", + "dependencies": { + "@reactflow/core": "11.11.3", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", + "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", + "dev": true + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz", + "integrity": "sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz", + "integrity": "sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz", + "integrity": "sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz", + "integrity": "sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz", + "integrity": "sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz", + "integrity": "sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-6.5.1.tgz", + "integrity": "sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "^6.5.1", + "@svgr/babel-plugin-remove-jsx-attribute": "*", + "@svgr/babel-plugin-remove-jsx-empty-expression": "*", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^6.5.1", + "@svgr/babel-plugin-svg-dynamic-title": "^6.5.1", + "@svgr/babel-plugin-svg-em-dimensions": "^6.5.1", + "@svgr/babel-plugin-transform-react-native-svg": "^6.5.1", + "@svgr/babel-plugin-transform-svg-component": "^6.5.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-6.5.1.tgz", + "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.19.6", + "@svgr/babel-preset": "^6.5.1", + "@svgr/plugin-jsx": "^6.5.1", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz", + "integrity": "sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.0", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz", + "integrity": "sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.19.6", + "@svgr/babel-preset": "^6.5.1", + "@svgr/hast-util-to-babel-ast": "^6.5.1", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "^6.0.0" + } + }, + "node_modules/@swc/core": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.28.tgz", + "integrity": "sha512-muCdNIqOTURUgYeyyOLYE3ShL8SZO6dw6bhRm6dCvxWzCZOncPc5fB0kjcPXTML+9KJoHL7ks5xg+vsQK+v6ig==", + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.8" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.5.28", + "@swc/core-darwin-x64": "1.5.28", + "@swc/core-linux-arm-gnueabihf": "1.5.28", + "@swc/core-linux-arm64-gnu": "1.5.28", + "@swc/core-linux-arm64-musl": "1.5.28", + "@swc/core-linux-x64-gnu": "1.5.28", + "@swc/core-linux-x64-musl": "1.5.28", + "@swc/core-win32-arm64-msvc": "1.5.28", + "@swc/core-win32-ia32-msvc": "1.5.28", + "@swc/core-win32-x64-msvc": "1.5.28" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.28.tgz", + "integrity": "sha512-sP6g63ybzIdOWNDbn51tyHN8EMt7Mb4RMeHQEsXB7wQfDvzhpWB+AbfK6Gs3Q8fwP/pmWIrWW9csKOc1K2Mmkg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.5.28.tgz", + "integrity": "sha512-Bd/agp/g7QocQG5AuorOzSC78t8OzeN+pCN/QvJj1CvPhvppjJw6e1vAbOR8vO2vvGi2pvtf3polrYQStJtSiA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.28.tgz", + "integrity": "sha512-Wr3TwPGIveS9/OBWm0r9VAL8wkCR0zQn46J8K01uYCmVhUNK3Muxjs0vQBZaOrGu94mqbj9OXY+gB3W7aDvGdA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.28.tgz", + "integrity": "sha512-8G1ZwVTuLgTAVTMPD+M97eU6WeiRIlGHwKZ5fiJHPBcz1xqIC7jQcEh7XBkobkYoU5OILotls3gzjRt8CMNyDQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.28.tgz", + "integrity": "sha512-0Ajdzb5Fzvz+XUbN5ESeHAz9aHHSYiQcm+vmsDi0TtPHmsalfnqEPZmnK0zPALPJPLQP2dDo4hELeDg3/c3xgA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.28.tgz", + "integrity": "sha512-ueQ9VejnQUM2Pt+vT0IAKoF4vYBWUP6n1KHGdILpoGe3LuafQrqu7RoyQ15C7/AYii7hAeNhTFdf6gLbg8cjFg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.28.tgz", + "integrity": "sha512-G5th8Mg0az8CbY4GQt9/m5hg2Y0kGIwvQBeVACuLQB6q2Y4txzdiTpjmFqUUhEvvl7Klyx1IHvNhfXs3zpt7PA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.28.tgz", + "integrity": "sha512-JezwCGavZ7CkNXx4yInI4kpb71L0zxzxA9BFlmnsGKEEjVQcKc3hFpmIzfFVs+eotlBUwDNb0+Yo9m6Cb7lllA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.28.tgz", + "integrity": "sha512-q8tW5J4RkOkl7vYShnWS//VAb2Ngolfm9WOMaF2GRJUr2Y/Xeb/+cNjdsNOqea2BzW049D5vdP7XPmir3/zUZw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.5.28", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.28.tgz", + "integrity": "sha512-jap6EiB3wG1YE1hyhNr9KLPpH4PGm+5tVMfN0l7fgKtV0ikgpcEN/YF94tru+z5m2HovqYW009+Evq9dcVGmpg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", + "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.8.tgz", + "integrity": "sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA==", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.15.1.tgz", + "integrity": "sha512-PnVV3d2poenUM31ZbZi/yXkBu3J7kd5k2u51CGwwNojag451AjTH9N6n41yjXz2fpLeewleyLBmNS6+HcGDlXw==", + "dependencies": { + "remove-accents": "0.5.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", + "integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.36.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", + "integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "4.36.1", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.17.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.17.3.tgz", + "integrity": "sha512-5gwg5SvPD3lNAXPuJJz1fOCEZYk9/GeBFH3w/hCgnfyszOIzwkwgp5I7Q4MJtn0WECp84b5STQUDdmvGi8m3nA==", + "dependencies": { + "@tanstack/table-core": "8.17.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.17.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.17.3.tgz", + "integrity": "sha512-mPBodDGVL+fl6d90wUREepHa/7lhsghg2A3vFpakEhrhtbIlgNAZiMr7ccTgak5qbHqF14Fwy+W1yFWQt+WmYQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.9.tgz", + "integrity": "sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", + "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", + "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/howler": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.11.tgz", + "integrity": "sha512-7aBoUL6RbSIrqKnpEgfa1wSNUBK06mn08siP2QI0zYk7MXfEJAaORc4tohamQYqCqVESoDyRWSdQn2BOKWj2Qw==", + "dev": true + }, + "node_modules/@types/js-levenshtein": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.3.tgz", + "integrity": "sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "dev": true + }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.19.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", + "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-color": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.12.tgz", + "integrity": "sha512-pr3uKE3lSvf7GFo1Rn2K3QktiZQFFrSgSGJ/3iMvSOYWt2pPAJ97rVdVfhWxYJZ8prAEXzoP2XX//3qGSQgu7Q==", + "dev": true, + "dependencies": { + "@types/react": "*", + "@types/reactcss": "*" + } + }, + "node_modules/@types/react-datepicker": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.19.6.tgz", + "integrity": "sha512-uH5fzxt9eXxnc+hDCy/iRSFqU2+9lR/q2lAmaG4WILMai1o3IOdpcV+VSypzBFJLTEC2jrfeDXcdol0CJVMq4g==", + "dev": true, + "dependencies": { + "@popperjs/core": "^2.9.2", + "@types/react": "*", + "date-fns": "^2.0.1", + "react-popper": "^2.2.5" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/reactcss": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.12.tgz", + "integrity": "sha512-BrXUQ86/wbbFiZv8h/Q1/Q1XOsaHneYmCb/tHe9+M8XBAAUc2EHfdY0DY22ZZjVSaXr5ix7j+zsqO2eGZub8lQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.9.tgz", + "integrity": "sha512-bCorlULvl0xTdjj4BPUHX4cqs9I+go2TfW/7Do1nnFYWS0CPP429Qr1AY42kiFhCwLpvAkWFr1XIBHd8j6/MCQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", + "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", + "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.12", + "@babel/plugin-transform-react-jsx-self": "^7.18.6", + "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "magic-string": "^0.27.0", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.1.0-beta.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "dev": true, + "optional": true + }, + "node_modules/abortcontroller-polyfill": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz", + "integrity": "sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==", + "dev": true + }, + "node_modules/babel-preset-react-app": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", + "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/plugin-proposal-class-properties": "^7.16.0", + "@babel/plugin-proposal-decorators": "^7.16.4", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", + "@babel/plugin-proposal-numeric-separator": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@babel/plugin-proposal-private-methods": "^7.16.0", + "@babel/plugin-transform-flow-strip-types": "^7.16.0", + "@babel/plugin-transform-react-display-name": "^7.16.0", + "@babel/plugin-transform-runtime": "^7.16.4", + "@babel/preset-env": "^7.16.4", + "@babel/preset-react": "^7.16.0", + "@babel/preset-typescript": "^7.16.0", + "@babel/runtime": "^7.16.3", + "babel-plugin-macros": "^3.1.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001632", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz", + "integrity": "sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==" + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz", + "integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-7.0.0.tgz", + "integrity": "sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" + }, + "node_modules/downshift": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.2.tgz", + "integrity": "sha512-iOv+E1Hyt3JDdL9yYcOgW7nZ7GQ2Uz6YbggwXvKUSleetYhU2nXD482Rz6CzvM4lvI1At34BYruKAL4swRGxaA==", + "dependencies": { + "@babel/runtime": "^7.14.8", + "compute-scroll-into-view": "^2.0.4", + "prop-types": "^15.7.2", + "react-is": "^17.0.2", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.799", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.799.tgz", + "integrity": "sha512-3D3DwWkRTzrdEpntY0hMLYwj7SeBk1138CkPE8sBDSj3WzrzOiG2rHm3luw8jucpf+WiyLBCZyU9lMHyQI9M9Q==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-react-app": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", + "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.16.0", + "@babel/eslint-parser": "^7.16.3", + "@rushstack/eslint-patch": "^1.1.0", + "@typescript-eslint/eslint-plugin": "^5.5.0", + "@typescript-eslint/parser": "^5.5.0", + "babel-preset-react-app": "^10.0.1", + "confusing-browser-globals": "^1.0.11", + "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.3.0", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", + "eslint-plugin-testing-library": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.0" + } + }, + "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-react-app/node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-react-app/node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-config-react-app/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-flowtype": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", + "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21", + "string-natural-compare": "^3.0.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@babel/plugin-syntax-flow": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.9", + "eslint": "^8.1.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", + "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "^5.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-testing-library": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz", + "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.58.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-typescript": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-typescript/-/eslint-plugin-typescript-0.14.0.tgz", + "integrity": "sha512-2u1WnnDF2mkWWgU1lFQ2RjypUlmRoBEvQN02y9u+IL12mjWlkKFGEBnVsjs9Y8190bfPQCvWly1c2rYYUSOxWw==", + "deprecated": "Deprecated: Use @typescript-eslint/eslint-plugin instead", + "dev": true, + "dependencies": { + "requireindex": "~1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formik": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", + "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/formik/node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, + "node_modules/framer-motion": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-8.5.5.tgz", + "integrity": "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA==", + "dependencies": { + "@motionone/dom": "^10.15.3", + "hey-listen": "^1.0.8", + "tslib": "^2.4.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-port": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", + "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/graphql": { + "version": "16.8.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.2.tgz", + "integrity": "sha512-cvVIBILwuoSyD54U4cF/UXDh5yAobhNV/tPygI4lZhgOIJQE/WLWC4waBRb4I6bDVYb3OVx3lfHbaQOEoUD5sg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.3.0.tgz", + "integrity": "sha512-5e57etwBpNcDc0b6KCVWEh/Ro063OxPvzVimUdM0/tsYM/T7Hfy3kknIGj78SFTOhNd8AZY41U8mOHoO4LzmIQ==", + "dev": true + }, + "node_modules/hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/howler": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz", + "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/htmlnano": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.1.tgz", + "integrity": "sha512-kAERyg/LuNZYmdqgCdYvugyLWNFAm8MWXpQMz1pLpetmCbFwoMxvkSoaAMlFrOC4OKTWI4KlZGT/RsNxg4ghOw==", + "dependencies": { + "cosmiconfig": "^9.0.0", + "posthtml": "^0.16.5", + "timsort": "^0.3.0" + }, + "peerDependencies": { + "cssnano": "^7.0.0", + "postcss": "^8.3.11", + "purgecss": "^6.0.0", + "relateurl": "^0.2.7", + "srcset": "5.0.1", + "svgo": "^3.0.2", + "terser": "^5.10.0", + "uncss": "^0.17.3" + }, + "peerDependenciesMeta": { + "cssnano": { + "optional": true + }, + "postcss": { + "optional": true + }, + "purgecss": { + "optional": true + }, + "relateurl": { + "optional": true + }, + "srcset": { + "optional": true + }, + "svgo": { + "optional": true + }, + "terser": { + "optional": true + }, + "uncss": { + "optional": true + } + } + }, + "node_modules/htmlnano/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, + "node_modules/i18next": { + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.5.1.tgz", + "integrity": "sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.20.6" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz", + "integrity": "sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-json": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", + "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==" + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.25.1.tgz", + "integrity": "sha512-V0RMVZzK1+rCHpymRv4URK2lNhIRyO8g7U7zOFwVAhJuat74HtkjIQpQRKNCwFEYkRGpafOpmXXLoaoBcyVtBg==", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.25.1", + "lightningcss-darwin-x64": "1.25.1", + "lightningcss-freebsd-x64": "1.25.1", + "lightningcss-linux-arm-gnueabihf": "1.25.1", + "lightningcss-linux-arm64-gnu": "1.25.1", + "lightningcss-linux-arm64-musl": "1.25.1", + "lightningcss-linux-x64-gnu": "1.25.1", + "lightningcss-linux-x64-musl": "1.25.1", + "lightningcss-win32-x64-msvc": "1.25.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.25.1.tgz", + "integrity": "sha512-G4Dcvv85bs5NLENcu/s1f7ehzE3D5ThnlWSDwE190tWXRQCQaqwcuHe+MGSVI/slm0XrxnaayXY+cNl3cSricw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.25.1.tgz", + "integrity": "sha512-dYWuCzzfqRueDSmto6YU5SoGHvZTMU1Em9xvhcdROpmtOQLorurUZz8+xFxZ51lCO2LnYbfdjZ/gCqWEkwixNg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.25.1.tgz", + "integrity": "sha512-hXoy2s9A3KVNAIoKz+Fp6bNeY+h9c3tkcx1J3+pS48CqAt+5bI/R/YY4hxGL57fWAIquRjGKW50arltD6iRt/w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.25.1.tgz", + "integrity": "sha512-tWyMgHFlHlp1e5iW3EpqvH5MvsgoN7ZkylBbG2R2LWxnvH3FuWCJOhtGcYx9Ks0Kv0eZOBud789odkYLhyf1ng==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.25.1.tgz", + "integrity": "sha512-Xjxsx286OT9/XSnVLIsFEDyDipqe4BcLeB4pXQ/FEA5+2uWCCuAEarUNQumRucnj7k6ftkAHUEph5r821KBccQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.25.1.tgz", + "integrity": "sha512-IhxVFJoTW8wq6yLvxdPvyHv4NjzcpN1B7gjxrY3uaykQNXPHNIpChLB52+wfH+yS58zm1PL4LemUp8u9Cfp6Bw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.25.1.tgz", + "integrity": "sha512-RXIaru79KrREPEd6WLXfKfIp4QzoppZvD3x7vuTKkDA64PwTzKJ2jaC43RZHRt8BmyIkRRlmywNhTRMbmkPYpA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.25.1.tgz", + "integrity": "sha512-TdcNqFsAENEEFr8fJWg0Y4fZ/nwuqTRsIr7W7t2wmDUlA8eSXVepeeONYcb+gtTj1RaXn/WgNLB45SFkz+XBZA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.25.1.tgz", + "integrity": "sha512-9KZZkmmy9oGDSrnyHuxP6iMhbsgChUiu/NSgOx+U1I/wTngBStDf2i2aGRCHvFqj19HqqBEI4WuGVQBa2V6e0A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/linkify-react": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz", + "integrity": "sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==", + "peerDependencies": { + "linkifyjs": "^4.0.0", + "react": ">= 15.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz", + "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==" + }, + "node_modules/lmdb": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-2.8.5.tgz", + "integrity": "sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==", + "hasInstallScript": true, + "dependencies": { + "msgpackr": "^1.9.5", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.1.1", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "2.8.5", + "@lmdb/lmdb-darwin-x64": "2.8.5", + "@lmdb/lmdb-linux-arm": "2.8.5", + "@lmdb/lmdb-linux-arm64": "2.8.5", + "@lmdb/lmdb-linux-x64": "2.8.5", + "@lmdb/lmdb-win32-x64": "2.8.5" + } + }, + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mocksse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mocksse/-/mocksse-1.0.4.tgz", + "integrity": "sha512-W5DR/wwmx/EZUgjN1g+pvlhvFFtRJ3CqGRKqsK/B1hTxrjMb/t3JCbk6aomJD4WomrnueqMaTAhcAkIZJYd73w==", + "dev": true + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/msgpackr": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.2.tgz", + "integrity": "sha512-L60rsPynBvNE+8BWipKKZ9jHcSGbtyJYIwjRq0VrIvQ08cRjntGXJYW/tmciZ2IHWIY8WEW32Qa2xbh5+SKBZA==", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/msgpackr-extract/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/msgpackr-extract/node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/msw": { + "version": "0.49.3", + "resolved": "https://registry.npmjs.org/msw/-/msw-0.49.3.tgz", + "integrity": "sha512-kRCbDNbNnRq5LC1H/NUceZlrPAvSrMH6Or0mirIuH69NY84xwDruPn/hkXTovIK1KwDwbk+ZdoSyJlpiekLxEA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@mswjs/cookies": "^0.2.2", + "@mswjs/interceptors": "^0.17.5", + "@open-draft/until": "^1.0.3", + "@types/cookie": "^0.4.1", + "@types/js-levenshtein": "^1.1.1", + "chalk": "4.1.1", + "chokidar": "^3.4.2", + "cookie": "^0.4.2", + "graphql": "^15.0.0 || ^16.0.0", + "headers-polyfill": "^3.1.0", + "inquirer": "^8.2.0", + "is-node-process": "^1.0.1", + "js-levenshtein": "^1.1.6", + "node-fetch": "^2.6.7", + "outvariant": "^1.3.0", + "path-to-regexp": "^6.2.0", + "strict-event-emitter": "^0.4.3", + "type-fest": "^2.19.0", + "yargs": "^17.3.1" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.4.x <= 4.9.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/msw/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/msw/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/msw/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/msw/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/msw/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "engines": { + "node": "^16 || ^18 || >= 20" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp-build-optional-packages/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ordered-binary": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.1.tgz", + "integrity": "sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/outvariant": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.2.tgz", + "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==", + "dev": true + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parcel": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/parcel/-/parcel-2.12.0.tgz", + "integrity": "sha512-W+gxAq7aQ9dJIg/XLKGcRT0cvnStFAQHPaI0pvD0U2l6IVLueUAm3nwN7lkY62zZNmlvNx6jNtE4wlbS+CyqSg==", + "dependencies": { + "@parcel/config-default": "2.12.0", + "@parcel/core": "2.12.0", + "@parcel/diagnostic": "2.12.0", + "@parcel/events": "2.12.0", + "@parcel/fs": "2.12.0", + "@parcel/logger": "2.12.0", + "@parcel/package-manager": "2.12.0", + "@parcel/reporter-cli": "2.12.0", + "@parcel/reporter-dev-server": "2.12.0", + "@parcel/reporter-tracer": "2.12.0", + "@parcel/utils": "2.12.0", + "chalk": "^4.1.0", + "commander": "^7.0.0", + "get-port": "^4.2.0" + }, + "bin": { + "parcel": "lib/bin.js" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/parcel/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/parcel/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/parcel/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/parcel/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/parcel/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/parcel/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/posthtml": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.6.tgz", + "integrity": "sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==", + "dependencies": { + "posthtml-parser": "^0.11.0", + "posthtml-render": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/posthtml-parser": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.10.2.tgz", + "integrity": "sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg==", + "dependencies": { + "htmlparser2": "^7.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/posthtml-render": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz", + "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==", + "dependencies": { + "is-json": "^2.0.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/posthtml/node_modules/posthtml-parser": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz", + "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==", + "dependencies": { + "htmlparser2": "^7.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-color": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", + "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==", + "dependencies": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-cookie": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz", + "integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.0.1", + "hoist-non-react-statics": "^3.0.0", + "universal-cookie": "^4.0.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, + "node_modules/react-datepicker": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz", + "integrity": "sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==", + "dependencies": { + "@popperjs/core": "^2.11.8", + "classnames": "^2.2.6", + "date-fns": "^2.30.0", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0", + "react-popper": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-error-overlay": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", + "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==" + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "node_modules/react-hook-form": { + "version": "7.52.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz", + "integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-i18next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.3.1.tgz", + "integrity": "sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==", + "dependencies": { + "@babel/runtime": "^7.20.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-idle-timer": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-5.7.2.tgz", + "integrity": "sha512-+BaPfc7XEUU5JFkwZCx6fO1bLVK+RBlFH+iY4X34urvIzZiZINP6v2orePx3E6pAztJGE7t4DzvL7if2SL/0GQ==", + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + } + }, + "node_modules/react-onclickoutside": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.1.tgz", + "integrity": "sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-redux": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-redux/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-select": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", + "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-text-selection-popover": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-text-selection-popover/-/react-text-selection-popover-2.0.2.tgz", + "integrity": "sha512-VbQnJMHX6GrMRS5QGQnb8YuFL45JRcosraTJjdmjib4Xt9MOcTHXmuIyI12xbG2QZv2Tsa+aOZvYgTlo8I00dA==", + "dependencies": { + "use-text-selection": "^1.1.3" + }, + "peerDependencies": { + "react": "^16.8.0,^17.x,^18.x", + "react-dom": "^16.8.0,^17.x,^18.x" + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", + "integrity": "sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "dependencies": { + "lodash": "^4.0.1" + } + }, + "node_modules/reactflow": { + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.3.tgz", + "integrity": "sha512-wusd1Xpn1wgsSEv7UIa4NNraCwH9syBtubBy4xVNXg3b+CDKM+sFaF3hnMx0tr0et4km9urIDdNvwm34QiZong==", + "dependencies": { + "@reactflow/background": "11.3.13", + "@reactflow/controls": "11.2.13", + "@reactflow/core": "11.11.3", + "@reactflow/minimap": "11.7.13", + "@reactflow/node-resizer": "2.2.13", + "@reactflow/node-toolbar": "1.3.13" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexify-string": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/regexify-string/-/regexify-string-1.0.19.tgz", + "integrity": "sha512-EREOggl31J6v2Hk3ksPuOof0DMq5QhFfVQ7iDaGQ6BeA1QcrV4rhGvwCES5a72ITMmLBDAOb6cOWbn8/Ja82Ig==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==", + "dev": true, + "engines": { + "node": ">=0.10.5" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sass": { + "version": "1.77.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.5.tgz", + "integrity": "sha512-oDfX1mukIlxacPdQqNb6mV2tVCrnE+P3nVYioy72V5tlk56CPNcO4TCuFcaCRKKfJ1M3lH95CleRS+dVKL2qMg==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/srcset": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", + "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" + }, + "node_modules/strict-event-emitter": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz", + "integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-natural-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", + "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, + "node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/term-size": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/timeago.js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz", + "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==" + }, + "node_modules/timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==" + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfck": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz", + "integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/universal-cookie": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", + "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "dependencies": { + "@types/cookie": "^0.3.3", + "cookie": "^0.4.0" + } + }, + "node_modules/universal-cookie/node_modules/@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-composed-ref": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", + "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", + "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/use-text-selection": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/use-text-selection/-/use-text-selection-1.1.5.tgz", + "integrity": "sha512-JOuQYG0vKHRj0dfax0dy/HxyF31MN0Q2UP1rl1LtFA0qnQ0Uw4XGh4BucHA9g8kxlnVFv+JTlJQ4B+TwXCGxOg==", + "dependencies": { + "parcel": "^2.0.0-beta.2" + }, + "peerDependencies": { + "react": "^17.0.1" + } + }, + "node_modules/usehooks-ts": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.16.0.tgz", + "integrity": "sha512-bez95WqYujxp6hFdM/CpRDiVPirZPxlMzOH2QB8yopoKQMXpscyZoxOjpEdaxvV+CAWUDSM62cWnqHE0E/MZ7w==", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-env-compatible": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vite-plugin-env-compatible/-/vite-plugin-env-compatible-1.1.1.tgz", + "integrity": "sha512-4lqhBWhOzP+SaCPoCVdmpM5cXzjKQV5jgFauxea488oOeElXo/kw6bXkMIooZhrh9q7gclTl8en6N9NmnqUwRQ==", + "dev": true + }, + "node_modules/vite-plugin-svgr": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-2.4.0.tgz", + "integrity": "sha512-q+mJJol6ThvqkkJvvVFEndI4EaKIjSI0I3jNFgSoC9fXAz1M7kYTVUin8fhUsFojFDKZ9VHKtX6NXNaOLpbsHA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.2", + "@svgr/core": "^6.5.1" + }, + "peerDependencies": { + "vite": "^2.6.0 || 3 || 4" + } + }, + "node_modules/vite-plugin-transform": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vite-plugin-transform/-/vite-plugin-transform-2.0.1.tgz", + "integrity": "sha512-sI9SzcuFbCj04YHEmhw9C14kNnVq3QFLWq7eofjNnDWnw/p+i+6pnSvVZSx1GDVpW1ciZglrv794XEU/lGGvyA==", + "dev": true + }, + "node_modules/vite-tsconfig-paths": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==" + }, + "node_modules/web-encoding": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", + "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", + "dev": true, + "dependencies": { + "util": "^0.12.3" + }, + "optionalDependencies": { + "@zxing/text-encoding": "0.9.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/zustand": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zustand/node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + } + } +} diff --git a/GUI/package.json b/GUI/package.json new file mode 100644 index 00000000..09ab4a81 --- /dev/null +++ b/GUI/package.json @@ -0,0 +1,117 @@ +{ + "name": "byk-training-module-gui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 3001 --host", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "tsc --noEmit && eslint \"./src/**/*.{js,ts,tsx}\"", + "prettier": "prettier --write \"{,!(node_modules)/**/}*.{ts,tsx,js,json,css,less,scss}\"" + }, + "dependencies": { + "@buerokratt-ria/styles": "^0.0.1", + "@fontsource/roboto": "^4.5.8", + "@formkit/auto-animate": "^1.0.0-beta.5", + "@fortaine/fetch-event-source": "^3.0.6", + "@radix-ui/react-accessible-icon": "^1.0.1", + "@radix-ui/react-collapsible": "^1.0.1", + "@radix-ui/react-dialog": "^1.0.2", + "@radix-ui/react-popover": "^1.0.2", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-select": "^1.1.2", + "@radix-ui/react-switch": "^1.0.1", + "@radix-ui/react-tabs": "^1.0.1", + "@radix-ui/react-toast": "^1.1.2", + "@radix-ui/react-tooltip": "^1.0.2", + "@tanstack/match-sorter-utils": "^8.7.2", + "@tanstack/react-query": "^4.36.1", + "@tanstack/react-table": "^8.7.4", + "axios": "^1.2.1", + "clsx": "^1.2.1", + "date-fns": "^2.29.3", + "downshift": "^7.0.5", + "esbuild": "^0.19.5", + "formik": "^2.4.6", + "framer-motion": "^8.5.5", + "howler": "^2.2.4", + "i18next": "^22.4.5", + "i18next-browser-languagedetector": "^7.0.1", + "linkify-react": "^4.1.1", + "linkifyjs": "^4.1.1", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "react": "^18.2.0", + "react-color": "^2.19.3", + "react-cookie": "^4.1.1", + "react-datepicker": "^4.8.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dom": "^18.2.0", + "react-hook-form": "^7.52.1", + "react-i18next": "^12.1.1", + "react-icons": "^4.10.1", + "react-idle-timer": "^5.5.2", + "react-modal": "^3.16.1", + "react-redux": "^8.1.1", + "react-router-dom": "^6.5.0", + "react-select": "^5.7.4", + "react-text-selection-popover": "^2.0.2", + "react-textarea-autosize": "^8.4.0", + "reactflow": "^11.4.0", + "regexify-string": "^1.0.19", + "rxjs": "^7.8.1", + "timeago.js": "^4.0.2", + "usehooks-ts": "^2.9.1", + "uuid": "^9.0.0", + "yup": "^1.4.0", + "zustand": "^4.4.4" + }, + "devDependencies": { + "@types/howler": "^2.2.11", + "@types/lodash": "^4.14.191", + "@types/lodash.debounce": "^4.0.7", + "@types/node": "^18.11.17", + "@types/react": "^18.0.26", + "@types/react-color": "^3.0.6", + "@types/react-datepicker": "^4.8.0", + "@types/react-dom": "^18.0.9", + "@types/uuid": "^9.0.2", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", + "@vitejs/plugin-react": "^3.0.0", + "eslint": "^8.57.1", + "eslint-config-react-app": "^7.0.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-typescript": "^0.14.0", + "mocksse": "^1.0.4", + "msw": "^0.49.2", + "prettier": "^2.8.1", + "sass": "^1.57.0", + "typescript": "^4.9.3", + "vite": "^4.0.0", + "vite-plugin-env-compatible": "^1.1.1", + "vite-plugin-svgr": "^2.4.0", + "vite-plugin-transform": "^2.0.1", + "vite-tsconfig-paths": "^4.0.3" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "msw": { + "workerDirectory": "public" + } +} diff --git a/GUI/public/favicon.ico b/GUI/public/favicon.ico new file mode 100644 index 00000000..b9d127c2 Binary files /dev/null and b/GUI/public/favicon.ico differ diff --git a/GUI/public/mockServiceWorker.js b/GUI/public/mockServiceWorker.js new file mode 100644 index 00000000..671ec2cb --- /dev/null +++ b/GUI/public/mockServiceWorker.js @@ -0,0 +1,303 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (0.49.3). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70' +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: INTEGRITY_CHECKSUM, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + const accept = request.headers.get('accept') || '' + + // Bypass server-sent events. + if (accept.includes('text/event-stream')) { + return + } + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = Math.random().toString(16).slice(2) + + event.respondWith( + handleRequest(event, requestId).catch((error) => { + if (error.name === 'NetworkError') { + console.warn( + '[MSW] Successfully emulated a network error for the "%s %s" request.', + request.method, + request.url, + ) + return + } + + // At this point, any exception indicates an issue with the original request/response. + console.error( + `\ +[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, + request.method, + request.url, + `${error.name}: ${error.message}`, + ) + }), + ) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const clonedResponse = response.clone() + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: + clonedResponse.body === null ? null : await clonedResponse.text(), + headers: Object.fromEntries(clonedResponse.headers.entries()), + redirected: clonedResponse.redirected, + }, + }) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + const clonedRequest = request.clone() + + function passthrough() { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const headers = Object.fromEntries(clonedRequest.headers.entries()) + + // Remove MSW-specific request headers so the bypassed requests + // comply with the server's CORS preflight check. + // Operate with the headers as an object because request "Headers" + // are immutable. + delete headers['x-msw-bypass'] + + return fetch(clonedRequest, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + if (request.headers.get('x-msw-bypass') === 'true') { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const clientMessage = await sendToClient(client, { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + mode: request.mode, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.text(), + bodyUsed: request.bodyUsed, + keepalive: request.keepalive, + }, + }) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'MOCK_NOT_FOUND': { + return passthrough() + } + + case 'NETWORK_ERROR': { + const { name, message } = clientMessage.data + const networkError = new Error(message) + networkError.name = name + + // Rejecting a "respondWith" promise emulates a network error. + throw networkError + } + } + + return passthrough() +} + +function sendToClient(client, message) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2]) + }) +} + +function sleep(timeMs) { + return new Promise((resolve) => { + setTimeout(resolve, timeMs) + }) +} + +async function respondWithMock(response) { + await sleep(response.delay) + return new Response(response.body, response) +} diff --git a/GUI/rebuild.sh b/GUI/rebuild.sh new file mode 100644 index 00000000..c83c0b8b --- /dev/null +++ b/GUI/rebuild.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# Install dependencies +apk add nodejs + +# Rebuild the project +cd /opt/buerokratt-classifier +./node_modules/.bin/vite build -l warn +cp -ru build/* /usr/share/nginx/html/buerokratt-classifier + +# Start the Nginx server +nginx -g "daemon off;" diff --git a/GUI/src/App.tsx b/GUI/src/App.tsx new file mode 100644 index 00000000..9b2e8d21 --- /dev/null +++ b/GUI/src/App.tsx @@ -0,0 +1,86 @@ +import { FC, useEffect, useState } from 'react'; +import { Route, Routes, useNavigate, useLocation } from 'react-router-dom'; +import { Layout } from 'components'; +import useStore from 'store'; +import UserManagement from 'pages/UserManagement'; +import { useQuery } from '@tanstack/react-query'; +import { UserInfo } from 'types/userInfo'; +import { authQueryKeys } from 'utils/queryKeys'; +import { ROLES } from 'enums/roles'; +import LoadingScreen from 'pages/LoadingScreen/LoadingScreen'; +import Unauthorized from 'pages/Unauthorized/unauthorized'; +import IntegratedAgencies from 'pages/IntegratedAgencies'; +import Datasets from 'pages/Datasets'; +import ViewDataset from 'pages/ViewDataset'; +import DataModels from 'pages/DataModels'; +import CreateDataModel from 'pages/DataModels/CreateDataModel'; +import ConfigureDataModel from 'pages/DataModels/ConfigureDataModel'; +import TrainingSessions from 'pages/TrainingSessions'; +import TestModel from 'pages/TestModel'; +import DataGenerationSessions from 'pages/DataGenerationSessions'; + +const App: FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const [hasRedirected, setHasRedirected] = useState(false); + const { isLoading, data } = useQuery({ + queryKey: authQueryKeys.USER_DETAILS(), + + onSuccess: (res: { response: UserInfo }) => { + localStorage.setItem('exp', res.response.JWTExpirationTimestamp); + useStore.getState().setUserInfo(res.response); + }, + }); + + useEffect(() => { + if (!isLoading && data && !hasRedirected && location.pathname === '/') { + const isAdmin = (data as { response: UserInfo }).response.authorities.some( + (item) => item === ROLES.ROLE_ADMINISTRATOR + ); + if (isAdmin) { + navigate('/user-management'); + } else { + navigate('/dataset-groups'); + } + setHasRedirected(true); + } + }, [isLoading, data, navigate, hasRedirected, location.pathname]); + + return ( + <> + {isLoading ? ( + + ) : ( + + }> + {(data as { response: UserInfo })?.response.authorities.some( + (item) => item === ROLES.ROLE_ADMINISTRATOR + ) ? ( + <> + } /> + } /> + + + ) : ( + <> + } /> + } /> + + )} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + )} + + ); +}; + +export default App; diff --git a/GUI/src/assets/BackArrowButton.tsx b/GUI/src/assets/BackArrowButton.tsx new file mode 100644 index 00000000..e8e60eb8 --- /dev/null +++ b/GUI/src/assets/BackArrowButton.tsx @@ -0,0 +1,31 @@ +const BackArrowButton = () => { + return ( + + + + + + + + + + + + ); +}; + +export default BackArrowButton; diff --git a/GUI/src/assets/DataModelsIcon.tsx b/GUI/src/assets/DataModelsIcon.tsx new file mode 100644 index 00000000..855dd73f --- /dev/null +++ b/GUI/src/assets/DataModelsIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const DataModelsIcon = () => { + return ( + + + + ); +}; + +export default DataModelsIcon; diff --git a/GUI/src/assets/DatabaseIcon.tsx b/GUI/src/assets/DatabaseIcon.tsx new file mode 100644 index 00000000..5ab9d3b5 --- /dev/null +++ b/GUI/src/assets/DatabaseIcon.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +const DatabaseIcon = () => { + return ( + + + + + + ); +}; + +export default DatabaseIcon; diff --git a/GUI/src/assets/Dataset.tsx b/GUI/src/assets/Dataset.tsx new file mode 100644 index 00000000..6b46aff4 --- /dev/null +++ b/GUI/src/assets/Dataset.tsx @@ -0,0 +1,18 @@ +const Dataset = () => { + return ( + + + + ); +}; + +export default Dataset; diff --git a/GUI/src/assets/IncomingTextsIcon.tsx b/GUI/src/assets/IncomingTextsIcon.tsx new file mode 100644 index 00000000..fb6ccb9d --- /dev/null +++ b/GUI/src/assets/IncomingTextsIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const IncomingTextsIcon = () => { + return ( + + + + ); +}; + +export default IncomingTextsIcon; diff --git a/GUI/src/assets/IntegrationIcon.tsx b/GUI/src/assets/IntegrationIcon.tsx new file mode 100644 index 00000000..5553ea54 --- /dev/null +++ b/GUI/src/assets/IntegrationIcon.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +const IntegrationIcon = () => { + return ( + + + + + + + + + + + + ); +}; + +export default IntegrationIcon; diff --git a/GUI/src/assets/Jira.tsx b/GUI/src/assets/Jira.tsx new file mode 100644 index 00000000..37088791 --- /dev/null +++ b/GUI/src/assets/Jira.tsx @@ -0,0 +1,55 @@ +const Jira = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; +export default Jira; diff --git a/GUI/src/assets/Outlook.tsx b/GUI/src/assets/Outlook.tsx new file mode 100644 index 00000000..5eb0ebbc --- /dev/null +++ b/GUI/src/assets/Outlook.tsx @@ -0,0 +1,25 @@ +const Outlook = () => { + return ( + + + + + + ); +}; +export default Outlook; diff --git a/GUI/src/assets/SearchIcon.tsx b/GUI/src/assets/SearchIcon.tsx new file mode 100644 index 00000000..60e66761 --- /dev/null +++ b/GUI/src/assets/SearchIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const SearchIcon = () => { + return ( + + + + ); +}; + +export default SearchIcon; diff --git a/GUI/src/assets/TestModelIcon.tsx b/GUI/src/assets/TestModelIcon.tsx new file mode 100644 index 00000000..6b9c45f6 --- /dev/null +++ b/GUI/src/assets/TestModelIcon.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +const TestModelIcon = () => { + return ( + + + + + + + + + + + ); +}; + +export default TestModelIcon; diff --git a/GUI/src/assets/UserIcon.tsx b/GUI/src/assets/UserIcon.tsx new file mode 100644 index 00000000..83c84c0c --- /dev/null +++ b/GUI/src/assets/UserIcon.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +const UserIcon = () => { + return ( + + + + + ); +}; + +export default UserIcon; diff --git a/GUI/src/assets/logo-white.svg b/GUI/src/assets/logo-white.svg new file mode 100644 index 00000000..20257361 --- /dev/null +++ b/GUI/src/assets/logo-white.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + diff --git a/GUI/src/assets/logo.svg b/GUI/src/assets/logo.svg new file mode 100644 index 00000000..6039e9b5 --- /dev/null +++ b/GUI/src/assets/logo.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/GUI/src/assets/newMessageSound.mp3 b/GUI/src/assets/newMessageSound.mp3 new file mode 100644 index 00000000..9400b22a Binary files /dev/null and b/GUI/src/assets/newMessageSound.mp3 differ diff --git a/GUI/src/components/Box/Box.scss b/GUI/src/components/Box/Box.scss new file mode 100644 index 00000000..8801c053 --- /dev/null +++ b/GUI/src/components/Box/Box.scss @@ -0,0 +1,56 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.box { + padding: get-spacing(paldiski); + border-radius: 4px; + border: 1px solid; + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + + &:hover { + cursor: grab; + } + + &--default { + background-color: get-color(black-coral-1); + border-color: get-color(black-coral-3); + } + + &--blue { + background-color: get-color(sapphire-blue-0); + border-color: get-color(sapphire-blue-2); + } + + &--yellow { + background-color: get-color(dark-tangerine-0); + border-color: get-color(dark-tangerine-4); + } + + &--green { + background-color: get-color(sea-green-1); + border-color: get-color(sea-green-3); + } + + &--red { + background-color: get-color(jasper-1); + border-color: get-color(jasper-3); + } + + &--gray { + background-color: get-color(black-coral-1); + border-color: get-color(black-coral-3); + } + + &--dark-blue { + background-color: get-color(sapphire-blue-3); + border-color: get-color(sapphire-blue-5); + } + + &--orange { + background-color: get-color(orange-3); + border-color: get-color(orange-5); + } +} diff --git a/GUI/src/components/Box/index.tsx b/GUI/src/components/Box/index.tsx new file mode 100644 index 00000000..df4d3992 --- /dev/null +++ b/GUI/src/components/Box/index.tsx @@ -0,0 +1,16 @@ +import { forwardRef, PropsWithChildren } from 'react'; +import clsx from 'clsx'; + +import './Box.scss'; + +type BoxProps = { + color?: 'default' | 'blue' | 'yellow' | 'green' | 'red' | 'gray' | 'dark-blue' | 'orange'; +} + +const Box = forwardRef>(({ color = 'default', children }, ref) => { + return ( +
{children}
+ ); +}); + +export default Box; diff --git a/GUI/src/components/Button/Button.scss b/GUI/src/components/Button/Button.scss new file mode 100644 index 00000000..217c2c03 --- /dev/null +++ b/GUI/src/components/Button/Button.scss @@ -0,0 +1,151 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.btn { + $self: &; + appearance: none; + display: inline-flex; + align-items: center; + background: none; + border: 0; + color: get-color(black-coral-0); + cursor: pointer; + font: inherit; + gap: get-spacing(rapla); + overflow: visible; + padding: 8px 40px; + text-decoration: none; + font-size: $veera-font-size-100; + line-height: 24px; + border-radius: 20px; + white-space: nowrap; + height: fit-content; + + &:focus { + outline: none; + } + + &--disabled { + cursor: not-allowed; + } + + &--primary { + background-color: get-color(sapphire-blue-10); + + &:hover, + &:active { + background-color: get-color(sapphire-blue-13); + } + + &:focus { + box-shadow: inset 0 0 0 2px get-color(sapphire-blue-3) + } + + &#{$self}--disabled { + background-color: get-color(black-coral-2); + color: get-color(white); + } + } + + &--secondary { + background-color: get-color(white); + box-shadow: inset 0 0 0 2px get-color(sapphire-blue-10); + color: get-color(sapphire-blue-10); + + &:hover, + &:active { + box-shadow: inset 0 0 0 2px get-color(black-coral-2); + } + + &:focus { + box-shadow: inset 0 0 0 2px get-color(sapphire-blue-10); + } + + &#{$self}--disabled { + background-color: get-color(black-coral-2); + color: get-color(black-coral-6); + box-shadow: inset 0 0 0 2px get-color(black-coral-2); + } + } + + &--text { + padding: 0; + background: none; + color: get-color(sapphire-blue-10); + gap: 4px; + border-radius: 0; + + &:hover, + &:active { + text-decoration: underline; + } + + &:focus { + box-shadow: inset 0 0 0 2px get-color(sapphire-blue-10); + } + + &#{$self}--disabled { + color: get-color(black-coral-6); + } + } + + &--icon { + width: 36px; + height: 36px; + padding: 0; + justify-content: center; + color: get-color(black-coral-10); + font-size: 24px; + + &:hover, + &:active { + color: get-color(sapphire-blue-10); + } + + &:focus { + color: get-color(sapphire-blue-10); + box-shadow: inset 0 0 0 2px get-color(sapphire-blue-10); + } + } + + &--error { + background-color: get-color(jasper-10); + + &:hover, + &:active { + background-color: get-color(jasper-12); + } + + &:focus { + box-shadow: inset 0 0 0 2px get-color(jasper-13); + } + + &#{$self}--disabled { + background-color: get-color(black-coral-2); + } + } + + &--success { + background-color: get-color(sea-green-10); + + &:hover, + &:active { + background-color: get-color(sea-green-12); + } + + &:focus { + background-color: get-color(sea-green-10); + box-shadow: inset 0 0 0 2px get-color(sea-green-12); + } + + &#{$self}--disabled { + background-color: get-color(black-coral-2); + } + } + + &--s { + padding: 4.5px 24px; + } +} diff --git a/GUI/src/components/Button/index.tsx b/GUI/src/components/Button/index.tsx new file mode 100644 index 00000000..b35cd8c3 --- /dev/null +++ b/GUI/src/components/Button/index.tsx @@ -0,0 +1,56 @@ +import { ButtonHTMLAttributes, FC, PropsWithChildren, useRef } from 'react'; +import clsx from 'clsx'; + +import './Button.scss'; + +type ButtonProps = ButtonHTMLAttributes & { + appearance?: 'primary' | 'secondary' | 'text' | 'icon' | 'error' | 'success'; + size?: 'm' | 's'; + disabledWithoutStyle?: boolean; + showLoadingIcon?: boolean; +}; + +const Button: FC> = ({ + appearance = 'primary', + size = 'm', + disabled, + disabledWithoutStyle = false, + children, + showLoadingIcon = false, + ...rest +}) => { + const ref = useRef(null); + + const buttonClasses = clsx( + 'btn', + `btn--${appearance}`, + `btn--${size}`, + disabled && 'btn--disabled' + ); + + return ( + + ); +}; + +export default Button; \ No newline at end of file diff --git a/GUI/src/components/Card/Card.scss b/GUI/src/components/Card/Card.scss new file mode 100644 index 00000000..82d2665c --- /dev/null +++ b/GUI/src/components/Card/Card.scss @@ -0,0 +1,65 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.card { + $self: &; + background-color: get-color(white); + border: 1px solid get-color(black-coral-2); + border-radius: $veera-radius-s; + margin-bottom: 10px; + + &--borderless { + border: 0; + border-radius: 0; + + #{$self}__header { + border-radius: 0; + } + } + + &--fullWidth { + width: 100%; + } + + &__header, + &__body, + &__footer { + padding: get-spacing(haapsalu); + } + + &__header { + border-bottom: 1px solid get-color(black-coral-2); + background-color: #F9F9F9; + border-radius: $veera-radius-s $veera-radius-s 0 0; + + &.white { + background-color: white + } + } + + &__body { + &.divided { + display: flex; + flex-direction: column; + padding-left: 0px; + padding-right: 0px; + + > :not(:last-child) { + margin-bottom: get-spacing(haapsalu); + border-bottom: 1px solid get-color(black-coral-2); + padding-bottom: get-spacing(haapsalu); + padding-left: get-spacing(haapsalu); + } + + > :is(:last-child) { + padding-left: get-spacing(haapsalu); + } + } + } + + &__footer { + border-top: 1px solid get-color(black-coral-2); + } +} diff --git a/GUI/src/components/Card/index.tsx b/GUI/src/components/Card/index.tsx new file mode 100644 index 00000000..27eb7501 --- /dev/null +++ b/GUI/src/components/Card/index.tsx @@ -0,0 +1,39 @@ +import { FC, PropsWithChildren, ReactNode } from 'react'; +import clsx from 'clsx'; + +import './Card.scss'; + +type CardProps = { + header?: ReactNode; + footer?: ReactNode; + borderless?: boolean; + isHeaderLight?: boolean; + isBodyDivided?: boolean; + isFullWidth?: boolean; +}; + +const Card: FC> = ({ + header, + footer, + borderless, + isHeaderLight, + isBodyDivided, + children, + isFullWidth, +}) => { + return ( +
+ {header && ( +
+ {header} +
+ )} +
+ {children} +
+ {footer &&
{footer}
} +
+ ); +}; + +export default Card; diff --git a/GUI/src/components/Collapsible/Collapsible.scss b/GUI/src/components/Collapsible/Collapsible.scss new file mode 100644 index 00000000..24328e65 --- /dev/null +++ b/GUI/src/components/Collapsible/Collapsible.scss @@ -0,0 +1,35 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.collapsible { + border: 1px solid get-color(black-coral-2); + border-radius: 4px; + + &__trigger { + width: 100%; + display: flex; + align-items: center; + gap: 4px; + padding: get-spacing(haapsalu); + background-color: get-color(extra-light); + border-radius: 4px; + + &[aria-expanded=true] { + border-bottom: 1px solid get-color(black-coral-2); + border-radius: 4px 4px 0 0; + } + + .icon { + font-size: 21px; + } + } + + &__content { + padding: get-spacing(haapsalu); + background-color: get-color(white); + border-radius: 0 0 4px 4px; + overflow: hidden; + } +} diff --git a/GUI/src/components/Collapsible/index.tsx b/GUI/src/components/Collapsible/index.tsx new file mode 100644 index 00000000..02a13bda --- /dev/null +++ b/GUI/src/components/Collapsible/index.tsx @@ -0,0 +1,31 @@ +import { FC, PropsWithChildren, useState } from 'react'; +import * as RadixCollapsible from '@radix-ui/react-collapsible'; +import { MdOutlineAddBox, MdOutlineIndeterminateCheckBox } from 'react-icons/md'; + +import { Icon } from 'components'; +import './Collapsible.scss'; + +type CollapsibleProps = { + title: string; + defaultOpen?: boolean; +} + +const Collapsible: FC> = ({ defaultOpen = false, title, children }) => { + const [open, setOpen] = useState(defaultOpen); + + return ( + + + + + + {children} + + + ); +}; + +export default Collapsible; diff --git a/GUI/src/components/DataTable/CloseIcon.tsx b/GUI/src/components/DataTable/CloseIcon.tsx new file mode 100644 index 00000000..85de2dbb --- /dev/null +++ b/GUI/src/components/DataTable/CloseIcon.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import './DeboucedInput.scss'; + +const CloseIcon: React.FC = () => ( + + + + +); + +export default CloseIcon; diff --git a/GUI/src/components/DataTable/DataTable.scss b/GUI/src/components/DataTable/DataTable.scss new file mode 100644 index 00000000..c3c8e8cd --- /dev/null +++ b/GUI/src/components/DataTable/DataTable.scss @@ -0,0 +1,267 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/typography'; + +.data-table { + width: 100%; + color: get-color(black-coral-20); + text-align: left; + margin-bottom: 0; + display: table; + + &__scrollWrapper { + height: 100%; + min-height: 150px !important; + padding: 10px 20px; + overflow-x: auto; + white-space: nowrap; + display: block; + background-color: white; + border-radius: 10px; + border: solid 1px get-color(black-coral-1); + } + + &__page-size-selector { + display: flex; + gap: .7rem; + } + + thead, + tbody { + width: 100%; + } + + th { + padding: 12px 14.5px; + color: get-color(black-coral-12); + border-bottom: 1px solid get-color(black-coral-10); + font-weight: $veera-font-weight-beta; + vertical-align: middle; + position: relative; + } + + td { + padding: 12px 24px 12px 16px; + border-bottom: 1px solid get-color(black-coral-2); + vertical-align: middle; + max-width: fit-content; + + p { + white-space: break-spaces; + } + + .entity { + display: inline-flex; + align-items: center; + padding-left: 4px; + background-color: get-color(sapphire-blue-2); + border-radius: 4px; + + span { + display: inline-flex; + font-size: $veera-font-size-80; + background-color: get-color(white); + padding: 0 4px; + border-radius: 4px; + margin: 2px 2px 2px 4px; + } + } + } + + tbody { + tr { + &:last-child { + td { + border-bottom: 0; + } + } + } + } + + &__filter { + position: absolute; + top: 100%; + left: 0; + right: 0; + padding: get-spacing(paldiski); + background-color: get-color(white); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.14); + border-radius: 0 0 4px 4px; + border: 1px solid get-color(black-coral-2); + + input { + width: 100%; + display: block; + appearance: none; + background-color: get-color(white); + border: 1px solid get-color(black-coral-6); + border-radius: 5px; + color: var(--color-black); + font-size: $veera-font-size-100; + height: 32px; + line-height: 24px; + padding: get-spacing(paldiski); + + &::placeholder { + color: get-color(black-coral-6); + } + + &:focus { + outline: none; + border-color: get-color(sapphire-blue-10); + } + } + } + + &__dropdown_filter { + position: absolute; + top: 100%; + left: 0; + padding: get-spacing(paldiski); + background-color: get-color(white); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.14); + border-radius: 0 0 4px 4px; + border: 1px solid get-color(black-coral-2); + width: "fit-content"; + } + + &__pagination-wrapper { + margin-top: 10px; + display: flex; + padding: 6px 16px; + } + + &__pagination { + display: flex; + align-items: center; + gap: 15px; + margin: 0 auto; + + + .data-table__page-size { + margin-left: 0; + } + + .next, + .previous { + display: flex; + color: get-color(sapphire-blue-10); + + &[disabled] { + color: get-color(black-coral-11); + cursor: initial; + } + } + + .links { + display: flex; + align-items: center; + gap: 5px; + font-size: $veera-font-size-80; + color: get-color(black-coral-10); + + li { + display: block; + + a, + span { + display: flex; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + border-radius: 50%; + + &:hover { + text-decoration: none; + } + } + + &.active { + a, + span { + color: get-color(white); + background-color: get-color(sapphire-blue-10); + } + } + } + } + } + + &__page-size { + display: flex; + align-items: center; + gap: 8px; + font-size: $veera-font-size-80; + line-height: 16px; + color: get-color(black-coral-11); + margin-left: auto; + + select { + appearance: none; + font-size: $veera-font-size-70; + line-height: 16px; + height: 30px; + min-width: 50px; + padding: 6px 10px; + border: 1px solid #8f91a8; + border-radius: 2px; + background-color: get-color(white); + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNiIgdmlld0JveD0iMCAwIDEwIDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNS4zMTMwNiA1LjgwODIyQzUuMTU2ODUgNS45NjQ0MyA0LjkwMzU4IDUuOTY0NDMgNC43NDczNyA1LjgwODIyTDAuMjgyNzMgMS4zNDM1OEMwLjEyNjUyIDEuMTg3MzcgMC4xMjY1MiAwLjkzNDEwMiAwLjI4MjczIDAuNzc3ODkzTDAuNzc3NzA0IDAuMjgyOTE4QzAuOTMzOTE0IDAuMTI2NzA4IDEuMTg3MTggMC4xMjY3MDggMS4zNDMzOSAwLjI4MjkxN0w1LjAzMDIyIDMuOTY5NzRMOC43MTcwNCAwLjI4MjkxN0M4Ljg3MzI1IDAuMTI2NzA4IDkuMTI2NTIgMC4xMjY3MDggOS4yODI3MyAwLjI4MjkxN0w5Ljc3NzcgMC43Nzc4OTJDOS45MzM5MSAwLjkzNDEwMiA5LjkzMzkxIDEuMTg3MzcgOS43Nzc3IDEuMzQzNThMNS4zMTMwNiA1LjgwODIyWiIgZmlsbD0iIzU1NTg2NyIvPgo8L3N2Zz4K'); + background-repeat: no-repeat; + background-position: top 11px right 10px; + } + } +} + +.dataset-controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding: 0 1rem; + flex-wrap: wrap; + gap: 1rem; + + .filter-controls { + display: flex; + gap: 0.5rem; + align-items: center; + } + + .bulk-actions { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.5rem 1rem; + background-color: get-color(jasper-0); + border: 1px solid get-color(jasper-10); + border-radius: 6px; + + .selected-count { + font-size: 0.875rem; + color: get-color(jasper-10); + font-weight: 500; + } + } +} + +// Checkbox styling in table +.data-table { + input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: get-color(sapphire-blue-10); + } +} + +@media (max-width: 768px) { + .dataset-controls { + flex-direction: column; + align-items: stretch; + + .bulk-actions { + justify-content: center; + } + } +} \ No newline at end of file diff --git a/GUI/src/components/DataTable/DeboucedInput.scss b/GUI/src/components/DataTable/DeboucedInput.scss new file mode 100644 index 00000000..753f1ad0 --- /dev/null +++ b/GUI/src/components/DataTable/DeboucedInput.scss @@ -0,0 +1,11 @@ +.input-container { + position: relative; +} + +.search-icon { + position: absolute; + top: 50%; + right: 10px; + margin-left: 10px; + transform: translateY(-50%); +} diff --git a/GUI/src/components/DataTable/DebouncedInput.tsx b/GUI/src/components/DataTable/DebouncedInput.tsx new file mode 100644 index 00000000..1ad1f52f --- /dev/null +++ b/GUI/src/components/DataTable/DebouncedInput.tsx @@ -0,0 +1,54 @@ +import { FC, InputHTMLAttributes, useEffect, useState } from 'react'; +import './DeboucedInput.scss'; +import CloseIcon from './CloseIcon'; + +type DebouncedInputProps = Omit< + InputHTMLAttributes, + 'onChange' +> & { + value: string | number | string[]; + onChange: (value: string | number | string[]) => void; + debounce?: number; +}; + +const DebouncedInput: FC = ({ + value: initialValue, + onChange, + debounce = 500, + ...props +}) => { + const [value, setValue] = useState(initialValue); + + useEffect(() => { + setValue(initialValue); + }, [initialValue]); + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value); + }, debounce); + + return () => clearTimeout(timeout); + }, [value]); + + return ( +
+ setValue(e.target.value)} + /> + {value && ( + + )} +
+ ); +}; + +export default DebouncedInput; diff --git a/GUI/src/components/DataTable/DropdownFilter.tsx b/GUI/src/components/DataTable/DropdownFilter.tsx new file mode 100644 index 00000000..4009590b --- /dev/null +++ b/GUI/src/components/DataTable/DropdownFilter.tsx @@ -0,0 +1,55 @@ +import React, { FC, useState, MouseEvent } from 'react'; +import { Column, Table } from '@tanstack/react-table'; +import { useTranslation } from 'react-i18next'; +import { MdOutlineFilterList } from 'react-icons/md'; + +import { Icon } from 'components'; +import useDocumentEscapeListener from 'hooks/useDocumentEscapeListener'; + +type DropdownFilterProps = { + column: Column; + table: Table; + options: { label: string; value: string | number }[]; + onSelect: (value: string | number) => void; // <-- Add this prop +}; + +const DropdownFilter: FC = ({ column, table, options, onSelect }) => { + const { t } = useTranslation(); + const [filterOpen, setFilterOpen] = useState(false); + const [selectedValue, setSelectedValue] = useState(''); + + useDocumentEscapeListener(() => setFilterOpen(false)); + + const handleFilterToggle = (e: MouseEvent) => { + e.stopPropagation(); + setFilterOpen(!filterOpen); + }; + + const handleSelect = (e: React.ChangeEvent) => { + setSelectedValue(e.target.value); + setFilterOpen(false); + onSelect(e.target.value); // <-- Call the callback with the selected value + }; + + return ( + <> + + {filterOpen && ( +
+ +
+ )} + + ); +}; + +export default DropdownFilter; \ No newline at end of file diff --git a/GUI/src/components/DataTable/Filter.tsx b/GUI/src/components/DataTable/Filter.tsx new file mode 100644 index 00000000..038d8118 --- /dev/null +++ b/GUI/src/components/DataTable/Filter.tsx @@ -0,0 +1,65 @@ +import React, { FC, useState, MouseEvent } from 'react'; +import { Column, Table } from '@tanstack/react-table'; +import { useTranslation } from 'react-i18next'; +import { MdOutlineSearch } from 'react-icons/md'; + +import { Icon } from 'components'; +import useDocumentEscapeListener from 'hooks/useDocumentEscapeListener'; +import DebouncedInput from './DebouncedInput'; + +type FilterProps = { + column: Column; + table: Table; +}; + +const Filter: FC = ({ column, table }) => { + const { t } = useTranslation(); + const [filterOpen, setFilterOpen] = useState(false); + const firstValue = table + .getPreFilteredRowModel() + .flatRows[0]?.getValue(column.id); + + const columnFilterValue = column.getFilterValue(); + + useDocumentEscapeListener(() => setFilterOpen(false)); + + const handleFilterToggle = (e: MouseEvent) => { + e.stopPropagation(); + setFilterOpen(!filterOpen); + }; + + return ( + <> + + {filterOpen && ( +
+ {typeof firstValue === 'number' ? ( + + column.setFilterValue((old: [number, number]) => [ + value, + old?.[1], + ]) + } + /> + ) : ( + column.setFilterValue(value)} + placeholder={t('global.search') + '...'} + /> + )} +
+ )} + + ); +}; + +export default Filter; diff --git a/GUI/src/components/DataTable/index.tsx b/GUI/src/components/DataTable/index.tsx new file mode 100644 index 00000000..b01e0630 --- /dev/null +++ b/GUI/src/components/DataTable/index.tsx @@ -0,0 +1,318 @@ +import React, { CSSProperties, FC, ReactNode, useId } from 'react'; +import { + ColumnDef, + useReactTable, + getCoreRowModel, + flexRender, + getSortedRowModel, + SortingState, + FilterFn, + getFilteredRowModel, + VisibilityState, + getPaginationRowModel, + PaginationState, + TableMeta, + Row, + RowData, ColumnFiltersState, RowSelectionState, + +} from '@tanstack/react-table'; +import { + RankingInfo, + rankItem, +} from '@tanstack/match-sorter-utils'; +import { + MdUnfoldMore, + MdExpandMore, + MdExpandLess, + MdOutlineEast, + MdOutlineWest, +} from 'react-icons/md'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; +import { Icon, Track } from 'components'; +import Filter from './Filter'; +import './DataTable.scss'; +import DropdownFilter from './DropdownFilter'; + +type DataTableProps = { + data: any; + columns: ColumnDef[]; + tableBodyPrefix?: ReactNode; + isClientSide?: boolean; + sortable?: boolean; + filterable?: boolean; + pagination?: PaginationState; + sorting?: SortingState; + setPagination?: (state: PaginationState) => void; + setSorting?: (state: SortingState) => void; + globalFilter?: string; + setGlobalFilter?: React.Dispatch>; + columnVisibility?: VisibilityState; + setColumnVisibility?: React.Dispatch>; + disableHead?: boolean; + pagesCount?: number; + meta?: TableMeta; + dropdownFilters?: DropdownFilterConfig[]; + onSelect?: (value: string | number) => void | undefined + showPageSizeSelector?: boolean; + pageSizeOptions?: number[]; + rowSelection?: RowSelectionState; + setRowSelection?: (state: RowSelectionState) => void; +}; + +type ColumnMeta = { + meta: { + size: number | string; + } +} + +type CustomColumnDef = ColumnDef & ColumnMeta; + +type DropdownFilterConfig = { + columnId: string; + options: { label: string; value: string | number }[]; +}; + +declare module '@tanstack/table-core' { + interface FilterFns { + fuzzy: FilterFn; + } + + interface FilterMeta { + itemRank: RankingInfo; + } +} + +declare module '@tanstack/react-table' { + interface TableMeta { + getRowStyles: (row: Row) => CSSProperties; + } + class Column { + columnDef: CustomColumnDef; + } +} + +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta({ + itemRank, + }); + return itemRank.passed; +}; + +const DataTable: FC = ( + { + data, + columns, + isClientSide = true, + tableBodyPrefix, + sortable, + filterable, + pagination, + sorting, + setPagination, + setSorting, + globalFilter, + setGlobalFilter, + columnVisibility, + setColumnVisibility, + disableHead, + pagesCount, + meta, + dropdownFilters, + onSelect, + showPageSizeSelector = false, + pageSizeOptions = [10, 20, 50, 100], + rowSelection, + setRowSelection, + }, +) => { + const id = useId(); + const { t } = useTranslation(); + const [columnFilters, setColumnFilters] = React.useState([]); +const table = useReactTable({ + data, + columns, + filterFns: { + fuzzy: fuzzyFilter, + }, + state: { + sorting, + columnFilters, + globalFilter, + columnVisibility, + ...{ pagination }, + ...(rowSelection && { rowSelection }), + }, + meta, + onColumnFiltersChange: setColumnFilters, + onGlobalFilterChange: setGlobalFilter, + onColumnVisibilityChange: setColumnVisibility, + globalFilterFn: fuzzyFilter, + enableRowSelection: !!setRowSelection, + onRowSelectionChange: setRowSelection + ? (updaterOrValue) => { + if (typeof updaterOrValue === 'function') { + setRowSelection(updaterOrValue(table.getState().rowSelection)); + } else { + setRowSelection(updaterOrValue); + } + } + : undefined, + onSortingChange: (updater) => { + if (typeof updater !== 'function') return; + setSorting?.(updater(table.getState().sorting)); + }, + onPaginationChange: (updater) => { + if (typeof updater !== 'function') return; + setPagination?.(updater(table.getState().pagination)); + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + ...(pagination && { getPaginationRowModel: getPaginationRowModel() }), + ...(sortable && { getSortedRowModel: getSortedRowModel() }), + manualPagination: isClientSide ? undefined : true, + manualSorting: isClientSide ? undefined : true, + pageCount: isClientSide ? undefined : pagesCount, + }); + + const handlePageSizeChange = (newPageSize: number) => { + if (setPagination && pagination) { + setPagination({ + pageIndex: 0, + pageSize: newPageSize, + }); + } + }; + + return ( +
+ + {!disableHead && ( + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + )} + + {tableBodyPrefix} + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( + + {sortable && header.column.getCanSort() && ( + + )} + {flexRender(header.column.columnDef.header, header.getContext())} + {dropdownFilters && header.column.getCanFilter() && ( + (() => { + const dropdownConfig = dropdownFilters?.find( + (df) => df.columnId === header.column.id + ); + + if (dropdownConfig) { + return ( + { })} + /> + ); + } + + })() + )} + {filterable && header.column.getCanFilter() && ( + )} + + )} +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
+ {pagination && ( +
+ {showPageSizeSelector && ( +
+ + {t('global.showEntries') || 'Show'} + + + + {t('global.entries') || 'entries'} + +
+ )} + {(table.getPageCount() * table.getState().pagination.pageSize) > table.getState().pagination.pageSize && ( +
+ + + +
+ )} +
+ )} +
+ ); +}; + +export default DataTable; \ No newline at end of file diff --git a/GUI/src/components/Dialog/Dialog.scss b/GUI/src/components/Dialog/Dialog.scss new file mode 100644 index 00000000..bc67c6e0 --- /dev/null +++ b/GUI/src/components/Dialog/Dialog.scss @@ -0,0 +1,63 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.dialog { + background-color: get-color(white); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.25); + border-radius: 4px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + max-width: 600px; + z-index: 1011111; + max-height: 90vh; + + &--large { + max-width: 800px; + } + + &__overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.54); + z-index: 100; + } + + &__header, + &__body, + &__footer { + padding: get-spacing(haapsalu); + } + + &__header { + display: flex; + align-items: center; + gap: get-spacing(haapsalu); + background-color: get-color(black-coral-0); + border-bottom: 1px solid get-color(black-coral-2); + } + + &__title { + flex: 1; + } + + &__close { + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + } + + &__body { + overflow: auto; + max-height: calc(90vh - 70px); + } + + &__footer { + border-top: 1px solid get-color(black-coral-2); + } +} diff --git a/GUI/src/components/Dialog/index.tsx b/GUI/src/components/Dialog/index.tsx new file mode 100644 index 00000000..7b2848c1 --- /dev/null +++ b/GUI/src/components/Dialog/index.tsx @@ -0,0 +1,45 @@ +import { FC, PropsWithChildren, ReactNode } from 'react'; +import * as RadixDialog from '@radix-ui/react-dialog'; +import { MdOutlineClose } from 'react-icons/md'; +import clsx from 'clsx'; +import './Dialog.scss'; +import Icon from 'components/Icon'; +import Track from 'components/Track'; + +type DialogProps = { + title?: string | null; + footer?: ReactNode; + onClose: () => void; + size?: 'default' | 'large'; + isOpen?: boolean; +} + +const Dialog: FC> = ({ title, footer, onClose, size = 'default', children,isOpen }) => { + return ( + + + + + { + title &&
+ {title} + + + +
+ } +
+ {children} +
+ {footer && ( + {footer} + )} +
+
+
+ ); +}; + +export default Dialog; diff --git a/GUI/src/components/Drawer/Drawer.scss b/GUI/src/components/Drawer/Drawer.scss new file mode 100644 index 00000000..df7bc711 --- /dev/null +++ b/GUI/src/components/Drawer/Drawer.scss @@ -0,0 +1,40 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.drawer { + position: fixed; + display: flex; + flex-direction: column; + top: 100px; + right: 0; + bottom: 0; + background-color: get-color(white); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.14); + width: 50%; + transition: transform .25s ease-out; + overflow: hidden; + z-index: 98; + + &__header { + display: flex; + align-items: center; + gap: get-spacing(haapsalu); + padding: get-spacing(haapsalu); + border-bottom: 1px solid get-color(black-coral-2); + + .icon { + font-size: 20px; + } + } + + &__title, + &__body { + flex: 1; + } + + &__body { + overflow: auto; + } +} diff --git a/GUI/src/components/Drawer/index.tsx b/GUI/src/components/Drawer/index.tsx new file mode 100644 index 00000000..9b6f771f --- /dev/null +++ b/GUI/src/components/Drawer/index.tsx @@ -0,0 +1,42 @@ +import { CSSProperties, FC, PropsWithChildren, useEffect, useRef } from 'react'; +import { MdOutlineClose } from 'react-icons/md'; +import autoAnimate from '@formkit/auto-animate'; + +import { Icon } from 'components'; +import './Drawer.scss'; + +type DrawerProps = { + title: string; + onClose: () => void; + style?: CSSProperties; +} + +const Drawer: FC> = ({ title, onClose, children, style }) => { + const ref = useRef(null); + + useEffect(() => { + ref.current && autoAnimate(ref.current); + const handleKeyup = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + document.addEventListener('keyup', handleKeyup); + + return () => document.removeEventListener('keyup', handleKeyup); + }, [onClose]); + + return ( +
+
+

{title}

+ +
+
+ {children} +
+
+ ); +}; + +export default Drawer; diff --git a/GUI/src/components/FileUpload/index.tsx b/GUI/src/components/FileUpload/index.tsx new file mode 100644 index 00000000..5750fba6 --- /dev/null +++ b/GUI/src/components/FileUpload/index.tsx @@ -0,0 +1,98 @@ +import { FormInput } from 'components/FormElements'; +import React, { + ChangeEvent, + forwardRef, + useImperativeHandle, + Ref, + useRef, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; + +type FileUploadProps = { + onFileSelect: (file: File | undefined) => void; + accept?: string | string[]; + disabled?: boolean; +}; + +export type FileUploadHandle = { + clearFile: () => void; +}; + +const FileUpload = forwardRef( + (props: FileUploadProps, ref: Ref) => { + const { onFileSelect, accept, disabled } = props; + const fileInputRef = useRef(null); + const [errorMessage, setErrorMessage] = useState(''); + const { t } = useTranslation(); + useImperativeHandle(ref, () => ({ + clearFile() { + onFileSelect(undefined); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }, + })); + + const handleFileChange = (e: ChangeEvent) => { + const file = e.target.files ? e.target.files[0] : undefined; + const maxFileSize = 20 * 1024 * 1024; // 20 MB in bytes + + if (file) { + if (file.size > maxFileSize) { + setErrorMessage(t('global.maxFileSize') ?? ''); + onFileSelect(undefined); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } else { + setErrorMessage(''); + onFileSelect(file); + } + } else { + setErrorMessage(''); + onFileSelect(undefined); + } + }; + + const restrictFormat = (accept: string | string[]) => { + if (typeof accept === 'string') { + if (accept === 'json') return '.json'; + else if (accept === 'xlsx') return '.xlsx'; + else if (accept === 'yaml') return '.yaml, .yml'; + return ''; + } else { + return accept.map((ext) => `.${ext}`).join(', '); + } + }; + + return ( +
+ + + + {errorMessage &&

{errorMessage}

} + +
+ ); + } +); + +export default FileUpload; diff --git a/GUI/src/components/FormElements/DynamicForm/index.tsx b/GUI/src/components/FormElements/DynamicForm/index.tsx new file mode 100644 index 00000000..4e307392 --- /dev/null +++ b/GUI/src/components/FormElements/DynamicForm/index.tsx @@ -0,0 +1,110 @@ +import React, { useEffect, useState } from 'react'; +import { useForm, Controller } from 'react-hook-form'; +import FormInput from '../FormInput'; +import FormSelect from '../FormSelect'; +import Button from 'components/Button'; +import Track from 'components/Track'; +import { useTranslation } from 'react-i18next'; +import { SelectedRowPayload } from 'types/datasets'; + +type ClientOption = { label: string; value: string; agencyId: number | string }; + +type DynamicFormProps = { + formData: {itemId:string |number, dataItem: string; agencyName: string; agencyId?: number | string }; + clientOptions: ClientOption[]; + onSubmit: (data: SelectedRowPayload) => void; + setPatchUpdateModalOpen: React.Dispatch>; +}; + +const DynamicForm: React.FC = ({ + formData, + clientOptions, + onSubmit, + setPatchUpdateModalOpen, +}) => { + const { control, handleSubmit, watch, getValues } = useForm({ + defaultValues: formData, + }); + const [isChanged, setIsChanged] = useState(false); + const { t } = useTranslation(); + + const allValues = watch(); +const [selectedClientId, setSelectedClientId] = useState(formData.agencyId ?? ''); + + useEffect(() => { + const currentValues = getValues(); + setIsChanged( + currentValues.dataItem !== formData.dataItem || + currentValues.agencyId !== formData.agencyId + ); + }, [allValues, formData, getValues]); + + const handleFormSubmit = (data: any) => { + const selectedClient = clientOptions.find(opt => opt.value === data.agencyId); + onSubmit({ + itemId: formData.itemId, + dataItem: data.dataItem, + agencyId: selectedClient?.value ?? "0", + agencyName: selectedClient?.label ?? data.agencyName, + }); +}; + + return ( +
+
+ + ( + + )} + /> +
+
+ + ( + ({ + label: opt.label, + value: opt.value, + }))} + {...field} + onSelectionChange={(selected) => { + const value = typeof selected?.value === 'object' + ? (selected?.value.id ?? '') + : (selected?.value ?? ''); + setSelectedClientId(value); + field.onChange(value); + }} + defaultValue={selectedClientId} + /> + )} + /> +
+ +
+ + +
+ +
+ ); +}; + +export default DynamicForm; \ No newline at end of file diff --git a/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss b/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss new file mode 100644 index 00000000..8bdf863d --- /dev/null +++ b/GUI/src/components/FormElements/FormCheckbox/FormCheckbox.scss @@ -0,0 +1,57 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.checkbox { + width: 100%; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + + &__label { + display: block; + flex: 0 0 85px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__item { + input[type=checkbox] { + display: none; + + + label { + display: block; + padding-left: 32px; + position: relative; + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + + &::before { + content: ''; + display: block; + width: 16px; + height: 16px; + box-shadow: inset 0 0 0 1px get-color(black-coral-2); + border-radius: 2px; + position: absolute; + left: 4px; + top: 4px; + } + } + + &:checked { + + label { + &::before { + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTEiIHZpZXdCb3g9IjAgMCAxNCAxMSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuNzQ5NzkgOC4xMjkwNkwxLjYyMjI5IDUuMDAxNTZMMC41NjEwMzUgNi4wNjI4MUw0Ljc0OTc5IDEwLjI1MTZMMTMuNzQ5OCAxLjI1MTU2TDEyLjY4ODUgMC4xOTAzMDhMNC43NDk3OSA4LjEyOTA2WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg=='); + background-color: get-color(sapphire-blue-10); + background-repeat: no-repeat; + background-position: center; + background-size: 13px 10px; + box-shadow: inset 0 0 0 1px get-color(sapphire-blue-10); + } + } + } + } + } +} diff --git a/GUI/src/components/FormElements/FormCheckbox/index.tsx b/GUI/src/components/FormElements/FormCheckbox/index.tsx new file mode 100644 index 00000000..66645255 --- /dev/null +++ b/GUI/src/components/FormElements/FormCheckbox/index.tsx @@ -0,0 +1,39 @@ +import { forwardRef, InputHTMLAttributes, useId } from 'react'; + +import './FormCheckbox.scss'; + +type FormCheckboxType = InputHTMLAttributes & { + label: string; + name: string; + hideLabel?: boolean; + item: { + label: string; + value: string; + checked?: boolean; + }; +} + +const FormCheckbox = forwardRef(( + { + label, + name, + hideLabel, + item, + ...rest + }, + ref, +) => { + const uid = useId(); + + return ( +
+ {label && !hideLabel && } +
+ + +
+
+ ); +}); + +export default FormCheckbox; diff --git a/GUI/src/components/FormElements/FormCheckboxes/FormCheckboxes.scss b/GUI/src/components/FormElements/FormCheckboxes/FormCheckboxes.scss new file mode 100644 index 00000000..8312649c --- /dev/null +++ b/GUI/src/components/FormElements/FormCheckboxes/FormCheckboxes.scss @@ -0,0 +1,68 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.checkboxes { + display: flex; + align-items: flex-start; + gap: get-spacing(paldiski); + + &__label { + display: block; + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper { + display: flex; + flex-direction: column; + gap: 8px; + } + + &__row { + display: flex; + gap: 20px; + } + + &__item { + input[type=checkbox] { + display: none; + + + label { + display: block; + padding-left: 32px; + position: relative; + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + text-transform: capitalize; + + &::before { + content: ''; + display: block; + width: 16px; + height: 16px; + box-shadow: inset 0 0 0 1px get-color(black-coral-2); + border-radius: 2px; + position: absolute; + left: 4px; + top: 4px; + } + } + + &:checked { + + label { + &::before { + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTEiIHZpZXdCb3g9IjAgMCAxNCAxMSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuNzQ5NzkgOC4xMjkwNkwxLjYyMjI5IDUuMDAxNTZMMC41NjEwMzUgNi4wNjI4MUw0Ljc0OTc5IDEwLjI1MTZMMTMuNzQ5OCAxLjI1MTU2TDEyLjY4ODUgMC4xOTAzMDhMNC43NDk3OSA4LjEyOTA2WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg=='); + background-color: get-color(sapphire-blue-10); + background-repeat: no-repeat; + background-position: center; + background-size: 13px 10px; + box-shadow: inset 0 0 0 1px get-color(sapphire-blue-10); + } + } + } + } + } +} diff --git a/GUI/src/components/FormElements/FormCheckboxes/index.tsx b/GUI/src/components/FormElements/FormCheckboxes/index.tsx new file mode 100644 index 00000000..47d8e235 --- /dev/null +++ b/GUI/src/components/FormElements/FormCheckboxes/index.tsx @@ -0,0 +1,77 @@ +import { ChangeEvent, FC, useId, useState, useEffect } from 'react'; + +import './FormCheckboxes.scss'; + +type FormCheckboxesType = { + label: string; + name: string; + hideLabel?: boolean; + onValuesChange?: (values: Record) => void; + items: { + label: string; + value: string; + }[] |undefined; + isStack?: boolean; + error?: string; + selectedValues?: string[]; +}; + +const FormCheckboxes: FC = ({ + label, + name, + hideLabel, + onValuesChange, + items, + isStack = true, + error, + selectedValues = [], +}) => { + const id = useId(); + const [internalSelectedValues, setInternalSelectedValues] = useState(selectedValues); + + useEffect(() => { + setInternalSelectedValues(selectedValues); + }, [selectedValues]); + + const handleValuesChange = (e: ChangeEvent) => { + const { checked, value } = e.target; + + const newValues = checked + ? [...internalSelectedValues, value] + : internalSelectedValues.filter((v: string) => v !== value); + + setInternalSelectedValues(newValues); + + if (onValuesChange) onValuesChange({ [name]: newValues }); + }; + + return ( +
+
+
+ {label && !hideLabel && ( + + )} +
+ {items?.map((item, index) => ( +
+ + +
+ ))} +
+
+
+
{error &&

{error}

}
+
+ ); +}; + +export default FormCheckboxes; diff --git a/GUI/src/components/FormElements/FormDatepicker/FormDatepicker.scss b/GUI/src/components/FormElements/FormDatepicker/FormDatepicker.scss new file mode 100644 index 00000000..55ac0785 --- /dev/null +++ b/GUI/src/components/FormElements/FormDatepicker/FormDatepicker.scss @@ -0,0 +1,154 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.datepicker { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper_column { + display: flex; + flex-direction: column; + gap: 7px; + position: relative; + width: 125px; + + .icon { + position: absolute; + right: 8px; + top: 8px; + pointer-events: none; + } + } + + &__wrapper_row { + display: flex; + flex-direction: row; + gap: 7px; + position: relative; + width: 125px; + + .icon { + position: absolute; + right: 8px; + top: 8px; + pointer-events: none; + } + } + + &__error { + width: 100%; + margin-right: 6px; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + color: get-color(black-coral-20); + border-radius: $veera-radius-s; + background-color: get-color(jasper-3); + font-size: 13px; + line-height: 20px; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); + + &::before { + content: ''; + display: block; + background-color: get-color(jasper-3); + border-left: 16px solid transparent; + border-right: 16px solid transparent; + border-bottom: 25px; + } + } + + input { + width: 100%; + display: block; + appearance: none; + background-color: get-color(white); + border: 1px solid get-color(black-coral-6); + border-radius: $veera-radius-s; + color: var(--color-black); + font-size: $veera-font-size-100; + height: 40px; + line-height: 24px; + padding: get-spacing(paldiski); + + &::placeholder { + color: get-color(black-coral-6); + } + + &:focus { + outline: none; + border-color: get-color(sapphire-blue-10); + } + } + + &--error { + input { + border-color: get-color(jasper-10); + } + } + + &--disabled & { + input { + background-color: get-color(black-coral-0); + } + } +} + +.react-datepicker { + font-family: inherit; + font-size: 14px; + border: 1px solid get-color(black-coral-6); + border-radius: 4px; + + &-popper[data-placement^=bottom] { + padding: 0; + } + + &-wrapper { + display: block; + } + + &__input-container { + display: block; + } + + &__triangle { + &::before, + &::after { + content: none !important; + } + } + + &__navigation { + width: 50px; + height: 50px; + top: 0; + + &:hover { + background-color: var(--color-bg); + } + + &--previous { + border-top-left-radius: 4px; + border-right: 1px solid var(--color-gray); + left: 0; + } + + &--next { + border-top-right-radius: 4px; + border-left: 1px solid var(--color-gray); + right: 0; + } + } +} diff --git a/GUI/src/components/FormElements/FormDatepicker/index.tsx b/GUI/src/components/FormElements/FormDatepicker/index.tsx new file mode 100644 index 00000000..1de8e635 --- /dev/null +++ b/GUI/src/components/FormElements/FormDatepicker/index.tsx @@ -0,0 +1,98 @@ +import { forwardRef, useId } from 'react'; +import ReactDatePicker, { registerLocale } from 'react-datepicker'; +import clsx from 'clsx'; +import { et } from 'date-fns/locale'; +import { ControllerRenderProps } from 'react-hook-form'; +import { + MdChevronRight, + MdChevronLeft, + MdOutlineToday, + MdOutlineSchedule, +} from 'react-icons/md'; + +import { Icon } from 'components'; +import 'react-datepicker/dist/react-datepicker.css'; +import './FormDatepicker.scss'; + +registerLocale('et-EE', et); + +type FormDatepickerProps = ControllerRenderProps & { + label: string; + name: string; + hideLabel?: boolean; + disabled?: boolean; + placeholder?: string; + timePicker?: boolean; + direction?: 'row' | 'column'; +}; + +const FormDatepicker = forwardRef( + ( + { + label, + name, + hideLabel, + disabled, + placeholder, + timePicker, + direction = 'column', + ...rest + }, + ref + ) => { + const id = useId(); + const { value, onChange } = rest; + + const datepickerClasses = clsx( + 'datepicker', + disabled && 'datepicker--disabled' + ); + + return ( +
+ {label && !hideLabel && ( + + )} +
+ } + nextMonthButtonLabel={} + aria-label={hideLabel ? label : undefined} + showTimeSelect={timePicker} + showTimeSelectOnly={timePicker} + timeIntervals={15} + timeFormat="HH:mm:ss" + timeInputLabel="" + portalId="overlay-root" + {...rest} + onChange={onChange} + /> + + ) : ( + + ) + } + size="medium" + /> +
+
+ ); + } +); + +export default FormDatepicker; diff --git a/GUI/src/components/FormElements/FormInput/FormInput.scss b/GUI/src/components/FormElements/FormInput/FormInput.scss new file mode 100644 index 00000000..c010c478 --- /dev/null +++ b/GUI/src/components/FormElements/FormInput/FormInput.scss @@ -0,0 +1,97 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.input { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper { + flex: 1; + display: flex; + flex-direction: column; + gap: 0px; + position: relative; + + .icon { + position: absolute; + top: 10px; + right: 10px; + } + } + + &__inline_error { + color: get-color(jasper-10); + font-size: 12px; + + } + + &__error { + width: 100%; + margin-right: 6px; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + color: get-color(black-coral-20); + border-radius: $veera-radius-s; + background-color: get-color(jasper-3); + font-size: 13px; + line-height: 20px; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); + + &::before { + content: ''; + display: block; + background-color: get-color(jasper-3); + border-left: 16px solid transparent; + border-right: 16px solid transparent; + border-bottom: 25px; + } + } + + input { + width: 100%; + display: block; + appearance: none; + background-color: get-color(white); + border: 1px solid get-color(black-coral-6); + border-radius: $veera-radius-s; + color: var(--color-black); + font-size: $veera-font-size-100; + height: 40px; + line-height: 24px; + padding: get-spacing(paldiski); + + &::placeholder { + color: get-color(black-coral-6); + } + + &:focus { + outline: none; + border-color: get-color(sapphire-blue-10); + } + } + + &--error { + input { + border-color: get-color(jasper-10); + } + } + + &--disabled & { + input { + background-color: get-color(black-coral-0); + border: solid 1px get-color(jasper-10); + } + } +} diff --git a/GUI/src/components/FormElements/FormInput/index.tsx b/GUI/src/components/FormElements/FormInput/index.tsx new file mode 100644 index 00000000..dd8df673 --- /dev/null +++ b/GUI/src/components/FormElements/FormInput/index.tsx @@ -0,0 +1,50 @@ +import { forwardRef, InputHTMLAttributes, PropsWithChildren, useId } from 'react'; +import clsx from 'clsx'; +import './FormInput.scss'; +import { DefaultTFuncReturn } from 'i18next'; + +type InputProps = PropsWithChildren> & { + label: string; + name: string; + hideLabel?: boolean; + maxLength?: number; + error?: string; + placeholder?:string | DefaultTFuncReturn; +}; + +const FormInput = forwardRef( + ( + { label, name, disabled, hideLabel, maxLength, error, children,placeholder, ...rest }, + ref + ) => { + const id = useId(); + + const inputClasses = clsx('input', disabled && 'input--disabled', error && 'input--error'); + + return ( +
+ {label && !hideLabel && ( + + )} +
+ + {error &&

{error}

} + {children} +
+
+ ); + } +); + +export default FormInput; diff --git a/GUI/src/components/FormElements/FormRadios/FormRadios.scss b/GUI/src/components/FormElements/FormRadios/FormRadios.scss new file mode 100644 index 00000000..d0db7fbd --- /dev/null +++ b/GUI/src/components/FormElements/FormRadios/FormRadios.scss @@ -0,0 +1,76 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.radios { + width: 100%; + display: flex; + align-items: flex-start; + gap: get-spacing(paldiski); + + &__label { + display: block; + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper { + display: flex; + gap: 8px; + } + + &__stack { + gap: 8px; + } + + &__item { + input[type=radio] { + display: none; + + + label { + display: block; + padding-left: 32px; + position: relative; + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + text-transform: capitalize; + + &::before { + content: ''; + display: block; + width: 16px; + height: 16px; + box-shadow: inset 0 0 0 1px get-color(black-coral-2); + border-radius: 50%; + position: absolute; + left: 4px; + top: 4px; + } + } + + &:checked { + + label { + &::before { + width: 20px; + height: 20px; + box-shadow: inset 0 0 0 1px #8F91A8; + } + + &::after { + content: ''; + display: block; + width: 10px; + height: 10px; + border-radius: 50%; + background-color: get-color(sapphire-blue-10); + position: absolute; + top: 9px; + left: 9px; + } + } + } + } + } +} diff --git a/GUI/src/components/FormElements/FormRadios/index.tsx b/GUI/src/components/FormElements/FormRadios/index.tsx new file mode 100644 index 00000000..9c276d4a --- /dev/null +++ b/GUI/src/components/FormElements/FormRadios/index.tsx @@ -0,0 +1,65 @@ +import { FC, useId } from 'react'; +import './FormRadios.scss'; + +type FormRadiosType = { + label: string; + name: string; + hideLabel?: boolean; + items: { + label: string; + value: string; + }[] |undefined; + onChange: (selectedValue: string) => void; + selectedValue?: string; + isStack?: boolean; + error?: string; +}; + +const FormRadios: FC = ({ + label, + name, + hideLabel, + items, + onChange, + selectedValue, + isStack = false, + error, +}) => { + const id = useId(); + + return ( +
+
+
+ {label && !hideLabel && ( + + )} +
+ {items?.map((item, index) => ( +
+ { + onChange(event.target.value); + }} + /> + +
+ ))} +
+
+
+
{error &&

{error}

}
+
+ ); +}; + +export default FormRadios; + + + + diff --git a/GUI/src/components/FormElements/FormSelect/FormMultiselect.tsx b/GUI/src/components/FormElements/FormSelect/FormMultiselect.tsx new file mode 100644 index 00000000..ef9480ae --- /dev/null +++ b/GUI/src/components/FormElements/FormSelect/FormMultiselect.tsx @@ -0,0 +1,124 @@ +import { FC, ReactNode, SelectHTMLAttributes, useId, useState } from 'react'; +import { useSelect } from 'downshift'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; +import { MdArrowDropDown } from 'react-icons/md'; + +import { Icon } from 'components'; +import './FormSelect.scss'; + +type SelectOption = { label: string, value: string }; + +type FormMultiselectProps = SelectHTMLAttributes & { + label: ReactNode; + name: string; + placeholder?: string; + hideLabel?: boolean; + options: SelectOption[]; + selectedOptions?: SelectOption[]; + onSelectionChange?: (selection: SelectOption[] | null) => void; +}; + +const FormMultiselect: FC = ( + { + label, + hideLabel, + options, + disabled, + placeholder, + defaultValue, + selectedOptions, + onSelectionChange, + ...rest + }, +) => { + const id = useId(); + const { t } = useTranslation(); + const [selectedItems, setSelectedItems] = useState(selectedOptions ?? []); + const { + isOpen, + getToggleButtonProps, + getLabelProps, + getMenuProps, + highlightedIndex, + getItemProps, + } = useSelect({ + items: options, + stateReducer: (state, actionAndChanges) => { + const { changes, type } = actionAndChanges; + if (type === useSelect.stateChangeTypes.ItemClick) { + return { + ...changes, + isOpen: true, + highlightedIndex: state.highlightedIndex, + }; + } else { + return changes; + } + }, + selectedItem: null, + onSelectedItemChange: ({ selectedItem }) => { + if (!selectedItem) { + return; + } + const index = selectedItems.findIndex((item) => item.value === selectedItem.value); + const items = []; + if (index > 0) { + items.push( + ...selectedItems.slice(0, index), + ...selectedItems.slice(index + 1) + ); + } else if (index === 0) { + items.push(...selectedItems.slice(1)); + } else { + items.push(...selectedItems, selectedItem); + } + setSelectedItems(items); + if (onSelectionChange) onSelectionChange(items); + }, + }); + + const selectClasses = clsx( + 'select', + disabled && 'select--disabled', + ); + + const placeholderValue = placeholder || t('global.choose'); + + return ( +
+ {label && !hideLabel && } +
+
+ {selectedItems?.length > 0 ? `${t('global.chosen')} (${selectedItems?.length})` : placeholderValue} + } /> +
+ +
    + {isOpen && + options.map((item, index) => ( +
  • + s.value).includes(item.value)} + value={item.value} + onChange={() => null} + /> + {item.label} +
  • + ))} +
+
+
+ ); +}; + + +export default FormMultiselect; diff --git a/GUI/src/components/FormElements/FormSelect/FormSelect.scss b/GUI/src/components/FormElements/FormSelect/FormSelect.scss new file mode 100644 index 00000000..b6b4f434 --- /dev/null +++ b/GUI/src/components/FormElements/FormSelect/FormSelect.scss @@ -0,0 +1,128 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.select { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper { + width: 100%; + position: relative; + } + + &__error { + border: 1px solid get-color(jasper-10); + + } + + &__default { + border: 1px solid get-color(black-coral-6); + + } + + &__trigger { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + appearance: none; + background-color: get-color(white); + border-radius: $veera-radius-s; + color: get-color(black); + font-size: $veera-font-size-100; + height: 40px; + line-height: 24px; + padding: get-spacing(paldiski); + + .icon { + font-size: $veera-font-size-250; + } + + &[aria-expanded=true] { + border-color: get-color(sapphire-blue-10); + border-radius: 3px; + + + #{$self}__menu { + display: block; + } + + +#{$self}__menu_up { + display: block; + } + + .icon { + transform: rotate(180deg); + } + } + } + + &__menu { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: get-color(white); + border-radius: 4px; + border: 1px solid get-color(black-coral-2); + border-top: 1; + z-index: 9998; + max-height: 320px; + overflow: auto; + margin-top: 3px; + } + + &__menu_up { + display: none; + position: absolute; + top: auto; + left: 0; + right: 0; + bottom: 100%; + background-color: get-color(white); + border-radius: 4px; + border: 1px solid get-color(black-coral-2); + border-top: 1; + z-index: 9998; + max-height: 320px; + overflow: auto; + margin-bottom: 3px; + } + + &__option { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 15px; + + span { + display: block; + } + + &[aria-selected=true] { + background-color: #DDEBFF; + + &:hover, + &:focus { + background-color: get-color(sapphire-blue-10); + } + } + + &:hover, + &:focus { + background-color: get-color(black-coral-0); + } + } +} diff --git a/GUI/src/components/FormElements/FormSelect/index.tsx b/GUI/src/components/FormElements/FormSelect/index.tsx new file mode 100644 index 00000000..e1187a49 --- /dev/null +++ b/GUI/src/components/FormElements/FormSelect/index.tsx @@ -0,0 +1,148 @@ +import { + forwardRef, + ReactNode, + SelectHTMLAttributes, + useId, + useState, + useEffect, +} from 'react'; +import { useSelect } from 'downshift'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; +import { MdArrowDropDown } from 'react-icons/md'; + +import { Icon } from 'components'; +import './FormSelect.scss'; +import { ControllerRenderProps } from 'react-hook-form'; + +type FormSelectOption = { + label: string; + value: string | { name: string; id: string }; +}; + +type FormSelectProps = Partial & + SelectHTMLAttributes & { + label: ReactNode; + name: string; + placeholder?: string; + hideLabel?: boolean; + direction?: 'down' | 'up'; + options: FormSelectOption[]; + onSelectionChange?: (selection: FormSelectOption | null) => void; + error?: string; + defaultValue?: string | { name: string; id: string } | number; + }; + +const itemToString = (item: FormSelectOption | null) => { + return item ? item.value.toString() : ''; +}; + +const FormSelect = forwardRef( + ( + { + label, + hideLabel, + direction = 'down', + options, + disabled, + placeholder, + defaultValue, + onSelectionChange, + error, + ...rest + }, + ref + ) => { + const id = useId(); + const { t } = useTranslation(); + + const [selectedItem, setSelectedItem] = useState( + options?.find((o) => o.value === defaultValue) || + options?.find( + (o) => typeof o.value === 'object' && o.value?.name === defaultValue + ) || + null + ); + + useEffect(() => { + const newSelectedItem = + options?.find((o) => o.value === defaultValue) || + options?.find( + (o) => typeof o.value === 'object' && o.value?.name === defaultValue + ) || + null; + setSelectedItem(newSelectedItem); + }, [defaultValue, options]); + + const { + isOpen, + getToggleButtonProps, + getLabelProps, + getMenuProps, + highlightedIndex, + getItemProps, + } = useSelect({ + id, + items: options, + itemToString, + selectedItem, + onSelectedItemChange: ({ selectedItem: newSelectedItem }) => { + setSelectedItem(newSelectedItem ?? null); + if (onSelectionChange) onSelectionChange(newSelectedItem ?? null); + }, + }); + + const selectClasses = clsx('select', disabled && 'select--disabled'); + + const placeholderValue = + placeholder || t('global.select'); + + return ( +
+ {label && !hideLabel && ( + + )} +
+
+ {selectedItem?.label ?? placeholderValue} + } + /> +
+
    + {isOpen && + options.map((item, index) => ( +
  • + {item.label} +
  • + ))} +
+ {error &&

{error}

} +
+
+ ); + } +); + +export default FormSelect; diff --git a/GUI/src/components/FormElements/FormTextarea/FormTextarea.scss b/GUI/src/components/FormElements/FormTextarea/FormTextarea.scss new file mode 100644 index 00000000..ff1971ac --- /dev/null +++ b/GUI/src/components/FormElements/FormTextarea/FormTextarea.scss @@ -0,0 +1,109 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.textarea { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__wrapper { + flex: 1; + display: flex; + flex-direction: column; + gap: 7px; + position: relative; + } + + &__error { + width: 100%; + margin-right: 6px; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + color: get-color(black-coral-20); + border-radius: $veera-radius-s; + background-color: get-color(jasper-3); + font-size: 13px; + line-height: 20px; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); + + &::before { + content: ''; + display: block; + background-color: get-color(jasper-3); + border-left: 16px solid transparent; + border-right: 16px solid transparent; + border-bottom: 25px; + } + } + + &__max-length-top { + position: absolute; + top: 10px; + right: 8px; + font-size: $veera-font-size-80; + color: get-color(black-coral-12); + pointer-events: none; + } + + &__max-length-bottom { + position: absolute; + bottom: 10px; + right: 8px; + font-size: $veera-font-size-80; + color: get-color(black-coral-12); + pointer-events: none; + } + + textarea { + width: 100%; + display: block; + appearance: none; + background-color: get-color(white); + border: 1px solid get-color(black-coral-6); + border-radius: $veera-radius-s; + color: var(--color-black); + font-size: $veera-font-size-80; + line-height: $veera-line-height-500; + height: 40px; + min-height: 40px; + padding: get-spacing(paldiski); + + &::placeholder { + color: get-color(black-coral-6); + } + + &:focus { + outline: none; + border-color: get-color(sapphire-blue-10); + } + } + + &--error { + input { + border-color: get-color(jasper-10); + } + } + + &--disabled & { + input { + background-color: get-color(black-coral-0); + } + } + + &--maxlength-shown { + textarea { + padding-right: 70px; + } + } +} diff --git a/GUI/src/components/FormElements/FormTextarea/index.tsx b/GUI/src/components/FormElements/FormTextarea/index.tsx new file mode 100644 index 00000000..4190c782 --- /dev/null +++ b/GUI/src/components/FormElements/FormTextarea/index.tsx @@ -0,0 +1,72 @@ +import { ChangeEvent, forwardRef, useId, useState } from 'react'; +import TextareaAutosize, { TextareaAutosizeProps } from 'react-textarea-autosize'; +import clsx from 'clsx'; + +import './FormTextarea.scss'; + +type TextareaProps = TextareaAutosizeProps & { + label: string; + name: string; + hideLabel?: boolean; + showMaxLength?: boolean; + maxLengthBottom?: boolean; +}; + +const FormTextarea = forwardRef(( + { + label, + name, + maxLength = 2000, + minRows = 3, + maxRows = 3, + disabled, + hideLabel, + showMaxLength, + maxLengthBottom, + defaultValue, + onChange, + ...rest + }, + ref, +) => { + const id = useId(); + const [currentLength, setCurrentLength] = useState((typeof defaultValue === 'string' && defaultValue.length) || 0); + const textareaClasses = clsx( + 'textarea', + disabled && 'textarea--disabled', + showMaxLength && 'textarea--maxlength-shown', + ); + + const handleOnChange = (e: ChangeEvent) => { + if (showMaxLength) { + setCurrentLength(e.target.value.length); + } + }; + + return ( +
+ {label && !hideLabel && } +
+ { + if (onChange) onChange(e); + handleOnChange(e); + }} + {...rest} + /> + {showMaxLength && ( +
{currentLength}/{maxLength}
+ )} +
+
+ ); +}); + +export default FormTextarea; diff --git a/GUI/src/components/FormElements/SearchInput/SearchInput.scss b/GUI/src/components/FormElements/SearchInput/SearchInput.scss new file mode 100644 index 00000000..a5bec7c6 --- /dev/null +++ b/GUI/src/components/FormElements/SearchInput/SearchInput.scss @@ -0,0 +1,36 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; + +.search-input-container { + position: relative; + width: 100%; + + .search-button { + position: absolute; + right: 0px; + background: none; + border: none; + cursor: pointer; + color: get-color(black-coral-6); + padding: 5px; + display: flex; + align-items: center; + justify-content: center; + z-index: 2; + + &:hover { + color: get-color(sapphire-blue-10); + } + + &:disabled { + color: get-color(black-coral-3); + cursor: not-allowed; + } + } + + // Add some padding to the right of the input to prevent text overlap with the icon + input { + padding-right: 40px !important; + } +} \ No newline at end of file diff --git a/GUI/src/components/FormElements/SearchInput/index.tsx b/GUI/src/components/FormElements/SearchInput/index.tsx new file mode 100644 index 00000000..7a85a47c --- /dev/null +++ b/GUI/src/components/FormElements/SearchInput/index.tsx @@ -0,0 +1,82 @@ +import { forwardRef, useState, useEffect, ChangeEvent, KeyboardEvent } from 'react'; +import { MdOutlineSearch } from 'react-icons/md'; +import { Icon, FormInput } from 'components'; +import { DefaultTFuncReturn } from 'i18next'; +import './SearchInput.scss'; + +type SearchInputProps = { + onSearch: (searchTerm: string) => void; + placeholder?: string | DefaultTFuncReturn; + initialValue?: string; + label?: string; + disabled?: boolean; + name?: string; +}; + +const SearchInput = forwardRef( + ( + { + onSearch, + placeholder = 'Search...', + initialValue = '', + label = '', + disabled = false, + name = 'search', + }, + ref + ) => { + const [searchTerm, setSearchTerm] = useState(initialValue==="all"?"":initialValue); + + // Add useEffect to update internal state when initialValue prop changes + useEffect(() => { + setSearchTerm(initialValue==="all"?"":initialValue); + }, [initialValue]); + + const handleChange = (e: ChangeEvent) => { + setSearchTerm(e.target.value); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + onSearch(searchTerm); + } + }; + + const handleSearchClick = () => { + onSearch(searchTerm); + }; + + return ( +
+ + + +
+ ); + } +); + +export default SearchInput; \ No newline at end of file diff --git a/GUI/src/components/FormElements/Switch/Switch.scss b/GUI/src/components/FormElements/Switch/Switch.scss new file mode 100644 index 00000000..fddf67c0 --- /dev/null +++ b/GUI/src/components/FormElements/Switch/Switch.scss @@ -0,0 +1,68 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.switch { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + &__button { + display: flex; + align-items: center; + gap: 4px; + height: 40px; + isolation: isolate; + padding: 4px; + border-radius: 20px; + background-color: get-color(black-coral-1); + font-size: $veera-font-size-80; + line-height: $veera-line-height-500; + color: get-color(black-coral-12); + position: relative; + transition: background-color .25s ease-out; + + &[aria-checked=true] { + background-color: var(--active-color, get-color(sapphire-blue-10)); + color: get-color(sapphire-blue-10); + + #{$self} { + &__off { + color: get-color(white); + background: none; + } + + &__on { + color: var(--active-color, get-color(sapphire-blue-10)); + background-color: get-color(white); + } + } + } + } + + &__thumb { + display: none; + } + + &__on, + &__off { + display: flex; + border-radius: 20px; + padding: 5.5px 10px; + font-weight: $veera-font-weight-delta; + transition: all .25s ease-out; + } + + &__off { + font-weight: $veera-font-weight-delta; + background-color: get-color(white); + } +} diff --git a/GUI/src/components/FormElements/Switch/index.tsx b/GUI/src/components/FormElements/Switch/index.tsx new file mode 100644 index 00000000..ed414c7e --- /dev/null +++ b/GUI/src/components/FormElements/Switch/index.tsx @@ -0,0 +1,68 @@ +import { forwardRef, useId } from 'react'; +import * as RadixSwitch from '@radix-ui/react-switch'; +import { useTranslation } from 'react-i18next'; +import { ControllerRenderProps } from 'react-hook-form'; + +import './Switch.scss'; + +type SwitchProps = Partial & { + onLabel?: string; + offLabel?: string; + onColor?: string; + name?: string; + label: string; + checked?: boolean; + defaultChecked?: boolean; + hideLabel?: boolean; + onCheckedChange?: (checked: boolean) => void; +}; + +const Switch = forwardRef( + ( + { + onLabel, + offLabel, + onColor, + name, + label, + checked, + hideLabel, + onCheckedChange, + defaultChecked, + }, + ref + ) => { + const id = useId(); + const { t } = useTranslation(); + const onValueLabel = onLabel || t('global.on'); + const offValueLabel = offLabel || t('global.off'); + + return ( +
+ {label && !hideLabel && ( + + )} + + + {onValueLabel} + {offValueLabel} + +
+ ); + } +); + +export default Switch; diff --git a/GUI/src/components/FormElements/SwitchBox/SwitchBox.scss b/GUI/src/components/FormElements/SwitchBox/SwitchBox.scss new file mode 100644 index 00000000..2f7a0490 --- /dev/null +++ b/GUI/src/components/FormElements/SwitchBox/SwitchBox.scss @@ -0,0 +1,45 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.switchbox { + $self: &; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + + &__button { + width: 48px; + height: 8px; + border-radius: 4px; + background-color: get-color(black-coral-6); + position: relative; + + &[aria-checked=true] { + background-color: get-color(sapphire-blue-4); + + #{$self} { + &__thumb { + transform: translate(24px, -50%); + background-color: get-color(sapphire-blue-10); + } + } + } + } + + &__thumb { + position: absolute; + width: 24px; + height: 24px; + border-radius: 50%; + background-color: get-color(white); + border: 1px solid get-color(black-coral-2); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.14); + left: 0; + top: 50%; + transform: translateY(-50%); + transition: all .25s ease-out; + } +} diff --git a/GUI/src/components/FormElements/SwitchBox/index.tsx b/GUI/src/components/FormElements/SwitchBox/index.tsx new file mode 100644 index 00000000..1550576a --- /dev/null +++ b/GUI/src/components/FormElements/SwitchBox/index.tsx @@ -0,0 +1,44 @@ +import { forwardRef, useId } from 'react'; +import * as RadixSwitch from '@radix-ui/react-switch'; +import { ControllerRenderProps } from 'react-hook-form'; + +import './SwitchBox.scss'; + +type SwitchBoxProps = Partial & { + name?: string; + label: string; + checked?: boolean; + hideLabel?: boolean; + onCheckedChange?: (checked: boolean) => void; +} + +const SwitchBox = forwardRef(( + { + name, + label, + checked, + hideLabel, + onCheckedChange, + }, + ref, +) => { + const id = useId(); + + return ( +
+ {label && !hideLabel && } + + + +
+ ); +}); + +export default SwitchBox; diff --git a/GUI/src/components/FormElements/index.tsx b/GUI/src/components/FormElements/index.tsx new file mode 100644 index 00000000..ac295d55 --- /dev/null +++ b/GUI/src/components/FormElements/index.tsx @@ -0,0 +1,23 @@ +import FormInput from './FormInput'; +import FormTextarea from './FormTextarea'; +import FormSelect from './FormSelect'; +import FormMultiselect from './FormSelect/FormMultiselect'; +import Switch from './Switch'; +import FormCheckboxes from './FormCheckboxes'; +import FormRadios from './FormRadios'; +import FormCheckbox from './FormCheckbox'; +import FormDatepicker from './FormDatepicker'; +import SwitchBox from './SwitchBox'; + +export { + FormInput, + FormTextarea, + FormSelect, + FormMultiselect, + Switch, + FormCheckboxes, + FormRadios, + FormCheckbox, + FormDatepicker, + SwitchBox, +}; diff --git a/GUI/src/components/Header/Header.scss b/GUI/src/components/Header/Header.scss new file mode 100644 index 00000000..542c06f6 --- /dev/null +++ b/GUI/src/components/Header/Header.scss @@ -0,0 +1,10 @@ +@import '@buerokratt-ria/styles/styles/tools/spacing'; +@import '@buerokratt-ria/styles/styles/tools/color'; + +.header { + height: 100px; + padding: 24px 24px 24px 42px; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.14), 0 2px 2px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.2); + background-color: get-color(white); + z-index: 99; +} diff --git a/GUI/src/components/Header/index.tsx b/GUI/src/components/Header/index.tsx new file mode 100644 index 00000000..6b5e3d58 --- /dev/null +++ b/GUI/src/components/Header/index.tsx @@ -0,0 +1,196 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { FC, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; + +import { Track, Button, Dialog } from 'components'; +import useStore from 'store'; +import { useToast } from 'hooks/useToast'; +import apiDev from 'services/api-dev'; +import { useCookies } from 'react-cookie'; +import './Header.scss'; +import { useDialog } from 'hooks/useDialog'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import { authEndpoints } from 'utils/endpoints'; +import { authQueryKeys } from 'utils/queryKeys'; +import { UserInfo } from 'types/userInfo'; + +interface HeaderProps { + toastContext: any; + user: UserInfo | null; +} + +const Header: FC = () => { + const { t } = useTranslation(); + const userInfo = useStore((state) => state.userInfo); + const toast = useToast(); + + const { open } = useDialog(); + + const [sessionTimeOutDuration, setSessionTimeOutDuration] = + useState(30); + const [sessionTimeOutModalOpened, setSessionTimeOutModalOpened] = + useState(false); + const [sessionExtentionInProgress, setSessionExtentionInProgress] = + useState(false); + const customJwtCookieKey = 'customJwtCookie'; + + useEffect(() => { + const interval = setInterval(() => { + const expirationTimeStamp = localStorage.getItem('exp'); + if ( + expirationTimeStamp !== 'null' && + expirationTimeStamp !== null && + expirationTimeStamp !== undefined + ) { + const expirationDate = new Date(parseInt(expirationTimeStamp) ?? ''); + const currentDate = new Date(Date.now()); + if ( + expirationDate.getTime() - currentDate.getTime() <= 240000 + ) { + if (!sessionTimeOutModalOpened) { + setSessionTimeOutModalOpened(true); + setSessionTimeOutDuration(30); + } + } + } + }, 2000); + return () => clearInterval(interval); + }, [open, sessionTimeOutDuration]); + + useEffect(() => { + let timer= null; + if (sessionTimeOutModalOpened) { + timer = setInterval(() => { + setSessionTimeOutDuration((prev) => { + if (prev > 0) { + return prev - 1; + } else { + if (!sessionExtentionInProgress) handleLogout(); + return 0; + } + }); + }, 1000); + } else if (timer) { + clearInterval(timer); + } + + return () => { + if (timer) { + clearInterval(timer); + } + }; + }, [sessionTimeOutModalOpened]); + + const [, setCookie] = useCookies([customJwtCookieKey]); + + const setNewCookie = (cookieValue: string) => { + const cookieOptions = { path: '/' }; + setCookie(customJwtCookieKey, cookieValue, cookieOptions); + }; + + const extendUserSessionMutation = useMutation({ + mutationFn: async () => { + return await apiDev.get(authEndpoints.GET_EXTENDED_COOKIE()); + }, + onSuccess: (data) => { + setNewCookie(data?.data?.response); + setSessionTimeOutDuration(30); + setSessionTimeOutModalOpened(false); + setSessionExtentionInProgress(false); + refetch() + }, + onError: (error: AxiosError) => { + handleLogout(); + }, + }); + + const { refetch } = useQuery({ + queryKey: authQueryKeys.USER_DETAILS(), + onSuccess: (res: { response: UserInfo }) => { + localStorage.setItem('exp', res.response.JWTExpirationTimestamp); + useStore.getState().setUserInfo(res.response); + }, + enabled: false + }); + const logoutMutation = useMutation({ + mutationFn: () => apiDev.get(authEndpoints.LOGOUT()), + onSuccess() { + localStorage.removeItem('exp'); + window.location.href = import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN; + }, + onError: async (error: AxiosError) => { + toast.open({ + type: 'error', + title: t('global.notificationError'), + message: error.message, + }); + }, + }); + + const handleLogout = () => { + localStorage.removeItem('exp'); + logoutMutation.mutate(); + }; + return ( +
+
+ + {userInfo && ( + + + + )} + +
+ + {sessionTimeOutModalOpened && ( + setSessionTimeOutModalOpened(false)} + isOpen={sessionTimeOutModalOpened} + title={t('global.sessionTimeOutTitle') ?? ''} + footer={ +
+ + +
+ } + > +

+ {t('global.sessionTimeOutDesc', { + seconds: sessionTimeOutDuration, + }) ?? ''} +

+
+ )} +
+ ); +}; + +export default Header; diff --git a/GUI/src/components/Icon/Icon.scss b/GUI/src/components/Icon/Icon.scss new file mode 100644 index 00000000..ce570acf --- /dev/null +++ b/GUI/src/components/Icon/Icon.scss @@ -0,0 +1,17 @@ +@import 'src/styles/tools/spacing'; + +.icon { + display: inline-flex; + align-items: center; + justify-content: center; + + &--small { + width: get-spacing(haapsalu); + height: get-spacing(haapsalu); + } + + &--medium { + width: get-spacing(kuressaare); + height: get-spacing(kuressaare); + } +} diff --git a/GUI/src/components/Icon/index.tsx b/GUI/src/components/Icon/index.tsx new file mode 100644 index 00000000..d9ab3988 --- /dev/null +++ b/GUI/src/components/Icon/index.tsx @@ -0,0 +1,26 @@ +import { CSSProperties, forwardRef, ReactNode, StyleHTMLAttributes } from 'react'; +import * as AccessibleIcon from '@radix-ui/react-accessible-icon'; +import clsx from 'clsx'; + +import './Icon.scss'; + +type IconProps = StyleHTMLAttributes & { + label?: string | null; + icon: ReactNode; + size?: 'small' | 'medium'; +}; + +const Icon = forwardRef(({ label, icon, size = 'small', ...rest }, ref) => { + const iconClasses = clsx( + 'icon', + `icon--${size}`, + ); + + return ( + + {icon} + + ); +}); + +export default Icon; diff --git a/GUI/src/components/Label/Label.scss b/GUI/src/components/Label/Label.scss new file mode 100644 index 00000000..4daeb742 --- /dev/null +++ b/GUI/src/components/Label/Label.scss @@ -0,0 +1,90 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.label { + $self: &; + display: flex; + padding: 1.5px 16px; + font-size: 12px; + font-weight: $veera-font-weight-delta; + border: 2px solid; + background-color: get-color(white); + border-radius: $veera-radius-s; + position: relative; + width: fit-content; + height: fit-content; + text-transform: capitalize; + + &--info { + color: get-color(sapphire-blue-10); + border-color: get-color(sapphire-blue-10); + + #{$self} { + &__icon { + border-color: get-color(sapphire-blue-10); + } + } + } + + &--warning { + color: get-color(dark-tangerine-10); + border-color: get-color(dark-tangerine-10); + + #{$self} { + &__icon { + border-color: get-color(dark-tangerine-10); + } + } + } + + &--error { + color: get-color(jasper-10); + border-color: get-color(jasper-10); + + #{$self} { + &__icon { + border-color: get-color(jasper-10); + } + } + } + + &--default { + color: get-color(black-coral-7); + border-color: get-color(black-coral-7); + + #{$self} { + &__icon { + border-color: get-color(black-coral-7); + } + } + } + + &--success { + color: get-color(sea-green-10); + border-color: get-color(sea-green-10); + + #{$self} { + &__icon { + border-color: get-color(sea-green-10); + } + } + } + + &__icon { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + font-size: 13px; + line-height: 15px; + right: -8px; + top: 4px; + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid; + background-color: get-color(white); + } +} diff --git a/GUI/src/components/Label/index.tsx b/GUI/src/components/Label/index.tsx new file mode 100644 index 00000000..e27d0d4f --- /dev/null +++ b/GUI/src/components/Label/index.tsx @@ -0,0 +1,40 @@ +import { forwardRef, PropsWithChildren, ReactNode } from 'react'; +import clsx from 'clsx'; +import { MdOutlineCheck } from 'react-icons/md'; + +import { Tooltip } from 'components'; +import './Label.scss'; + +type LabelProps = { + type?: 'warning' | 'error' | 'info' | 'success' | 'default'; + tooltip?: ReactNode; +} + +const Label = forwardRef>(( + { + type = 'default', + tooltip, + children, + }, ref, +) => { + const labelClasses = clsx( + 'label', + `label--${type}`, + tooltip && 'label--tooltip', + ); + + return ( + + {children} + {tooltip && ( + + + {type === 'success' ? : 'i'} + + + )} + + ); +}); + +export default Label; diff --git a/GUI/src/components/LabelChip/index.scss b/GUI/src/components/LabelChip/index.scss new file mode 100644 index 00000000..ed40b04a --- /dev/null +++ b/GUI/src/components/LabelChip/index.scss @@ -0,0 +1,23 @@ +.label-chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 20px; + border-radius: 16px; + background-color: #e0e0e0; + margin: 4px; + gap: 7px; +} + +.label-chip .label { + margin-right: 8px; +} + +.label-chip .button { + background: none; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/GUI/src/components/LabelChip/index.tsx b/GUI/src/components/LabelChip/index.tsx new file mode 100644 index 00000000..146e80c1 --- /dev/null +++ b/GUI/src/components/LabelChip/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import './index.scss'; +import { MdClose } from 'react-icons/md'; + +type LabelChipProps = { + label: string; + onRemove: () => void; +}; + +const LabelChip: React.FC = ({ label, onRemove }) => { + return ( +
+ {label} + +
+ ); +}; + +export default LabelChip; diff --git a/GUI/src/components/Layout/Layout.scss b/GUI/src/components/Layout/Layout.scss new file mode 100644 index 00000000..13674f6f --- /dev/null +++ b/GUI/src/components/Layout/Layout.scss @@ -0,0 +1,28 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; + +.layout { + height: 100%; + display: flex; + + &__wrapper { + flex: 1; + display: flex; + flex-direction: column; + position: relative; + } + + &__main { + flex: 1; + display: flex; + flex-direction: column; + overflow-x: hidden; + gap: get-spacing(haapsalu); + padding: get-spacing(haapsalu); + position: absolute; + top: 100px; + left: 0; + right: 0; + bottom: 0px; + } +} diff --git a/GUI/src/components/Layout/index.tsx b/GUI/src/components/Layout/index.tsx new file mode 100644 index 00000000..c26eca42 --- /dev/null +++ b/GUI/src/components/Layout/index.tsx @@ -0,0 +1,23 @@ +import { FC } from 'react'; +import { Outlet } from 'react-router-dom'; +import useStore from 'store'; +import './Layout.scss'; +import { useToast } from '../../hooks/useToast'; +import Header from 'components/Header'; +import MainNavigation from 'components/MainNavigation'; + +const Layout: FC = () => { + return ( +
+ +
+
+
+ +
+
+
+ ); +}; + +export default Layout; diff --git a/GUI/src/components/MainNavigation/MainNavigation.scss b/GUI/src/components/MainNavigation/MainNavigation.scss new file mode 100644 index 00000000..93b2556d --- /dev/null +++ b/GUI/src/components/MainNavigation/MainNavigation.scss @@ -0,0 +1,130 @@ +@import '@buerokratt-ria/styles/styles/tools/spacing'; +@import '@buerokratt-ria/styles/styles/tools/color'; +@import '@buerokratt-ria/styles/styles/settings/variables/typography'; + +.nav { + $self: &; + width: 208px; + background-color: get-color(sapphire-blue-10); + overflow: auto; + scrollbar-width: none; + transition: width .1s ease-out; + z-index: 100; + + &::-webkit-scrollbar { + display: none; + } + + li, a, .nav__toggle, .nav__menu-toggle { + font-size: 14px; + line-height: 1.5; + } + + &__menu-toggle { + display: flex; + align-items: center; + + &:hover { + background-color: get-color(sapphire-blue-8); + } + + &:active { + background-color: get-color(sapphire-blue-7); + } + } + + a, .nav__toggle { + width: 100%; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + color: get-color(black-coral-0); + padding: 14px 8px 14px 32px; + box-shadow: inset 0 -1px 0 get-color(sapphire-blue-14); + + span:not(.icon) { + flex: 1; + display: block; + } + + &:hover { + background-color: get-color(sapphire-blue-8); + } + + &:active { + background-color: #2E78B3; + } + + &.active { + background-color: #2E78B3; + font-weight: 700; + } + } + + &__toggle { + &[aria-expanded=true] { + font-weight: 700; + + .icon { + transform: rotate(180deg); + } + + + ul { + display: block; + } + } + + &.nav__toggle--icon { + + .icon:first-child { + transform: none; + } + } + } + + &__toggle-icon { + margin-left: auto; + } + + &__menu-toggle { + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + color: get-color(white); + padding: 14px 8px; + box-shadow: inset 0 -1px 0 get-color(sapphire-blue-14); + } + + &__submenu { + display: none; + + a, .nav__toggle { + background-color: get-color(sapphire-blue-14); + box-shadow: inset 0 -1px 0 get-color(sapphire-blue-17); + } + + #{$self} { + &__submenu { + a { + background-color: get-color(sapphire-blue-17); + box-shadow: inset 0 -1px 0 get-color(black); + padding: 14px 48px 14px 40px; + } + } + } + } +} + +.collapsed { + .nav__submenu { + visibility: hidden; + height: 0; + } + + button[aria-expanded=true] { + .icon { + transform: rotate(0deg); + } + } +} diff --git a/GUI/src/components/MainNavigation/index.tsx b/GUI/src/components/MainNavigation/index.tsx new file mode 100644 index 00000000..353705cb --- /dev/null +++ b/GUI/src/components/MainNavigation/index.tsx @@ -0,0 +1,162 @@ +import { FC, MouseEvent, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NavLink, useLocation } from 'react-router-dom'; +import { MdCorporateFare, MdFileCopy, MdKeyboardArrowDown, MdOutlineDataset, MdSearch, MdSupervisorAccount } from 'react-icons/md'; +import { useQuery } from '@tanstack/react-query'; +import clsx from 'clsx'; +import { Icon } from 'components'; +import type { MenuItem } from 'types/mainNavigation'; +import './MainNavigation.scss'; +import apiDev from 'services/api-dev'; +import { userManagementEndpoints } from 'utils/endpoints'; +import { integratedAgenciesQueryKeys } from 'utils/queryKeys'; +import { ROLES } from 'enums/roles'; + +const MainNavigation: FC = () => { + const { t } = useTranslation(); + const [menuItems, setMenuItems] = useState([]); + + const items = [ + { + id: 'userManagement', + label: t('menu.userManagement'), + path: '/user-management', + icon: , + }, + { + id: 'agencies', + label: t('menu.agencies'), + path: '/integrated-agencies', + icon: + }, + { + id: 'dataSets', + label: t('menu.dataSets.title'), + path: '', + icon: , + children: [ + { + label: t('menu.dataSets.overview'), + path: 'datasets', + }, + { + label: t('menu.dataSets.progress'), + path: 'datasets/progress', + } + ], + }, + { + id: 'dataModels', + label: t('menu.dataModels.title'), + path: '', + icon: , + children: [ + { + label: t('menu.dataModels.overview'), + path: 'data-models', + }, + { + label: t('menu.dataModels.progress'), + path: 'training/progress', + } + ], + }, + { + id: 'testing', + label: t('menu.testModel'), + path: '/testing', + icon: + } + ]; + + const filterItemsByRole = (role: string[], items: MenuItem[]) => { + return items?.filter((item) => { + if (role.includes(ROLES.ROLE_ADMINISTRATOR)) return item?.id; + else if (role.includes(ROLES.ROLE_MODEL_TRAINER)) + return item?.id !== 'userManagement' && item?.id !== 'integration'; + else return false; + }); + }; + + useQuery(integratedAgenciesQueryKeys.USER_ROLES(), { + queryFn: async () => { + const res = await apiDev.get(userManagementEndpoints.FETCH_USER_ROLES()); + return res?.data?.response; + }, + onSuccess: (res) => { + const roles = res; + const filteredItems = filterItemsByRole(roles, items); + setMenuItems(filteredItems); + }, + onError: (error) => { + console.error('Error fetching user roles:', error); + }, + }); + const location = useLocation(); + const navCollapsed = false; + + const handleNavToggle = (event: MouseEvent) => { + const isExpanded = + event?.currentTarget?.getAttribute('aria-expanded') === 'true'; + event?.currentTarget?.setAttribute( + 'aria-expanded', + isExpanded ? 'false' : 'true' + ); + }; + + const renderMenuTree = (menuItems: MenuItem[]) => { + return menuItems?.map((menuItem) => ( +
  • + {menuItem?.children ? ( +
    + +
      + {renderMenuTree(menuItem?.children)} +
    +
    + ) : ( + + {' '} + + {menuItem?.label} + + )} +
  • + )); + }; + + if (!menuItems) return null; + + return ( + + ); +}; + +export default MainNavigation; \ No newline at end of file diff --git a/GUI/src/components/Popover/Popover.scss b/GUI/src/components/Popover/Popover.scss new file mode 100644 index 00000000..9278c909 --- /dev/null +++ b/GUI/src/components/Popover/Popover.scss @@ -0,0 +1,15 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/typography'; + +.popover { + background-color: get-color(white); + padding: 4px; + border-radius: 4px; + filter: drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.25)); + font-size: $veera-font-size-80; + + &__arrow { + fill: get-color(white); + } +} diff --git a/GUI/src/components/Popover/index.tsx b/GUI/src/components/Popover/index.tsx new file mode 100644 index 00000000..929015bd --- /dev/null +++ b/GUI/src/components/Popover/index.tsx @@ -0,0 +1,27 @@ +import { FC, PropsWithChildren, ReactNode } from 'react'; +import * as RadixPopover from '@radix-ui/react-popover'; + +import './Popover.scss'; + +type PopoverProps = { + content: ReactNode; + defaultOpen?: boolean; +} + +const Popover: FC> = ({ children, content, defaultOpen = false }) => { + return ( + + + {children} + + + + {content} + + + + + ); +}; + +export default Popover; diff --git a/GUI/src/components/ProgressBar/index.scss b/GUI/src/components/ProgressBar/index.scss new file mode 100644 index 00000000..bc4f3a53 --- /dev/null +++ b/GUI/src/components/ProgressBar/index.scss @@ -0,0 +1,28 @@ +.progress-bar-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + } + + .progress-bar-label { + margin-bottom: 4px; + font-size: 14px; + } + + .progress-bar-root { + position: relative; + overflow: hidden; + background-color: #e0e0e0; + border-radius: 4px; + width: 100%; + height: 10px; + } + + .progress-bar-indicator { + background-color: #07478d; + height: 100%; + transition: width 0.3s; + border-radius: 20px; + } + \ No newline at end of file diff --git a/GUI/src/components/ProgressBar/index.tsx b/GUI/src/components/ProgressBar/index.tsx new file mode 100644 index 00000000..69d6a444 --- /dev/null +++ b/GUI/src/components/ProgressBar/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import * as Progress from '@radix-ui/react-progress'; +import './index.scss'; + +type ProgressBarProps = { + value: number; + max: number; + label?: string; +}; + +const ProgressBar: React.FC = ({ value, max, label }) => { + return ( +
    + + + + {label && } + +
    + ); +}; + +export default ProgressBar; diff --git a/GUI/src/components/Section/Section.scss b/GUI/src/components/Section/Section.scss new file mode 100644 index 00000000..cdbb136e --- /dev/null +++ b/GUI/src/components/Section/Section.scss @@ -0,0 +1,11 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/typography'; + +.section { + padding: get-spacing(haapsalu); + + &:not(:last-child) { + border-bottom: 1px solid get-color(black-coral-2); + } +} diff --git a/GUI/src/components/Section/index.tsx b/GUI/src/components/Section/index.tsx new file mode 100644 index 00000000..7ecd131d --- /dev/null +++ b/GUI/src/components/Section/index.tsx @@ -0,0 +1,13 @@ +import { forwardRef, PropsWithChildren } from 'react'; + +import './Section.scss'; + +const Section = forwardRef(({ children }, ref) => { + return ( +
    + {children} +
    + ); +}); + +export default Section; diff --git a/GUI/src/components/Toast/Toast.scss b/GUI/src/components/Toast/Toast.scss new file mode 100644 index 00000000..fd340916 --- /dev/null +++ b/GUI/src/components/Toast/Toast.scss @@ -0,0 +1,73 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.toast { + padding: 16px; + border-radius: 5px; + border: 1px solid; + display: flex; + flex-direction: column; + gap: 8px; + position: relative; + transition: opacity 0.25s ease-out; + + &__title { + display: flex; + align-items: center; + gap: 8px; + padding-right: 25px; + } + + &__list { + position: fixed; + bottom: 0; + right: 0; + display: flex; + flex-direction: column; + gap: 16px; + padding: 8px; + width: 408px; + max-width: 100vw; + z-index: 9999; + list-style: none; + } + + &__content { + font-size: $veera-font-size-80; + + a { + display: inline; + color: get-color(sapphire-blue-10); + text-decoration: underline; + } + } + + &__close { + position: absolute; + top: 16px; + right: 16px; + font-size: 20px; + } + + &--success { + border-color: get-color(sea-green-10); + background-color: get-color(sea-green-0); + } + + &--info { + border-color: get-color(sapphire-blue-10); + background-color: get-color(sapphire-blue-1); + } + + &--error { + border-color: get-color(jasper-10); + background-color: #FCEEEE; + } + + &--warning { + border-color: get-color(dark-tangerine-10); + background-color: get-color(dark-tangerine-1); + } +} diff --git a/GUI/src/components/Toast/index.tsx b/GUI/src/components/Toast/index.tsx new file mode 100644 index 00000000..ffa29f61 --- /dev/null +++ b/GUI/src/components/Toast/index.tsx @@ -0,0 +1,54 @@ +import { FC, useState } from 'react'; +import * as RadixToast from '@radix-ui/react-toast'; +import { + MdOutlineClose, + MdOutlineInfo, + MdCheckCircleOutline, + MdOutlineWarningAmber, + MdErrorOutline, +} from 'react-icons/md'; +import clsx from 'clsx'; + +import { Icon } from 'components'; +import type { ToastType } from 'context/ToastContext'; +import './Toast.scss'; + +type ToastProps = { + toast: ToastType; + close: () => void; +}; + +const toastIcons = { + info: , + success: , + warning: , + error: , +}; + +const Toast: FC = ({ toast, close }) => { + const [open, setOpen] = useState(true); + + const toastClasses = clsx('toast', `toast--${toast.type}`); + + return ( + + + + {toast.title} + + + {toast.message} + + + } size="medium" /> + + + ); +}; + +export default Toast; diff --git a/GUI/src/components/Tooltip/Tooltip.scss b/GUI/src/components/Tooltip/Tooltip.scss new file mode 100644 index 00000000..bd062f75 --- /dev/null +++ b/GUI/src/components/Tooltip/Tooltip.scss @@ -0,0 +1,16 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/typography'; + +.tooltip { + background-color: get-color(white); + padding: 4px; + border-radius: 4px; + filter: drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.25)); + font-size: $veera-font-size-80; + max-width: 50vw; + + &__arrow { + fill: get-color(white); + } +} diff --git a/GUI/src/components/Tooltip/index.tsx b/GUI/src/components/Tooltip/index.tsx new file mode 100644 index 00000000..3cd41ac2 --- /dev/null +++ b/GUI/src/components/Tooltip/index.tsx @@ -0,0 +1,28 @@ +import { FC, PropsWithChildren, ReactNode } from 'react'; +import * as RadixTooltip from '@radix-ui/react-tooltip'; + +import './Tooltip.scss'; + +type TooltipProps = { + content: ReactNode; +} + +const Tooltip: FC> = ({ content, children }) => { + return ( + + + + {children} + + + + {content} + + + + + + ); +}; + +export default Tooltip; diff --git a/GUI/src/components/Track/index.tsx b/GUI/src/components/Track/index.tsx new file mode 100644 index 00000000..2b66b6e7 --- /dev/null +++ b/GUI/src/components/Track/index.tsx @@ -0,0 +1,57 @@ +import { FC, HTMLAttributes, PropsWithChildren } from 'react'; + +type TrackProps = HTMLAttributes & { + gap?: number; + align?: 'left' | 'center' | 'right' | 'stretch'; + justify?: 'start' | 'between' | 'center' | 'around' | 'end'; + direction?: 'horizontal' | 'vertical'; + isMultiline?: boolean; +} + +const alignMap = { + left: 'flex-start', + center: 'center', + right: 'flex-end', + stretch: 'stretch', +}; + +const justifyMap = { + start: 'flex-start', + between: 'space-between', + center: 'center', + around: 'space-around', + end: 'flex-end', +}; + +const Track: FC> = ( + { + gap = 0, + align = 'center', + justify = 'start', + direction = 'horizontal', + isMultiline = false, + children, + style, + ...rest + }, +) => { + return ( +
    + {children} +
    + ); +}; + +export default Track; diff --git a/GUI/src/components/index.tsx b/GUI/src/components/index.tsx new file mode 100644 index 00000000..5bb3b36f --- /dev/null +++ b/GUI/src/components/index.tsx @@ -0,0 +1,55 @@ +import Layout from './Layout'; +import Button from './Button'; +import Icon from './Icon'; +import Track from './Track'; +import { + FormInput, + FormTextarea, + FormSelect, + FormMultiselect, + Switch, + FormCheckboxes, + FormRadios, + FormCheckbox, + FormDatepicker, + SwitchBox, +} from './FormElements'; +import DataTable from './DataTable'; +import Tooltip from './Tooltip'; +import Card from './Card'; +import Label from './Label'; +import Toast from './Toast'; +import Popover from './Popover'; +import Collapsible from './Collapsible'; +import Box from './Box'; +import Drawer from './Drawer'; +import Dialog from './Dialog'; +import Section from './Section'; + +export { + Layout, + Button, + Icon, + Track, + Tooltip, + DataTable, + FormInput, + FormTextarea, + FormSelect, + FormMultiselect, + FormDatepicker, + Switch, + SwitchBox, + Card, + Label, + Toast, + FormCheckboxes, + FormRadios, + FormCheckbox, + Popover, + Collapsible, + Box, + Drawer, + Dialog, + Section, +}; diff --git a/GUI/src/components/molecules/CircularSpinner/CircularSpinner.tsx b/GUI/src/components/molecules/CircularSpinner/CircularSpinner.tsx new file mode 100644 index 00000000..60eaa8ad --- /dev/null +++ b/GUI/src/components/molecules/CircularSpinner/CircularSpinner.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import './Spinner.scss'; + +interface SpinnerProps { + size?: number; +} + +const CircularSpinner: React.FC = ({ size = 80 }) => { + return ( +
    +
    +
    + ); +}; + +export default CircularSpinner; \ No newline at end of file diff --git a/GUI/src/components/molecules/CircularSpinner/Spinner.scss b/GUI/src/components/molecules/CircularSpinner/Spinner.scss new file mode 100644 index 00000000..d2297dea --- /dev/null +++ b/GUI/src/components/molecules/CircularSpinner/Spinner.scss @@ -0,0 +1,23 @@ +.spinner-container { + display: flex; + justify-content: center; + align-items: center; + height: 80vh; + } + + .spinner { + border: 4px solid rgba(0, 0, 0, 0.1); + border-top: 4px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + \ No newline at end of file diff --git a/GUI/src/components/molecules/DataGenerationSessionCard/ValidationSessionCard.scss b/GUI/src/components/molecules/DataGenerationSessionCard/ValidationSessionCard.scss new file mode 100644 index 00000000..49a53320 --- /dev/null +++ b/GUI/src/components/molecules/DataGenerationSessionCard/ValidationSessionCard.scss @@ -0,0 +1,8 @@ +.validationHeader { + display: flex; + gap: 10px; + + @media (max-width: 600px) { + flex-wrap: wrap; + } +} diff --git a/GUI/src/components/molecules/DataGenerationSessionCard/index.tsx b/GUI/src/components/molecules/DataGenerationSessionCard/index.tsx new file mode 100644 index 00000000..817d2fbf --- /dev/null +++ b/GUI/src/components/molecules/DataGenerationSessionCard/index.tsx @@ -0,0 +1,63 @@ +import { useTranslation } from 'react-i18next'; +import ProgressBar from 'components/ProgressBar'; +import { Card, Label } from 'components'; +import { DataGenerationSessionsStatuses } from 'enums/datasetEnums'; +import './ValidationSessionCard.scss'; + +type ValidationSessionCardProps = { + dgName: string; + version: string; + isLatest: boolean; + status?: string; + validationMessage?: string; + progress: number; +}; + +const DataGenerationSessionCard: React.FC = ({ + dgName, + version, + isLatest, + status, + validationMessage, + progress, +}) => { + const { t } = useTranslation(); + + return ( + + {dgName} + {isLatest && } + {status === DataGenerationSessionsStatuses.VALIDATION_FAILED_STATUS && ( + + )} + + } + > +
    + {(status === DataGenerationSessionsStatuses.VALIDATION_FAILED_STATUS || + status === DataGenerationSessionsStatuses.VALIDATION_SUCCESS_STATUS) && + progress === 100 ? ( +
    + {validationMessage} +
    + ) : ( +
    +
    {status}
    + +
    {validationMessage}
    +
    + )} +
    +
    + ); +}; + +export default DataGenerationSessionCard; diff --git a/GUI/src/components/molecules/DataGenerationStatusLabel/index.tsx b/GUI/src/components/molecules/DataGenerationStatusLabel/index.tsx new file mode 100644 index 00000000..9c8425bd --- /dev/null +++ b/GUI/src/components/molecules/DataGenerationStatusLabel/index.tsx @@ -0,0 +1,36 @@ +import { DataGenerationStatus } from 'enums/datasetEnums'; +import Label from 'components/Label'; +import { LabelType } from 'enums/commonEnums'; +import { useTranslation } from 'react-i18next'; + +const DataGenerationStatusLabel = ({ + status, +}: { + status: string | undefined; +}) => { + const { t } = useTranslation(); + + if (status === DataGenerationStatus.SUCCESS) { + return ( + + ); + } else if (status === DataGenerationStatus.FAILED) { + return ( + + ); + } else if (status === DataGenerationStatus.IN_PROGRESS) { + return ( + + ); + } else { + return null; + } +}; + +export default DataGenerationStatusLabel; diff --git a/GUI/src/components/molecules/DataModelCard/DataModel.scss b/GUI/src/components/molecules/DataModelCard/DataModel.scss new file mode 100644 index 00000000..32d938a3 --- /dev/null +++ b/GUI/src/components/molecules/DataModelCard/DataModel.scss @@ -0,0 +1,14 @@ +.training-results-grid-container { + display: grid; + grid-template-columns: 3fr 1fr 1fr; + gap: 10px; +} + +.space-between { + display: flex; + gap: 1rem; +} + +.mt-3{ + margin-top: 3rem; +} \ No newline at end of file diff --git a/GUI/src/components/molecules/DataModelCard/index.tsx b/GUI/src/components/molecules/DataModelCard/index.tsx new file mode 100644 index 00000000..c14b3064 --- /dev/null +++ b/GUI/src/components/molecules/DataModelCard/index.tsx @@ -0,0 +1,175 @@ +import { FC, PropsWithChildren } from 'react'; +import Button from 'components/Button'; +import Label from 'components/Label'; +import { useDialog } from 'hooks/useDialog'; +import './DataModel.scss'; +import { Maturity, TrainingStatus } from 'enums/dataModelsEnums'; +import { useTranslation } from 'react-i18next'; +import { TrainingResultsResponse } from 'types/dataModels'; +import { formatDate } from 'utils/commonUtilts'; +import { useNavigate } from 'react-router-dom'; +import ModelResults from '../TrainingResults'; + +type DataModelCardProps = { + modelId: number | string; + dataModelName?: string; + datasetVersion?: string; + version?: string; + isLatest?: boolean; + lastTrained?: string; + trainingStatus?: string; + modelStatus?: string; + deploymentEnv?: string; + results?: TrainingResultsResponse | null; +}; + +const DataModelCard: FC> = ({ + modelId, + dataModelName, + datasetVersion, + version, + isLatest, + lastTrained, + trainingStatus, + modelStatus, + deploymentEnv, + results, + +}) => { + const { open, close } = useDialog(); + const { t } = useTranslation(); + const navigate = useNavigate(); + + let trainingResults = null; + if (results?.value) { + try { + trainingResults = JSON.parse(results.value); + } catch (error) { + console.error("Failed to parse training results:", error); + } + } + + const configureDataModel = () => { + navigate(`/configure-datamodel?datamodelId=${modelId}`); + } + + const renderTrainingStatus = (status: string | undefined) => { + if (status === TrainingStatus.RETRAINING_NEEDED) { + return ( + + ); + } else if (status === TrainingStatus.TRAINED) { + return ( + + ); + } else if (status === TrainingStatus.TRAINING_INPROGRESS || status === TrainingStatus.INITIATING_TRAINING) { + return ( + + ); + } else if (status === TrainingStatus.FAILED) { + return ( + + ); + } else if (status === TrainingStatus.NOT_TRAINED) { + return ; + } + }; + + const renderMaturityLabel = (status: string | undefined) => { + if (status === Maturity.UNDEPLOYED) { + return ( + + ); + } else if (status === Maturity.PRODUCTION) { + return ( + + ); + } else if (status === Maturity.TESTING) { + return ( + + ); + } + }; + + return ( +
    +
    +
    +

    {dataModelName}

    + +
    + +
    +
    +
    {`${t('dataModels.dataModelCard.datasetVersion') ?? ''} `}
    +
    {`: ${datasetVersion}`}
    +
    +

    + {t('dataModels.dataModelCard.lastTrained') ?? ''}:{' '} + {lastTrained && formatDate(new Date(lastTrained), 'D.M.yy-H:m')} +

    +
    +
    + {renderTrainingStatus(trainingStatus)} + + {isLatest && } + {renderMaturityLabel(deploymentEnv)} +
    + +
    + + ), + size: 'large', + content: ( +
    + {results ? ( + + ) : ( +
    + {t('dataModels.trainingResults.noResults') ?? ''} +
    + )} +
    + ), + }); + }} + > + {t('dataModels.trainingResults.viewResults') ?? ''} + + +
    +
    +
    + ); +}; + +export default DataModelCard; \ No newline at end of file diff --git a/GUI/src/components/molecules/DataModelForm/index.tsx b/GUI/src/components/molecules/DataModelForm/index.tsx new file mode 100644 index 00000000..fcacb5b7 --- /dev/null +++ b/GUI/src/components/molecules/DataModelForm/index.tsx @@ -0,0 +1,156 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + FormCheckboxes, + FormInput, + FormRadios, + FormSelect, + Label, +} from 'components'; +import { formattedArray, toLabelValueArray } from 'utils/commonUtilts'; +import { useQuery } from '@tanstack/react-query'; +import CircularSpinner from '../CircularSpinner/CircularSpinner'; +import { DataModel } from 'types/dataModels'; +import { dataModelsQueryKeys, datasetQueryKeys } from 'utils/queryKeys'; +import { getDeploymentEnvironments } from 'services/datamodels'; +import { getAllDatasetVersions } from 'services/datasets'; +import ModelResults from '../TrainingResults'; + +type DataModelFormType = { + dataModel: any; + handleChange: (name: keyof DataModel, value: any) => void; + errors?: Record; + type: string; +}; + +const DataModelForm: FC = ({ + dataModel, + handleChange, + errors, + type, +}) => { + const { t } = useTranslation(); + const [showTrainingResults, setShowTrainingResults] = useState(true); + const { data: deploymentEnvironmentsData } = useQuery({ + queryKey: datasetQueryKeys.DATASET_VERSIONS(), + queryFn: () => getDeploymentEnvironments(), + }); + + const { data: datasetVersions } = useQuery({ + queryKey: dataModelsQueryKeys.DATA_MODEL_DEPLOYMENT_ENVIRONMENTS(), + queryFn: () => getAllDatasetVersions(), + }); + + let trainingResults = null; + if (dataModel?.trainingResults?.value) { + try { + trainingResults = JSON.parse(dataModel.trainingResults.value); + } catch (error) { + console.error('Failed to parse training results JSON:', error); + } + } + return ( +
    + {type === 'create' ? ( +
    +
    + handleChange('modelName', e.target.value)} + error={errors?.modelName} + /> +
    +
    + {t('dataModels.dataModelForm.modelVersion')}{' '} + +
    +
    + ) : ( +
    +
    {dataModel.modelName}
    + +
    + )} + + {((type === 'configure') || type === 'create') + ? ( +
    +
    + {t('dataModels.dataModelForm.datasetGroup')}{' '} +
    +
    + { + handleChange('datasetId', selection?.value); + }} + value={dataModel?.datasetId === null && t('dataModels.dataModelForm.errors.datasetVersionNotExist')} + defaultValue={dataModel?.datasetId ? dataModel?.datasetId : t('dataModels.dataModelForm.errors.datasetVersionNotExist')} + error={errors?.datasetId} + /> +
    + {(type === 'configure') && !dataModel.datasetId && {t('dataModels.dataModelForm.errors.datasetVersionNotExist')}} +
    +
    + +
    + {t('dataModels.dataModelForm.baseModels')}{' '} +
    + +
    + + handleChange('baseModels', values.baseModels) + } + error={errors?.baseModels} + selectedValues={dataModel?.baseModels} + /> + {type === 'configure' && trainingResults && ( + setShowTrainingResults((prev) => !prev)} + > + {showTrainingResults ? "Hide Training Results" : "View Training Results"} + + )} +
    + {showTrainingResults && trainingResults && } + +
    + {t('dataModels.dataModelForm.deploymentPlatform')}{' '} +
    +
    + handleChange('deploymentEnvironment', value)} + error={errors?.deploymentEnvironment} + selectedValue={dataModel?.deploymentEnvironment} + /> +
    +
    + ) : ( + + )} +
    + ); +}; + +export default DataModelForm; diff --git a/GUI/src/components/molecules/DatasetCard/DatasetGroupCard.scss b/GUI/src/components/molecules/DatasetCard/DatasetGroupCard.scss new file mode 100644 index 00000000..a81ab511 --- /dev/null +++ b/GUI/src/components/molecules/DatasetCard/DatasetGroupCard.scss @@ -0,0 +1,91 @@ +.row { + margin: 10px 0; + display: flex; + align-items: center; +} + +.switch-row { + justify-content: space-between; +} + +.status-indicators { + display: flex; + align-items: center; + gap: 10px; +} + +.dot { + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 8px; +} + +.green { + background-color: green; +} + +.grey { + background-color: grey; +} + +.icon-text-row { + flex-direction: column; + justify-content: center; + text-align: center; +} + +.icon-image { + width: 50px; + height: 50px; +} + +.icon-text { + margin: 5px 0 0 0; + font-size: 16px; +} + +.label-row { + justify-content: flex-end; + display: flex; + align-items: center; + margin-top: 15px; +} + +.left-label { + font-size: 14px; + color: #333; +} + +.status { + display: flex; + align-items: center; + font-size: 12px; + color: #555; + bottom: 10px; + left: 20px; +} + +.colored-label{ + background-color: white; + border: solid 2px #D73E3E; + border-radius: 6px; + color: #D73E3E; + width: fit-content; + padding: 0px 5px; + font-size: 14px; + margin-right: 5px; +} + +.text{ + font-size: 16px; + margin-bottom: 5px; +} + +.py-3{ + padding: 8px 0px; +} + +.flex{ + display: flex; +} \ No newline at end of file diff --git a/GUI/src/components/molecules/DatasetCard/index.tsx b/GUI/src/components/molecules/DatasetCard/index.tsx new file mode 100644 index 00000000..d735abb3 --- /dev/null +++ b/GUI/src/components/molecules/DatasetCard/index.tsx @@ -0,0 +1,82 @@ +import { FC, PropsWithChildren } from 'react'; +import './DatasetGroupCard.scss'; +import { Switch } from 'components/FormElements'; +import Button from 'components/Button'; +import Label from 'components/Label'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useDialog } from 'hooks/useDialog'; +import { datasetQueryKeys } from 'utils/queryKeys'; +import { ButtonAppearanceTypes, LabelType } from 'enums/commonEnums'; +import { useTranslation } from 'react-i18next'; +import { formatDate } from 'utils/commonUtilts'; +import SyncStatusLabel from '../SyncStatusLabel'; +import DataGenerationStatusLabel from '../DataGenerationStatusLabel'; +import { useNavigate } from 'react-router-dom'; + +type DatasetCardProps = { + datasetId: number | string; + lastTrained?: Date | null | string; + isLatest?: boolean; + dataGenerationStatus?: string; + lastModelTrained?: string; + majorVersion?: string | number; + minorVersion?: string | number; +}; + +const DatasetCard: FC> = ({ + datasetId, + lastTrained, + dataGenerationStatus, + lastModelTrained, + majorVersion, + minorVersion +}) => { + + const { t } = useTranslation(); +const navigate = useNavigate(); + +const viewDataset = () => { + navigate(`/view-dataset?datasetId=${datasetId}`); + +}; + return ( +
    +
    +
    +
    {`V${majorVersion}.${minorVersion}`}
    +
    +
    +

    + {t('integratedAgencies.agencyCard.lastModelTrained')}:{' '} + {lastModelTrained === "" ? "N/A" : lastModelTrained} +

    +

    + {t('integratedAgencies.agencyCard.lastUsedForTraining')}:{' '} + {lastTrained === "1970-01-01T00:00:00.000+00:00" ? "N/A" : formatDate(lastTrained as Date, 'D.M.yy-H:m')} +

    +
    + +
    + + {/* {isLatest ? ( + + ) : null} */} +
    + +
    + +
    +
    +
    + ); +}; + +export default DatasetCard; diff --git a/GUI/src/components/molecules/IntegratedAgencyCard/DatasetGroupCard.scss b/GUI/src/components/molecules/IntegratedAgencyCard/DatasetGroupCard.scss new file mode 100644 index 00000000..a81ab511 --- /dev/null +++ b/GUI/src/components/molecules/IntegratedAgencyCard/DatasetGroupCard.scss @@ -0,0 +1,91 @@ +.row { + margin: 10px 0; + display: flex; + align-items: center; +} + +.switch-row { + justify-content: space-between; +} + +.status-indicators { + display: flex; + align-items: center; + gap: 10px; +} + +.dot { + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 8px; +} + +.green { + background-color: green; +} + +.grey { + background-color: grey; +} + +.icon-text-row { + flex-direction: column; + justify-content: center; + text-align: center; +} + +.icon-image { + width: 50px; + height: 50px; +} + +.icon-text { + margin: 5px 0 0 0; + font-size: 16px; +} + +.label-row { + justify-content: flex-end; + display: flex; + align-items: center; + margin-top: 15px; +} + +.left-label { + font-size: 14px; + color: #333; +} + +.status { + display: flex; + align-items: center; + font-size: 12px; + color: #555; + bottom: 10px; + left: 20px; +} + +.colored-label{ + background-color: white; + border: solid 2px #D73E3E; + border-radius: 6px; + color: #D73E3E; + width: fit-content; + padding: 0px 5px; + font-size: 14px; + margin-right: 5px; +} + +.text{ + font-size: 16px; + margin-bottom: 5px; +} + +.py-3{ + padding: 8px 0px; +} + +.flex{ + display: flex; +} \ No newline at end of file diff --git a/GUI/src/components/molecules/IntegratedAgencyCard/index.tsx b/GUI/src/components/molecules/IntegratedAgencyCard/index.tsx new file mode 100644 index 00000000..3f801fb6 --- /dev/null +++ b/GUI/src/components/molecules/IntegratedAgencyCard/index.tsx @@ -0,0 +1,165 @@ +import { FC, PropsWithChildren } from 'react'; +import './DatasetGroupCard.scss'; +import { Switch } from 'components/FormElements'; +import Button from 'components/Button'; +import Label from 'components/Label'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useDialog } from 'hooks/useDialog'; +import { Operation } from 'types/datasets'; +import { datasetQueryKeys, integratedAgenciesQueryKeys } from 'utils/queryKeys'; +import { ButtonAppearanceTypes, LabelType, ToastTypes } from 'enums/commonEnums'; +import { useTranslation } from 'react-i18next'; +import { formatDate } from 'utils/commonUtilts'; +import SyncStatusLabel from '../SyncStatusLabel'; +import { SyncStatus } from 'enums/datasetEnums'; +import { disableAgncy, enableAgncy, resync } from 'services/agencies'; +import { AxiosError } from 'axios'; +import { useToast } from 'hooks/useToast'; + +type IntegratedAgencyCardProps = { + agencyId: string; + agencyName?: string; + lastTrained?: Date | null | string; + isLatest?: boolean; + isEnabled?: boolean; + lastUpdated?: Date | null; + lastUsed?: Date | null; + validationStatus?: string; + syncStatus?: string; + lastModelTrained?: string; + lastSynced?: Date | null; + enableAllowed?: boolean; +}; + +const IntegratedAgencyCard: FC> = ({ + agencyId, + agencyName, + isLatest, + isEnabled, + lastUpdated, + lastTrained, + syncStatus, + lastModelTrained, + lastSynced, + enableAllowed +}) => { + + const { t } = useTranslation(); + + const queryClient = useQueryClient(); + const toast = useToast(); + + + const enableAgencyMutation = useMutation({ + mutationFn: ({ + id }: { + id: string | number + }) => enableAgncy(id as string), + onSuccess: async () => { + await queryClient.invalidateQueries( + integratedAgenciesQueryKeys.INTEGRATED_AGENCIES_LIST() + ); + }, + onError: (error: AxiosError) => { + toast.open({ + type: ToastTypes.ERROR, + title: t('global.notificationError'), + message: error?.message ?? '', + }); + }, + }); + + const disableAgencyMutation = useMutation({ + mutationFn: ({ + id, + }: { + id: string | number; + }) => disableAgncy(id as string), + onSuccess: async () => { + await queryClient.invalidateQueries( + integratedAgenciesQueryKeys.INTEGRATED_AGENCIES_LIST() + ); + }, + onError: (error: AxiosError) => { + toast.open({ + type: ToastTypes.ERROR, + title: t('global.notificationError'), + message: error?.message ?? '', + }); + }, + }); + + const resyncAgencyMutation = useMutation({ + mutationFn: ({ + id }: { + id: string | number + }) => resync(id as string), + onSuccess: async () => { + await queryClient.invalidateQueries( + integratedAgenciesQueryKeys.INTEGRATED_AGENCIES_LIST() + ); + } + // onError: (error: AxiosError) => { + // toast.open({ + // type: ToastTypes.ERROR, + // title: t('global.notificationError'), + // message: error?.message ?? '', + // }); + // }, + }); + + const handleCheckedChange = (checked: boolean) => { + if (checked) { + enableAgencyMutation.mutate({ id: agencyId }); + } else { + disableAgencyMutation.mutate({ id: agencyId }); + } + }; + + return ( +
    +
    +
    +
    {agencyName}
    + +
    +
    +

    + {t('integratedAgencies.agencyCard.lastModelTrained')}:{' '} + {lastModelTrained === "" ? "N/A" : lastModelTrained} +

    +

    + {t('integratedAgencies.agencyCard.lastUsedForTraining')}:{' '} + {lastTrained === "1970-01-01T00:00:00.000+00:00" ? "N/A" : formatDate(lastTrained as Date, 'D.M.yy-H:m')} +

    +

    + {t('integratedAgencies.agencyCard.lastSynced')}:{' '} + {lastSynced && formatDate(lastSynced, 'DD.MM.yy-HH:mm')} +

    +
    + + +
    + +
    + +
    + +
    +
    +
    + ); +}; + +export default IntegratedAgencyCard; diff --git a/GUI/src/components/molecules/NoDataView/NoDataView.scss b/GUI/src/components/molecules/NoDataView/NoDataView.scss new file mode 100644 index 00000000..e6bde393 --- /dev/null +++ b/GUI/src/components/molecules/NoDataView/NoDataView.scss @@ -0,0 +1,7 @@ +.p-5 { + padding: 5rem; +} + +.text-grey { + color: grey; +} diff --git a/GUI/src/components/molecules/NoDataView/index.tsx b/GUI/src/components/molecules/NoDataView/index.tsx new file mode 100644 index 00000000..c5f2bc8a --- /dev/null +++ b/GUI/src/components/molecules/NoDataView/index.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { MdDashboard } from 'react-icons/md'; +import './NoDataView.scss'; +interface NoDataViewProps { + text?: string; + description?: string; +} + +const NoDataView: React.FC = ({ text, description }) => { + return ( +
    + {} +
    + {text} +
    +
    +
    + {description} +
    +
    + ); +}; + +export default NoDataView; diff --git a/GUI/src/components/molecules/Pagination/Pagination.scss b/GUI/src/components/molecules/Pagination/Pagination.scss new file mode 100644 index 00000000..5c89eb88 --- /dev/null +++ b/GUI/src/components/molecules/Pagination/Pagination.scss @@ -0,0 +1,194 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/typography'; + +.data-table { + width: 100%; + color: get-color(black-coral-20); + text-align: left; + margin-bottom: 0; + display: table; + + &__scrollWrapper { + height: 100%; + overflow-x: auto; + white-space: nowrap; + display: block; + padding: 5px; + background-color: white; + border-radius: 10px; + border: solid 1px get-color(black-coral-1); + } + + thead, + tbody { + width: 100%; + } + + th { + padding: 12px 14.5px; + color: get-color(black-coral-12); + border-bottom: 1px solid get-color(black-coral-10); + font-weight: $veera-font-weight-beta; + vertical-align: middle; + position: relative; + } + + td { + padding: 12px 24px 12px 16px; + border-bottom: 1px solid get-color(black-coral-2); + vertical-align: middle; + max-width: fit-content; + + p { + white-space: break-spaces; + } + + .entity { + display: inline-flex; + align-items: center; + padding-left: 4px; + background-color: get-color(sapphire-blue-2); + border-radius: 4px; + + span { + display: inline-flex; + font-size: $veera-font-size-80; + background-color: get-color(white); + padding: 0 4px; + border-radius: 4px; + margin: 2px 2px 2px 4px; + } + } + } + + tbody { + tr { + &:last-child { + td { + border-bottom: 0; + } + } + } + } + + &__filter { + position: absolute; + top: 100%; + left: 0; + right: 0; + padding: get-spacing(paldiski); + background-color: get-color(white); + border-radius: 0 0 4px 4px; + border: 1px solid get-color(black-coral-2); + + input { + width: 100%; + display: block; + appearance: none; + background-color: get-color(white); + border: 1px solid get-color(black-coral-6); + border-radius: 5px; + color: var(--color-black); + font-size: $veera-font-size-100; + height: 32px; + line-height: 24px; + padding: get-spacing(paldiski); + + &::placeholder { + color: get-color(black-coral-6); + } + + &:focus { + outline: none; + border-color: get-color(sapphire-blue-10); + } + } + } + + &__pagination-wrapper { + display: flex; + padding: 6px 16px; + } + + &__pagination { + display: flex; + align-items: center; + gap: 15px; + margin: 0 auto; + + + .data-table__page-size { + margin-left: 0; + } + + .next, + .previous { + display: flex; + color: get-color(sapphire-blue-10); + + &[disabled] { + color: get-color(black-coral-11); + cursor: initial; + } + } + + .links { + display: flex; + align-items: center; + gap: 5px; + font-size: $veera-font-size-80; + color: get-color(black-coral-10); + + li { + display: block; + + a, + span { + display: flex; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + border-radius: 50%; + + &:hover { + text-decoration: none; + } + } + + &.active { + a, + span { + color: get-color(white); + background-color: get-color(sapphire-blue-10); + } + } + } + } + } + + &__page-size { + display: flex; + align-items: center; + gap: 8px; + font-size: $veera-font-size-80; + line-height: 16px; + color: get-color(black-coral-11); + margin-left: auto; + + select { + appearance: none; + font-size: $veera-font-size-70; + line-height: 16px; + height: 30px; + min-width: 50px; + padding: 6px 10px; + border: 1px solid #8f91a8; + border-radius: 2px; + background-color: get-color(white); + background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNiIgdmlld0JveD0iMCAwIDEwIDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNS4zMTMwNiA1LjgwODIyQzUuMTU2ODUgNS45NjQ0MyA0LjkwMzU4IDUuOTY0NDMgNC43NDczNyA1LjgwODIyTDAuMjgyNzMgMS4zNDM1OEMwLjEyNjUyIDEuMTg3MzcgMC4xMjY1MiAwLjkzNDEwMiAwLjI4MjczIDAuNzc3ODkzTDAuNzc3NzA0IDAuMjgyOTE4QzAuOTMzOTE0IDAuMTI2NzA4IDEuMTg3MTggMC4xMjY3MDggMS4zNDMzOSAwLjI4MjkxN0w1LjAzMDIyIDMuOTY5NzRMOC43MTcwNCAwLjI4MjkxN0M4Ljg3MzI1IDAuMTI2NzA4IDkuMTI2NTIgMC4xMjY3MDggOS4yODI3MyAwLjI4MjkxN0w5Ljc3NzcgMC43Nzc4OTJDOS45MzM5MSAwLjkzNDEwMiA5LjkzMzkxIDEuMTg3MzcgOS43Nzc3IDEuMzQzNThMNS4zMTMwNiA1LjgwODIyWiIgZmlsbD0iIzU1NTg2NyIvPgo8L3N2Zz4K'); + background-repeat: no-repeat; + background-position: top 11px right 10px; + } + } +} diff --git a/GUI/src/components/molecules/Pagination/index.tsx b/GUI/src/components/molecules/Pagination/index.tsx new file mode 100644 index 00000000..7c1c3b9d --- /dev/null +++ b/GUI/src/components/molecules/Pagination/index.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { MdOutlineWest, MdOutlineEast } from 'react-icons/md'; +import clsx from 'clsx'; +import { Link } from 'react-router-dom'; + +interface PaginationProps { + pageCount: number; + pageIndex: number; + canPreviousPage: boolean; + canNextPage: boolean; + onPageChange: (pageIndex: number) => void; + id?: string; +} + +const Pagination: React.FC = ({ + pageCount, + pageIndex, + canPreviousPage, + canNextPage, + onPageChange, + id, +}) => { + return ( +
    + {pageCount > 1 && ( +
    + + + +
    + )} +
    + ); +}; + +export default Pagination; diff --git a/GUI/src/components/molecules/ProgressBar/index.scss b/GUI/src/components/molecules/ProgressBar/index.scss new file mode 100644 index 00000000..bc4f3a53 --- /dev/null +++ b/GUI/src/components/molecules/ProgressBar/index.scss @@ -0,0 +1,28 @@ +.progress-bar-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + } + + .progress-bar-label { + margin-bottom: 4px; + font-size: 14px; + } + + .progress-bar-root { + position: relative; + overflow: hidden; + background-color: #e0e0e0; + border-radius: 4px; + width: 100%; + height: 10px; + } + + .progress-bar-indicator { + background-color: #07478d; + height: 100%; + transition: width 0.3s; + border-radius: 20px; + } + \ No newline at end of file diff --git a/GUI/src/components/molecules/ProgressBar/index.tsx b/GUI/src/components/molecules/ProgressBar/index.tsx new file mode 100644 index 00000000..69d6a444 --- /dev/null +++ b/GUI/src/components/molecules/ProgressBar/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import * as Progress from '@radix-ui/react-progress'; +import './index.scss'; + +type ProgressBarProps = { + value: number; + max: number; + label?: string; +}; + +const ProgressBar: React.FC = ({ value, max, label }) => { + return ( +
    + + + + {label && } + +
    + ); +}; + +export default ProgressBar; diff --git a/GUI/src/components/molecules/SyncStatusLabel/index.tsx b/GUI/src/components/molecules/SyncStatusLabel/index.tsx new file mode 100644 index 00000000..07d13746 --- /dev/null +++ b/GUI/src/components/molecules/SyncStatusLabel/index.tsx @@ -0,0 +1,56 @@ +import { SyncStatus } from 'enums/datasetEnums'; +import React from 'react'; +import Label from 'components/Label'; +import { LabelType } from 'enums/commonEnums'; +import { useTranslation } from 'react-i18next'; + +const SyncStatusLabel = ({ + status, +}: { + status: string | undefined; +}) => { + const { t } = useTranslation(); + + if (status === SyncStatus.SYNCED) { + return ( + + ); + } else if (status === SyncStatus.UNAVAILABLE ) { + return ( + + ); + }else if (status === SyncStatus.FAILED ) { + return ( + + ); + } + else if (status === SyncStatus.RESYNC_NEEDED) { + return ( + + ); + }else if (status === SyncStatus.IN_PROGRESS) { + return ( + + ); + }else if (status === SyncStatus.RESYNC_IN_PROGRESS) { + return ( + + ); + } else { + return null; + } +}; + +export default SyncStatusLabel; diff --git a/GUI/src/components/molecules/TableSkeleton/SkeletonTable.scss b/GUI/src/components/molecules/TableSkeleton/SkeletonTable.scss new file mode 100644 index 00000000..5f433ecf --- /dev/null +++ b/GUI/src/components/molecules/TableSkeleton/SkeletonTable.scss @@ -0,0 +1,31 @@ +.skeleton { + display: inline-block; + height: 1.5rem; + width: 100%; + background-color: #e0e0e0; + border-radius: 4px; + animation: pulse 1.5s infinite ease-in-out; + } + + @keyframes pulse { + 0% { + background-color: #e0e0e0; + } + 50% { + background-color: #f0f0f0; + } + 100% { + background-color: #e0e0e0; + } + } + + .table { + width: 100%; + border-collapse: collapse; + } + + .table th, + .table td { + padding: 0.75rem; + text-align: left; + } \ No newline at end of file diff --git a/GUI/src/components/molecules/TableSkeleton/TableSkeleton.tsx b/GUI/src/components/molecules/TableSkeleton/TableSkeleton.tsx new file mode 100644 index 00000000..b8a23d3f --- /dev/null +++ b/GUI/src/components/molecules/TableSkeleton/TableSkeleton.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import './SkeletonTable.scss'; + +interface SkeletonTableProps { + rowCount: number; +} + +const SkeletonTable: React.FC = ({ rowCount }) => { + const skeletonRows = Array.from({ length: rowCount }, (_, index) => ( + + +
    + + + )); + + return ( + + {skeletonRows} +
    + ); +}; + +export default SkeletonTable; \ No newline at end of file diff --git a/GUI/src/components/molecules/TrainingResults/TrainingResults.scss b/GUI/src/components/molecules/TrainingResults/TrainingResults.scss new file mode 100644 index 00000000..ad1bf4b1 --- /dev/null +++ b/GUI/src/components/molecules/TrainingResults/TrainingResults.scss @@ -0,0 +1,224 @@ +.results-wrapper { + padding: .5rem; + + .best-model-header { + color: #2e7d32; + margin-bottom: 1rem; + font-size: 1.25rem; + font-weight: 600; + } + + .best-model-summary { + display: flex; + gap: 2rem; + margin-bottom: 1.5rem; + padding: 1rem; + background-color: #f8fff8; + border: 1px solid #4caf50; + border-radius: 8px; + + .summary-metric { + display: flex; + flex-direction: column; + align-items: center; + + .metric-label { + font-size: 0.875rem; + color: #666; + margin-bottom: 0.25rem; + } + + .metric-value { + font-size: 1.125rem; + font-weight: 600; + color: #2e7d32; + } + } + } + + .section-title { + margin-bottom: 1rem; + color: #333; + font-size: 1.125rem; + } + + .model-section { + margin-bottom: 1.2rem; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 1rem 2rem; + background-color: #fff; + + &.best-model { + border-color: #4caf50; + background-color: #f8fff8; + } + + .model-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + + .model-name { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; + font-size: 1.125rem; + color: #333; + + .best-badge { + background-color: #4caf50; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + } + } + + .model-summary-stats { + display: flex; + gap: 1rem; + font-size: 0.875rem; + color: #666; + + span { + white-space: nowrap; + } + } + } + + .model-metrics-card { + border: 1px solid #ddd; + border-radius: 6px; + overflow: hidden; + margin-bottom: 1rem; + + .header-row { + display: flex; + background-color: #f5f5f5; + padding: 0.75rem; + font-weight: 600; + color: #333; + + .header-classes { + flex: 1; + } + + .header-metrics { + display: flex; + gap: 2rem; + min-width: 200px; + + div { + text-align: center; + flex: 1; + } + } + } + + .hr-divider { + margin: 0; + border: none; + border-top: 1px solid #ddd; + } + + .metric-row { + display: flex; + padding: 0.75rem; + border-bottom: 1px solid #f0f0f0; + + &:last-child { + border-bottom: none; + } + + &.average-row { + background-color: #f9f9f9; + font-weight: 600; + + .average-label { + color: #555; + } + + .metric-value.average { + color: #2e7d32; + } + } + + .metric-class { + flex: 1; + color: #333; + } + + .metric-values { + display: flex; + gap: 2rem; + min-width: 200px; + + .metric-value { + text-align: center; + flex: 1; + color: #666; + + &.f1 { + color: #1976d2; + } + + &.accuracy { + color: #388e3c; + } + } + } + } + } + + .model-details { + display: flex; + gap: 2rem; + flex-wrap: wrap; + + .detail-item { + display: flex; + gap: 0.5rem; + + .detail-label { + font-weight: 500; + color: #666; + } + + .detail-value { + color: #333; + } + } + } + } +} + +@media (max-width: 768px) { + .results-wrapper { + .best-model-summary { + flex-direction: column; + gap: 1rem; + } + + .model-section { + .model-header { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + + .model-summary-stats { + flex-direction: column; + gap: 0.25rem; + } + } + + .model-details { + flex-direction: column; + gap: 0.5rem; + } + } + } +} \ No newline at end of file diff --git a/GUI/src/components/molecules/TrainingResults/index.tsx b/GUI/src/components/molecules/TrainingResults/index.tsx new file mode 100644 index 00000000..e5c32849 --- /dev/null +++ b/GUI/src/components/molecules/TrainingResults/index.tsx @@ -0,0 +1,123 @@ +import { ModelResultsProps } from 'types/dataModels'; +import './TrainingResults.scss'; + +interface TrainingResultModel { + avg_f1: number; + avg_accuracy: number; + combined_score: number; + metrics: [string[], number[], number[]]; // [classes, accuracies, f1_scores] + variant: { + name: string; + type: string; + base_model: string; + ood_method: string; + full_model_name: string; + confidence_scaling: boolean; + uncertainty_strategy: string; + human_handoff_threshold: number; + }; + model_path: string; +} + +const ModelResults: React.FC<{ models: TrainingResultModel[] }> = ({ models }) => { + console.log(models); + + // Find best performing model by avg_f1 + const bestModel = models?.reduce((best, current) => + current.avg_f1 > best.avg_f1 ? current : best + ); + + // Sort models by avg_f1 in descending order + const sortedModels = models?.sort((a, b) => b.avg_f1 - a.avg_f1) || []; + + // Check if there are multiple models + const hasMultipleModels = models && models.length > 1; + + const formatScore = (score: number) => (score * 100).toFixed(2) + '%'; + + const processModelMetrics = (model: TrainingResultModel) => { + const [classes, accuracies, f1Scores] = model.metrics; + + return classes.map((className, index) => ({ + className: className.replace(/_/g, ' '), + accuracy: accuracies[index], + f1: f1Scores[index] + })); + }; + + if (!models || models.length === 0) { + return ( +
    +

    No training results available

    +
    + ); + } + + return ( +
    + {hasMultipleModels && ( + <> +

    + Best Performing Model - {bestModel?.variant?.name || "N/A"} +

    +
    +
    + Average F1: + {formatScore(bestModel?.avg_f1 || 0)} +
    +
    + Average Accuracy: + {formatScore(bestModel?.avg_accuracy || 0)} +
    +
    + Combined Score: + {formatScore(bestModel?.combined_score || 0)} +
    +
    + + )} + +

    Training Results

    + + {sortedModels.map((model, idx) => { + const processedMetrics = processModelMetrics(model); + const isBest = hasMultipleModels && model === bestModel; + + return ( +
    +
    +
    + {model.variant.name} + {/* Only show best badge when there are multiple models */} + {hasMultipleModels && isBest && Best} +
    +
    + +
    +
    +
    Classes
    +
    +
    F1 Score
    +
    Accuracy
    +
    +
    +
    + + {processedMetrics.map((metric, metricIdx) => ( +
    +
    {metric.className}
    +
    +
    {formatScore(metric.f1)}
    +
    {formatScore(metric.accuracy)}
    +
    +
    + ))} +
    +
    + ); + })} +
    + ); +}; + +export default ModelResults; \ No newline at end of file diff --git a/GUI/src/components/molecules/TrainingSessionCard/index.tsx b/GUI/src/components/molecules/TrainingSessionCard/index.tsx new file mode 100644 index 00000000..ff9467a1 --- /dev/null +++ b/GUI/src/components/molecules/TrainingSessionCard/index.tsx @@ -0,0 +1,67 @@ +import { useTranslation } from 'react-i18next'; +import ProgressBar from 'components/ProgressBar'; +import { Card, Label } from 'components'; +import { TrainingSessionsStatuses } from 'enums/dataModelsEnums'; + +type TrainingSessionCardProps = { + modelName: string; + isLatest: boolean; + version: string; + status?: string; + trainingMessage?: string; + progress: number; + platform?: string; + maturity?: string; +}; + +const TrainingSessionCard: React.FC = ({ + modelName, + version, + isLatest, + status, + trainingMessage, + progress, + maturity, + platform, +}) => { + const { t } = useTranslation(); + + return ( + +
    +
    {modelName}
    + {isLatest && } + + {platform && }{' '} + {maturity && } + {status === TrainingSessionsStatuses.TRAINING_FAILED_STATUS && } +
    + + } + > +
    + {(status===TrainingSessionsStatuses.TRAINING_FAILED_STATUS || status===TrainingSessionsStatuses.TRAINING_SUCCESS_STATUS) && progress===100 ? ( +
    + {trainingMessage} +
    + ) : ( +
    +
    {status}
    + +
    + {trainingMessage} +
    +
    + )} +
    +
    + ); +}; + +export default TrainingSessionCard; diff --git a/GUI/src/components/molecules/UserManagementActionButtons/UserManagementActionButtons.tsx b/GUI/src/components/molecules/UserManagementActionButtons/UserManagementActionButtons.tsx new file mode 100644 index 00000000..4af03ccf --- /dev/null +++ b/GUI/src/components/molecules/UserManagementActionButtons/UserManagementActionButtons.tsx @@ -0,0 +1,91 @@ +import { FC } from 'react'; +import Button from 'components/Button'; +import Icon from 'components/Icon'; +import { useTranslation } from 'react-i18next'; +import { MdOutlineDeleteOutline, MdOutlineEdit } from 'react-icons/md'; +import { User } from 'types/user'; +import { ButtonAppearanceTypes, ToastTypes } from 'enums/commonEnums'; +import { useDialog } from 'hooks/useDialog'; +import { deleteUser } from 'services/users'; +import { userManagementQueryKeys } from 'utils/queryKeys'; +import { useToast } from 'hooks/useToast'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; + +const ActionButtons: FC<{ + row: User; + setEditableRow: React.Dispatch>; +}> = ({ row, setEditableRow }) => { + const { t } = useTranslation(); + const { open, close } = useDialog(); + const toast = useToast(); + const queryClient = useQueryClient(); + + const deleteUserMutation = useMutation({ + mutationFn: ({ id }: { id: string | number }) => deleteUser(id), + onSuccess: async () => { + close(); + await queryClient.invalidateQueries( + userManagementQueryKeys.getAllEmployees() + ); + toast.open({ + type: ToastTypes.SUCCESS, + title: t('global.notification'), + message: t('toast.success.userDeleted'), + }); + }, + onError: (error: AxiosError) => { + toast.open({ + type: ToastTypes.ERROR, + title: t('global.notificationError'), + message: error?.message ?? '', + }); + }, + }); + + return ( +
    + + + +
    + ), + }); + }} + > + } /> + {t('global.delete')} + + + ); +}; + +export default ActionButtons; diff --git a/GUI/src/config/dataModelsConfig.ts b/GUI/src/config/dataModelsConfig.ts new file mode 100644 index 00000000..e8c2c612 --- /dev/null +++ b/GUI/src/config/dataModelsConfig.ts @@ -0,0 +1,14 @@ +export const modelStatuses = + [ + {label: 'Active', value: 'active'}, + {label: 'Deprecated', value: 'deprecated'}, + ] + +export const trainingStatuses = + [ + {label: 'Trained', value: 'trained'}, + {label: 'Not Trained', value: 'not_trained'}, + {label: 'Training in Progress', value: 'training_in_progress'}, + {label: 'Retraining Needed', value: 'retraining_needed'}, + {label: 'Training Failed', value: 'training_failed'} + ] diff --git a/GUI/src/config/rolesConfig.json b/GUI/src/config/rolesConfig.json new file mode 100644 index 00000000..02b429cd --- /dev/null +++ b/GUI/src/config/rolesConfig.json @@ -0,0 +1,4 @@ +[ + { "label": "ROLE_ADMINISTRATOR", "value": "ROLE_ADMINISTRATOR" }, + { "label": "ROLE_MODEL_TRAINER", "value": "ROLE_MODEL_TRAINER" } +] diff --git a/GUI/src/constants/config.ts b/GUI/src/constants/config.ts new file mode 100644 index 00000000..5c0855f4 --- /dev/null +++ b/GUI/src/constants/config.ts @@ -0,0 +1,5 @@ +export const EMERGENCY_NOTICE_LENGTH = 250; +export const WELCOME_MESSAGE_LENGTH = 250; +export const USER_IDLE_STATUS_TIMEOUT = 300000; // milliseconds +export const CHAT_INPUT_LENGTH = 500; +export const CHAT_HISTORY_PREFERENCES_KEY = 'chat-history-preferences'; diff --git a/GUI/src/constants/menuIcons.tsx b/GUI/src/constants/menuIcons.tsx new file mode 100644 index 00000000..a53fc7c9 --- /dev/null +++ b/GUI/src/constants/menuIcons.tsx @@ -0,0 +1,24 @@ +import { MdOutlineForum, MdOutlineAdb, MdOutlineEqualizer, MdSettings, MdOutlineMonitorWeight } from 'react-icons/md'; + +export const menuIcons = [ + { + id: 'userManagement', + icon: , + }, + { + id: 'training', + icon: , + }, + { + id: 'analytics', + icon: , + }, + { + id: 'settings', + icon: , + }, + { + id: 'monitoring', + icon: , + }, +]; diff --git a/GUI/src/context/DialogContext.tsx b/GUI/src/context/DialogContext.tsx new file mode 100644 index 00000000..f2b75c44 --- /dev/null +++ b/GUI/src/context/DialogContext.tsx @@ -0,0 +1,83 @@ +import React, { + createContext, + FC, + PropsWithChildren, + ReactNode, + useMemo, + useState, +} from 'react'; +import * as RadixDialog from '@radix-ui/react-dialog'; +import { MdOutlineClose } from 'react-icons/md'; +import clsx from 'clsx'; +import '../components/Dialog/Dialog.scss'; +import Icon from 'components/Icon'; +import Track from 'components/Track'; + +type DialogProps = { + title?: string | null; + footer?: ReactNode; + size?: 'default' | 'large'; + content: ReactNode; +}; + +type DialogContextType = { + open: (dialog: DialogProps) => void; + close: () => void; +}; +// operates Dialog modals where dynamic contents not involved +export const DialogContext = createContext(null!); + +export const DialogProvider: FC> = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + const [dialogProps, setDialogProps] = useState(null); + + const open = (dialog: DialogProps) => { + setDialogProps(dialog); + setIsOpen(true); + }; + + const close = () => { + setIsOpen(false); + setDialogProps(null); + }; + + const contextValue = useMemo(() => ({ open, close }), []); + + return ( + + {children} + {dialogProps && ( + + + + + {dialogProps.title && ( +
    + + {dialogProps.title} + + + + +
    + )} +
    {dialogProps.content}
    + {dialogProps.footer && ( + + {dialogProps.footer} + + )} +
    +
    +
    + )} +
    + ); +}; diff --git a/GUI/src/context/ToastContext.tsx b/GUI/src/context/ToastContext.tsx new file mode 100644 index 00000000..5c07ef4a --- /dev/null +++ b/GUI/src/context/ToastContext.tsx @@ -0,0 +1,58 @@ +import { + createContext, + FC, + PropsWithChildren, + ReactNode, + useMemo, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import * as RadixToast from '@radix-ui/react-toast'; + +import { Toast } from 'components'; +import { generateUEID } from 'utils/generateUEID'; + +export type ToastType = { + type: 'info' | 'success' | 'error' | 'warning'; + title: string; + message: ReactNode; +}; + +type ToastTypeWithId = ToastType & { id: string }; + +type ToastContextType = { + open: (toast: ToastType) => void; +}; + +export const ToastContext = createContext(null!); + +export const ToastProvider: FC = ({ children }) => { + const { t } = useTranslation(); + const [toasts, setToasts] = useState([]); + const open = (content: ToastType) => { + setToasts((prevState) => [ + ...prevState, + { id: generateUEID(), ...content }, + ]); + }; + const close = (id: string) => { + setToasts((prevState) => prevState.filter((toast) => toast.id === id)); + }; + + const contextValue = useMemo(() => ({ open }), []); + + return ( + + + {children} + {toasts.map((toast) => ( + close(toast.id)} /> + ))} + + + + ); +}; diff --git a/GUI/src/data/sampleDataset.ts b/GUI/src/data/sampleDataset.ts new file mode 100644 index 00000000..ba5d4cce --- /dev/null +++ b/GUI/src/data/sampleDataset.ts @@ -0,0 +1,19 @@ +export const sampleDatasetRows = { + fields:["id", "question", "clientName"], + dataPayload:[ + { id: 1, question: "How do I renew my passport?", clientName: "Tax Department", clientId: "12" }, + { id: 2, question: "What are the tax filing deadlines?", clientName: "Tax Department",clientId: "12" }, + { id: 3, question: "How can I apply for unemployment benefits?", clientName: "Tax Department",clientId: "12" }, + { id: 4, question: "Where can I get my birth certificate?", clientName: "Tax Department",clientId: "12" }, + { id: 5, question: "How do I register a new business?", clientName: "Tax Department",clientId: "12" }, + { id: 6, question: "What documents are needed for a driver's license?", clientName: "ID Department",clientId: "13" }, + { id: 7, question: "How do I pay property taxes?", clientName: "ID Department",clientId: "13" }, + { id: 8, question: "How can I access public healthcare?", clientName: "ID Departmenty",clientId: "13" }, + { id: 9, question: "How do I enroll my child in school?", clientName: "ID Department",clientId: "13" }, + { id: 10, question: "What is the process for marriage registration?", clientName: "ID Department",clientId: "13" }, + // { id: 11, question: "How do I report a lost ID card?", clientName: "ID Department" }, + // { id: 12, question: "How can I check my pension status?", clientName: "Pension Fund" }, + // { id: 13, question: "Where do I apply for building permits?", clientName: "Urban Planning Authority" }, + // { id: 14, question: "How do I get a copy of my criminal record?", clientName: "Police Department" }, + // { id: 15, question: "How do I request public information?", clientName: "Information Commission" }, +]}; \ No newline at end of file diff --git a/GUI/src/enums/commonEnums.ts b/GUI/src/enums/commonEnums.ts new file mode 100644 index 00000000..79f9444f --- /dev/null +++ b/GUI/src/enums/commonEnums.ts @@ -0,0 +1,18 @@ +export enum ToastTypes { + SUCCESS = 'success', + ERROR = 'error', +} + +export enum ButtonAppearanceTypes { + PRIMARY = 'primary', + SECONDARY = 'secondary', + ERROR = 'error', + TEXT = 'text', +} + +export enum LabelType { + SUCCESS = 'success', + ERROR = 'error', + INFO = 'info', + WARNING = 'warning', +} diff --git a/GUI/src/enums/correctedTextsEnums.ts b/GUI/src/enums/correctedTextsEnums.ts new file mode 100644 index 00000000..9ed55948 --- /dev/null +++ b/GUI/src/enums/correctedTextsEnums.ts @@ -0,0 +1,5 @@ +export enum CorrectedTextsModalContexts { + EXPORT = 'export', + SUCCESS = 'success', + ERROR = 'error' + } \ No newline at end of file diff --git a/GUI/src/enums/dataModelsEnums.ts b/GUI/src/enums/dataModelsEnums.ts new file mode 100644 index 00000000..e059554e --- /dev/null +++ b/GUI/src/enums/dataModelsEnums.ts @@ -0,0 +1,31 @@ +export enum TrainingStatus { + NOT_TRAINED = 'not_trained', + TRAINING_INPROGRESS = 'training_in_progress', + INITIATING_TRAINING = 'initiating_training', + TRAINED = 'trained', + RETRAINING_NEEDED = 'retraining_needed', + FAILED = 'training_failed', +} + +export enum Maturity { + PRODUCTION = 'production', + UNDEPLOYED = 'undeployed', + TESTING = 'testing', +} + +export enum Platform { + JIRA = 'jira', + OUTLOOK = 'outlook', + UNDEPLOYED = 'undeployed', +} + +export enum UpdateType { + MAJOR = 'major', + MINOR = 'minor', + MATURITY_LABEL = 'maturityLabel', +} + +export enum TrainingSessionsStatuses { + TRAINING_SUCCESS_STATUS = 'Model Trained And Deployed', + TRAINING_FAILED_STATUS = 'Training Failed' +} \ No newline at end of file diff --git a/GUI/src/enums/datasetEnums.ts b/GUI/src/enums/datasetEnums.ts new file mode 100644 index 00000000..039bff19 --- /dev/null +++ b/GUI/src/enums/datasetEnums.ts @@ -0,0 +1,64 @@ +export enum SyncStatus { + SYNCED = 'Synced_with_CKB', + UNAVAILABLE = 'Unavailable_in_CKB', + RESYNC_NEEDED = 'Resync_needed_with_CKB', + IN_PROGRESS ='Sync_in_progress_with_CKB', + RESYNC_IN_PROGRESS ='Resync_in_progress_with_CKB', + FAILED='Sync_with_CKB_Failed' +} + +export enum DataGenerationStatus { + IN_PROGRESS = 'Generation_in_Progress', + FAILED = 'Generation_Failed', + SUCCESS = 'Generation_Success', +} + +export enum DatasetViewEnum { + LIST = 'list', + INDIVIDUAL = 'individual', +} + +export enum CreateDatasetGroupModals { + SUCCESS = 'SUCCESS', + VALIDATION_ERROR = 'VALIDATION_ERROR', + NULL = 'NULL', +} + +export enum ViewDatasetGroupModalContexts { + EXPORT_MODAL = 'EXPORT_MODAL', + IMPORT_MODAL = 'IMPORT_MODAL', + PATCH_UPDATE_MODAL = 'PATCH_UPDATE_MODAL', + DELETE_ROW_MODAL = 'DELETE_ROW_MODAL', + CONFIRMATION_MODAL = 'CONFIRMATION_MODAL', + NULL = 'NULL', +} + +export enum UpdatePriority { + MAJOR = 'MAJOR', + MINOR = 'MINOR', + PATCH = 'PATCH', + NULL = 'NULL', +} + +export enum ImportExportDataTypes { + XLSX = 'xlsx', + JSON = 'json', + YAML = 'yaml', +} + +export enum StopWordImportOptions { + ADD = 'add', + DELETE = 'delete', +} + +export enum ValidationErrorTypes { + NAME = 'NAME', + CLASS_HIERARCHY = 'CLASS_HIERARCHY', + VALIDATION_CRITERIA = 'VALIDATION_CRITERIA', + NULL = 'NULL', +} + +export enum DataGenerationSessionsStatuses { + VALIDATION_SUCCESS_STATUS = 'Success', + VALIDATION_FAILED_STATUS = 'Fail' +} \ No newline at end of file diff --git a/GUI/src/enums/roles.ts b/GUI/src/enums/roles.ts new file mode 100644 index 00000000..b5cfd8a6 --- /dev/null +++ b/GUI/src/enums/roles.ts @@ -0,0 +1,4 @@ +export enum ROLES { + ROLE_ADMINISTRATOR = 'ROLE_ADMINISTRATOR', + ROLE_MODEL_TRAINER = 'ROLE_MODEL_TRAINER', +} diff --git a/GUI/src/hoc/with-authorization.tsx b/GUI/src/hoc/with-authorization.tsx new file mode 100644 index 00000000..9874ffa9 --- /dev/null +++ b/GUI/src/hoc/with-authorization.tsx @@ -0,0 +1,29 @@ +import { ROLES } from 'enums/roles'; +import React from 'react'; +import useStore from 'store'; + +function withAuthorization

    ( + WrappedComponent: React.ComponentType

    , + allowedRoles: ROLES[] = [] +): React.FC

    { + const CheckRoles: React.FC

    = ({ ...props }: P) => { + const userInfo = useStore((x) => x.userInfo); + const allowed = allowedRoles?.some((x) => + userInfo?.authorities.includes(x) + ); + + if (!userInfo) { + return Loading...; + } + + if (!allowed) { + return Unauthorized Access; + } + + return ; + }; + + return CheckRoles; +} + +export default withAuthorization; diff --git a/GUI/src/hooks/useDataGenerationSessions.tsx b/GUI/src/hooks/useDataGenerationSessions.tsx new file mode 100644 index 00000000..6a6dff52 --- /dev/null +++ b/GUI/src/hooks/useDataGenerationSessions.tsx @@ -0,0 +1,53 @@ +import { useQuery } from '@tanstack/react-query'; +import { DataGenerationSessionsStatuses } from 'enums/datasetEnums'; +import { useEffect, useState } from 'react'; +import { getDataGenerationProgress } from 'services/datasets'; +import sse from 'services/sse-service'; +import { SSEEventData, ValidationProgressData } from 'types/datasets'; +import { datasetQueryKeys } from 'utils/queryKeys'; + +export const useDataGenerationSessions = () => { + const [progresses, setProgresses] = useState([]); + + const { data: progressData, refetch } = useQuery( + datasetQueryKeys.GET_DATASET_GROUP_PROGRESS(), + getDataGenerationProgress, + { + onSuccess: (data) => setProgresses(data), + } + ); + + const handleUpdate = (sessionId: string, newData: SSEEventData) => { + setProgresses((prevProgresses) => + prevProgresses.map((progress) => + progress.id === sessionId ? { ...progress, ...newData } : progress + ) + ); + }; + + useEffect(() => { + if (!progressData) return; + + const eventSources = progressData + .filter( + (progress) => + !( + progress.generationStatus === + DataGenerationSessionsStatuses.VALIDATION_SUCCESS_STATUS || + progress.generationStatus === + DataGenerationSessionsStatuses.VALIDATION_FAILED_STATUS + ) && progress.progressPercentage !== 100 + ) + .map((progress) => + sse(`/${progress.id}`, 'dataset', (data: SSEEventData) => { + handleUpdate(data.sessionId, data); + }) + ); + + return () => { + eventSources.forEach((eventSource) => eventSource?.close()); + }; + }, [progressData, refetch]); + + return progresses; +}; diff --git a/GUI/src/hooks/useDialog.tsx b/GUI/src/hooks/useDialog.tsx new file mode 100644 index 00000000..c38ed60a --- /dev/null +++ b/GUI/src/hooks/useDialog.tsx @@ -0,0 +1,4 @@ +import { DialogContext } from 'context/DialogContext'; +import { useContext } from 'react'; + +export const useDialog = () => useContext(DialogContext); diff --git a/GUI/src/hooks/useDocumentEscapeListener.tsx b/GUI/src/hooks/useDocumentEscapeListener.tsx new file mode 100644 index 00000000..8f7b3b6b --- /dev/null +++ b/GUI/src/hooks/useDocumentEscapeListener.tsx @@ -0,0 +1,17 @@ +import { useLayoutEffect } from 'react'; + +const useDocumentEscapeListener = (callback: () => void) => { + useLayoutEffect(() => { + const handleKeyUp = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + callback(); + } + }; + + document.addEventListener('keyup', handleKeyUp); + + return () => document.removeEventListener('keyup', handleKeyUp); + }, [callback]); +}; + +export default useDocumentEscapeListener; diff --git a/GUI/src/hooks/useOptionLists.tsx b/GUI/src/hooks/useOptionLists.tsx new file mode 100644 index 00000000..f860f166 --- /dev/null +++ b/GUI/src/hooks/useOptionLists.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from 'react-i18next'; + +// maps translations with dropdown options +const useOptionLists = () => { + const { t } = useTranslation(); + + const dataTypesConfigs = [ + { label: t('optionLists.text'), value: 'text' }, + { label: t('optionLists.numbers'), value: 'numbers' }, + { label: t('optionLists.dateTimes'), value: 'datetime' }, + { label: t('optionLists.email'), value: 'email' }, + { label: t('optionLists.fileAttachements'), value: 'file_attachments' }, + ]; + + const importOptionsConfigs = [ + { label: t('optionLists.importToAdd'), value: 'add' }, + { label: t('optionLists.importToDelete'), value: 'delete' }, + ]; + + return { + dataTypesConfigs, + importOptionsConfigs, + }; +}; + +export default useOptionLists; diff --git a/GUI/src/hooks/useToast.tsx b/GUI/src/hooks/useToast.tsx new file mode 100644 index 00000000..51715549 --- /dev/null +++ b/GUI/src/hooks/useToast.tsx @@ -0,0 +1,5 @@ +import { useContext } from 'react'; + +import { ToastContext } from 'context/ToastContext'; + +export const useToast = () => useContext(ToastContext); diff --git a/GUI/src/hooks/useTrainingSessions.tsx b/GUI/src/hooks/useTrainingSessions.tsx new file mode 100644 index 00000000..abea65de --- /dev/null +++ b/GUI/src/hooks/useTrainingSessions.tsx @@ -0,0 +1,56 @@ +import { useQuery } from '@tanstack/react-query'; +import { TrainingSessionsStatuses } from 'enums/dataModelsEnums'; +import { useEffect, useState } from 'react'; +import { getDataModelsProgress } from 'services/datamodels'; +import sse from 'services/sse-service'; +import { TrainingProgressData } from 'types/dataModels'; +import { SSEEventData } from 'types/datasets'; +import { dataModelsQueryKeys } from 'utils/queryKeys'; + +export const useTrainingSessions = () => { + const [progresses, setProgresses] = useState([]); + + const { data: progressData, refetch } = useQuery( + dataModelsQueryKeys.GET_DATA_MODELS_PROGRESS(), + getDataModelsProgress, + { + onSuccess: (data) => { + setProgresses(data); + }, + } + ); + + const handleUpdate = (sessionId: string, newData: SSEEventData) => { + setProgresses((prevProgresses) => + prevProgresses.map((progress) => + progress.id === sessionId ? { ...progress, ...newData } : progress + ) + ); + }; + + useEffect(() => { + if (!progressData) return; + + const eventSources = progressData + .filter( + (progress) => + !( + progress.trainingStatus === + TrainingSessionsStatuses.TRAINING_SUCCESS_STATUS || + progress.trainingStatus === + TrainingSessionsStatuses.TRAINING_FAILED_STATUS + ) && progress.progressPercentage !== 100 + ) + .map((progress) => + sse(`/${progress.id}`, 'model', (data: SSEEventData) => { + handleUpdate(data.sessionId, data); + }) + ); + + return () => { + eventSources.forEach((eventSource) => eventSource?.close()); + }; + }, [progressData, refetch]); + + return progresses; +}; diff --git a/GUI/src/main.tsx b/GUI/src/main.tsx new file mode 100644 index 00000000..a44091f9 --- /dev/null +++ b/GUI/src/main.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import { + QueryClient, + QueryClientProvider, + QueryFunction, +} from '@tanstack/react-query'; + +import App from './App'; +import api from 'services/api'; +import apiDev from 'services/api-dev'; +import { ToastProvider } from 'context/ToastContext'; +import 'styles/main.scss'; +import '../i18n'; +import { CookiesProvider } from 'react-cookie'; +import { DialogProvider } from 'context/DialogContext'; + +const defaultQueryFn: QueryFunction | undefined = async ({ queryKey }) => { + if (queryKey.includes('prod')) { + const { data } = await apiDev.get(queryKey[0] as string); + return data; + } + + const { data } = await api.get(queryKey[0] as string); + return data; +}; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + queryFn: defaultQueryFn, + }, + }, +}); + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + + + + + + + +); diff --git a/GUI/src/model/ruuter-response-model.ts b/GUI/src/model/ruuter-response-model.ts new file mode 100644 index 00000000..07cafc1c --- /dev/null +++ b/GUI/src/model/ruuter-response-model.ts @@ -0,0 +1,11 @@ +export interface RuuterResponse { + data: Record | null; + error: string | null; +} + +export interface CustomJwtExtendResponse { + data: { + custom_jwt_extend: string; + }; + error: null; +} diff --git a/GUI/src/pages/DataGenerationSessions/index.tsx b/GUI/src/pages/DataGenerationSessions/index.tsx new file mode 100644 index 00000000..082e037b --- /dev/null +++ b/GUI/src/pages/DataGenerationSessions/index.tsx @@ -0,0 +1,36 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import NoDataView from 'components/molecules/NoDataView'; +import { useDataGenerationSessions } from 'hooks/useDataGenerationSessions'; +import DataGenerationSessionCard from 'components/molecules/DataGenerationSessionCard'; + +const DataGenerationSessions: FC = () => { + const { t } = useTranslation(); + const progresses = useDataGenerationSessions(); + + return ( +

    +
    +
    +
    {t('validationSessions.title')}
    +
    + {progresses?.length === 0 && ( + + )} + {progresses?.map((session) => ( + + ))} +
    +
    + ); +}; + +export default DataGenerationSessions; diff --git a/GUI/src/pages/DataModels/ConfigureDataModel.tsx b/GUI/src/pages/DataModels/ConfigureDataModel.tsx new file mode 100644 index 00000000..c6d0e34a --- /dev/null +++ b/GUI/src/pages/DataModels/ConfigureDataModel.tsx @@ -0,0 +1,375 @@ +import { FC, useEffect, useRef, useState } from 'react'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import { Button, Card, Dialog } from 'components'; +import { useDialog } from 'hooks/useDialog'; +import BackArrowButton from 'assets/BackArrowButton'; + +import DataModelForm from 'components/molecules/DataModelForm'; +import { getChangedAttributes } from 'utils/dataModelsUtils'; +import { Maturity, UpdateType } from 'enums/dataModelsEnums'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; +import { DataModel, UpdatedDataModelPayload } from 'types/dataModels'; +import { dataModelsQueryKeys } from 'utils/queryKeys'; +import { useTranslation } from 'react-i18next'; +import './DataModels.scss'; +import { configureDataModel, deleteDataModel, deployDataModel, getDataModelMetadata, getProductionDataModel } from 'services/datamodels'; +import { use } from 'i18next'; +import { set } from 'date-fns'; +import { areArraysEqual } from 'utils/commonUtilts'; + +const ConfigureDataModel: FC = () => { + const { t } = useTranslation(); + const { open, close } = useDialog(); + const navigate = useNavigate(); + const [modalOpen, setModalOpen] = useState(false); + const [modalType, setModalType] = useState(''); + const [modalTitle, setModalTitle] = useState(''); + const [modalDiscription, setModalDiscription] = useState(''); + const modalFunciton = useRef(() => { }); + const [searchParams] = useSearchParams(); + const modelId = searchParams.get('datamodelId'); + + const { data: modelMetadata } = useQuery({ + queryKey: dataModelsQueryKeys.GET_META_DATA(modelId ?? ''), + queryFn: () => getDataModelMetadata(modelId ?? ''), + }); + + const { data: prodDataModel, isLoading: isProdDataModelLoading } = useQuery({ + queryKey: dataModelsQueryKeys.GET_PROD_DATA_MODEL(), + queryFn: () => getProductionDataModel(), + }); + + const [initialData, setInitialData] = useState>({ + modelName: modelMetadata?.modelName, + datasetId: modelMetadata?.connectedDsId, + baseModels: modelMetadata?.baseModels, + deploymentEnvironment: modelMetadata?.deploymentEnv, + version: `V${modelMetadata?.major}.${modelMetadata?.minor}`, + }); + + const [dataModel, setDataModel] = useState({ + modelId: modelMetadata?.modelId, + modelName: modelMetadata?.modelName, + datasetId: modelMetadata?.connectedDsId.toString(), + baseModels: modelMetadata ? JSON.parse(modelMetadata?.baseModels.value) : [], + deploymentEnvironment: modelMetadata?.deploymentEnv, + version: `V${modelMetadata?.major}.${modelMetadata?.minor}`, + trainingResults: modelMetadata?.trainingResults, + }); + + useEffect(() => { + setInitialData({ + modelId: modelMetadata?.modelId, + modelName: modelMetadata?.modelName, + datasetId: modelMetadata?.connectedDsId.toString(), + baseModels: modelMetadata ? JSON.parse(modelMetadata?.baseModels.value) : [], + deploymentEnvironment: modelMetadata?.deploymentEnv, + version: `V${modelMetadata?.major}.${modelMetadata?.minor}`, + }); + setDataModel({ + modelId: modelMetadata?.modelId, + modelName: modelMetadata?.modelName, + datasetId: modelMetadata?.connectedDsId.toString(), + baseModels: modelMetadata ? JSON.parse(modelMetadata?.baseModels.value) : [], + deploymentEnvironment: modelMetadata?.deploymentEnv, + version: `V${modelMetadata?.major}.${modelMetadata?.minor}`, + trainingResults: modelMetadata?.trainingResults, + + }); + }, [modelMetadata]); + + + const handleDataModelAttributesChange = ( + name: keyof DataModel, + value: any + ) => { + setDataModel((prevDataModel) => ({ + ...prevDataModel, + [name]: value, + })); + }; + + const updateMutation = useMutation({ + mutationFn: configureDataModel, + onSuccess: () => { + open({ + title: t('dataModels.configureDataModel.saveChangesTitile'), + content: t('dataModels.configureDataModel.saveChangesDesc'), + footer: (
    ) + }); + + }, + onError: () => { + open({ + title: t('dataModels.configureDataModel.updateErrorTitile'), + content: t('dataModels.configureDataModel.updateErrorDesc'), + }); + }, + }); + + const deployMutation = useMutation({ + mutationFn: deployDataModel, + onSuccess: () => { + open({ + title: t('dataModels.configureDataModel.deployDataModalSuccessTitle'), + content: t('dataModels.configureDataModel.deployDataModalSuccessDesc'), + footer: (
    ) + }); + + }, + onError: () => { + open({ + title: t('dataModels.configureDataModel.deployDataModalErrorTitle'), + content: t('dataModels.configureDataModel.deployDataModalErrorDesc'), + }); + }, + }); + + const handleSaveChanges = () => { + const payload = getChangedAttributes(initialData, dataModel); + + const updateType = getUpdateType(payload); + + const updatedPayload = buildUpdatedPayload(updateType); + + const isDeploymentChanged = payload.deploymentEnvironment; + + const isChangingToProduction = dataModel.deploymentEnvironment === "production" && prodDataModel; + + if (isChangingToProduction) { + // Always show replace modal when changing to production + if (!updateType && isDeploymentChanged) { + const deployPayload = { + modelId: modelMetadata.modelId ?? "", + currentEnv: initialData.deploymentEnvironment ?? "", + targetEnv: dataModel.deploymentEnvironment ?? "" + }; + openModal( + t('dataModels.createDataModel.replaceDesc'), + t('dataModels.createDataModel.replaceTitle'), + () => deployMutation.mutate(deployPayload), + 'replace' + ); + } else if (updateType) { + const updateType = getUpdateType(payload); + openModal( + t('dataModels.createDataModel.replaceDesc'), + t('dataModels.createDataModel.replaceTitle'), + () => updateMutation.mutate(buildUpdatedPayload(updateType)), + 'replace' + ); + } + + } else if (!isChangingToProduction &&updateType) { + // Direct update for non-production changes + updateMutation.mutate(updatedPayload); + } else if (isDeploymentChanged) { + // Only deployment environment changed, no update needed, just deploy + const deployPayload = { + modelId: modelMetadata.modelId ?? "", + currentEnv: initialData.deploymentEnvironment ?? "", + targetEnv: dataModel.deploymentEnvironment ?? "" + }; + deployMutation.mutate(deployPayload); + } + }; + + const getUpdateType = (payload: any): string | undefined => { + if (payload.datasetId) { + return UpdateType.MAJOR; + } else if (!areArraysEqual(initialData.baseModels as string[], dataModel.baseModels as string[])) { + return UpdateType.MINOR; + } + return undefined; + }; + + const buildUpdatedPayload = (updateType: string | undefined) => ({ + modelGroupKey: modelMetadata.modelGroupKey ?? "", + modelName: dataModel.modelName ?? "", + connectedDsId: Number(dataModel.datasetId) ?? 0, + deploymentEnv: dataModel.deploymentEnvironment ?? "", + baseModels: dataModel.baseModels ?? [], + connectedDsMajorVersion: Number(dataModel.version?.split('.')[0]?.[1]) ?? 0, + connectedDsMinorVersion: Number(dataModel.version?.split('.')[1]) ?? 0, + updateType: updateType ?? "", + isTrainingNeeded: !areArraysEqual(initialData.baseModels as string[], dataModel.baseModels as string[]) + }); + + const deleteDataModelMutation = useMutation({ + mutationFn: deleteDataModel, + onSuccess: () => { + open({ + title: t('dataModels.configureDataModel.deleteModalSuccessTitle'), + content: t('dataModels.configureDataModel.deleteModalSuccessDesc'), + footer: ( +
    + +
    + ), + }); + }, + onError: () => { + open({ + title: t('dataModels.configureDataModel.deleteModalErrorTitle'), + content: t('dataModels.configureDataModel.deleteModalErrorDesc'), + }); + }, + }); + + const handleDelete = () => { + if (dataModel.deploymentEnvironment === Maturity.PRODUCTION) { + openModal( + t('dataModels.configureDataModel.deleteErrorDesc'), + t('dataModels.configureDataModel.deleteErrorTitle'), + () => navigate('/data-models'), + 'warning' + ); + } else { + openModal( + t('dataModels.configureDataModel.deleteConfirmationDesc'), + t('dataModels.configureDataModel.deleteConfirmation'), + () => deleteDataModelMutation.mutate(modelId), + 'delete' + ); + } + + }; + + + const openModal = ( + content: string, + title: string, + onConfirm: () => void, + modalType: string + ) => { + setModalOpen(true); + setModalType(modalType); + setModalDiscription(content); + setModalTitle(title); + modalFunciton.current = onConfirm; + }; + + return ( +
    +
    +
    + + + +
    + {t('dataModels.configureDataModel.title')} +
    +
    + + {modelMetadata?.modelStatus === "deprecated" && ( +
    +
    +

    {t('dataModels.configureDataModel.retrainCard')}

    + +
    +
    + )} + {false ? ( + + ) : ( + + )} +
    +
    + + + +
    + + setModalOpen(false)} + isOpen={modalOpen} + title={modalTitle} + footer={ +
    + + {modalType === 'delete' ? ( + + ) : modalType === 'replace' ? ( + + ) + : modalType === 'warning' ? ( + + ) : modalType === 'prod-update' ? ( + + ) + : ( + null + )} +
    + } + > +
    {modalDiscription}
    +
    +
    + ); +}; + +export default ConfigureDataModel; \ No newline at end of file diff --git a/GUI/src/pages/DataModels/CreateDataModel.tsx b/GUI/src/pages/DataModels/CreateDataModel.tsx new file mode 100644 index 00000000..4db498cf --- /dev/null +++ b/GUI/src/pages/DataModels/CreateDataModel.tsx @@ -0,0 +1,153 @@ +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button } from 'components'; +import { Link, useNavigate } from 'react-router-dom'; +import './DataModels.scss'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { useDialog } from 'hooks/useDialog'; +import BackArrowButton from 'assets/BackArrowButton'; +import DataModelForm from 'components/molecules/DataModelForm'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import { + DataModel, + ErrorsType, +} from 'types/dataModels'; +import { da } from 'date-fns/locale'; +import { createDataModel, getProductionDataModel } from 'services/datamodels'; +import { dataModelsQueryKeys } from 'utils/queryKeys'; + +const CreateDataModel: FC = () => { + const { t } = useTranslation(); + const { open, close } = useDialog(); + const navigate = useNavigate(); + + const [dataModel, setDataModel] = useState>({ + modelName: '', + datasetId: 0, + baseModels: [], + deploymentEnvironment: '', + version: 'V1.0', + }); + + const { data: prodDataModel, isLoading: isProdDataModelLoading } = useQuery({ + queryKey: dataModelsQueryKeys.GET_PROD_DATA_MODEL(), + queryFn: () => getProductionDataModel(), + }); + + const handleDataModelAttributesChange = (name: string, value: string) => { + setDataModel((prevFilters) => ({ + ...prevFilters, + [name]: value, + })); + + setErrors((prevErrors) => { + const updatedErrors = { ...prevErrors }; + + if (name === 'modelName' && value !== '') { + delete updatedErrors.modelName; + } + if (name === 'baseModels' && value !== '') { + delete updatedErrors.baseModels; + } + if (name === 'deploymentEnvironment' && value !== '') { + delete updatedErrors.deploymentEnvironment; + } + if (name === 'datasetId') { + delete updatedErrors.datasetId; + } + + return updatedErrors; + }); + }; + + const [errors, setErrors] = useState({ + modelName: '', + datasetId: '', + baseModels: '', + deploymentEnvironment: '', + }); + + const mutation = useMutation({ + mutationFn: createDataModel, + onSuccess: () => { + open({ + title: t('dataModels.createDataModel.successTitle'), + content: t('dataModels.createDataModel.successDesc'), + footer: (
    ) + }); + + }, + onError: () => { + open({ + title: t('dataModels.createDataModel.errorTitle'), + content: t('dataModels.createDataModel.errorDesc'), + }); + }, + }); + + const handleCreate = () => { + + const paylod = { + modelName: dataModel.modelName ?? "", + deploymentEnv: dataModel.deploymentEnvironment ?? "", + baseModels: dataModel.baseModels ?? [], + connectedDsId: Number(dataModel.datasetId) ?? 0, + connectedDsMajorVersion: Number(dataModel?.version?.split('.')[0]?.[1]) ?? "", + connectedDsMinorVersion: Number(dataModel?.version?.split('.')[1]) ?? "", + } + + if (prodDataModel && dataModel.deploymentEnvironment === "production") { + open({ + title: t('dataModels.createDataModel.replaceTitle'), + content: t('dataModels.createDataModel.replaceDesc'), + footer: (
    ) + }); + } else { + mutation.mutate(paylod); + } + }; + + const isCreateDisabled = () => { + return ( + !dataModel.modelName || + !dataModel.datasetId || + !dataModel.baseModels || + (Array.isArray(dataModel.baseModels) && dataModel.baseModels.length === 0) || + !dataModel.deploymentEnvironment + ); + }; + + return ( +
    +
    +
    +
    + + + +
    {t('dataModels.createDataModel.title')}
    +
    +
    + +
    +
    + + +
    +
    + ); +}; + +export default CreateDataModel; diff --git a/GUI/src/pages/DataModels/DataModels.scss b/GUI/src/pages/DataModels/DataModels.scss new file mode 100644 index 00000000..49197f97 --- /dev/null +++ b/GUI/src/pages/DataModels/DataModels.scss @@ -0,0 +1,92 @@ +.grey-card { + border: 1px solid #a6a8b1; + border-radius: 5px; + margin-bottom: 10px; + display: flex; + gap: 20px; + align-items: center; + background-color: #f9f9f9; + padding: 25px; +} + +.blue-card { + border-radius: 10px; + margin-bottom: 10px; + display: flex; + gap: 20px; + align-items: center; + background-color: #d7edff; + padding: 25px; + margin-top: 20px; +} + +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + margin: 0; + padding: 20px; +} + +@keyframes scaleIn { + 0% { + transform: scale(1); + opacity: 0; + } + + 100% { + transform: scale(1.02); + opacity: 1; + } +} + +.featured-content { + background-color: hsl(40, 100%, 96%); + border: 2px solid #f39c12; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + border-radius: 10px; + padding: 10px 20px; + margin: 20px auto; + transform: scale(1.02); + transition: transform 0.3s, box-shadow 0.3s; + animation: scaleIn 0.5s ease-out; +} + +.mt-30 { + margin-top: 30px; +} + +.m-30-0 { + margin: 30px 0px; +} + +.models-filter-div { + display: flex; + flex-wrap: wrap; + gap: 16px; + width: 100%; +} + +.filter-buttons { + display: flex; + gap: 16px; +} + +.data-model-buttons { + align-items: end; + gap: 10px; + justify-content: end; + margin: 25px -16px -16px; + padding: 20px 64px; + background-color: white; +} + +.metadata-card { + justify-content: center; + text-align: center; + background-color: #FFE8E9; + color: #D73E3E; + border-radius: .3rem; + padding: 2rem; + border: 1px solid #D73E3E; + margin-bottom: 2rem; +} \ No newline at end of file diff --git a/GUI/src/pages/DataModels/index.tsx b/GUI/src/pages/DataModels/index.tsx new file mode 100644 index 00000000..3ea98ac7 --- /dev/null +++ b/GUI/src/pages/DataModels/index.tsx @@ -0,0 +1,244 @@ +import { FC, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, FormSelect } from 'components'; +import Pagination from 'components/molecules/Pagination'; +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import { formattedArray } from 'utils/commonUtilts'; +import DataModelCard from 'components/molecules/DataModelCard'; +import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; +import { ButtonAppearanceTypes } from 'enums/commonEnums'; +import { + DataModelResponse, + DataModelsFilters, + FilterData, +} from 'types/dataModels'; +import { dataModelsQueryKeys } from 'utils/queryKeys'; +import NoDataView from 'components/molecules/NoDataView'; +import './DataModels.scss'; +import { getDataModelsOverview, getDeploymentEnvironments, getProductionDataModel } from 'services/datamodels'; +import { fi } from 'date-fns/locale'; +import { modelStatuses, trainingStatuses } from 'config/dataModelsConfig'; + +const DataModels: FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const [pageIndex, setPageIndex] = useState(1); + + const [view, setView] = useState<'list' | 'individual'>('list'); + + const [filters, setFilters] = useState({ + modelName: 'all', + modelStatus: 'all', + trainingStatus: 'all', + deploymentEnvironment: 'all', + sort: 'createdAt desc', + }); + + const { data: dataModelsData, isLoading: isModelDataLoading } = useQuery({ + queryKey: dataModelsQueryKeys.DATA_MODELS_OVERVIEW(pageIndex, filters.modelStatus, filters.trainingStatus, filters.deploymentEnvironment, filters.sort), + queryFn: () => getDataModelsOverview(pageIndex, filters.modelStatus, filters.trainingStatus, filters.deploymentEnvironment, filters.sort), + }); + + const { data: prodDataModel, isLoading: isProdDataModelLoading } = useQuery({ + queryKey: dataModelsQueryKeys.GET_PROD_DATA_MODEL(), + queryFn: () => getProductionDataModel(), + }); + + const { data: deploymentEnvironmentsData } = useQuery({ + queryKey: dataModelsQueryKeys.DATA_MODEL_DEPLOYMENT_ENVIRONMENTS(), + queryFn: () => getDeploymentEnvironments(), + }); + + const pageCount = dataModelsData?.[0]?.totalPages || 0; + + const handleFilterChange = ( + name: keyof DataModelsFilters, + value: string | number | undefined | { name: string; id: string } + ) => { + setFilters((prevFilters) => ({ + ...prevFilters, + [name]: value, + })); + }; + + return ( +
    +
    + {!isModelDataLoading ? ( +
    + {/*
    +
    +
    + {t('dataModels.productionModels')} +
    {' '} +
    + +
    */} +
    +
    +
    {t('dataModels.dataModels')}
    + +
    +
    +
    + + + handleFilterChange('modelStatus', selection?.value ?? '') + } + defaultValue={filters?.modelStatus} + style={{ width: '15%' }} + /> + + + handleFilterChange('trainingStatus', selection?.value) + } + defaultValue={filters?.trainingStatus} + style={{ width: '15%' }} + /> + + handleFilterChange('deploymentEnvironment', selection?.value) + } + defaultValue={filters?.deploymentEnvironment} + style={{ width: '25%' }} + /> + + handleFilterChange('sort', selection?.value) + } + defaultValue={filters?.sort} + style={{ width: '25%' }} + /> + +
    + +
    + {prodDataModel != null &&
    +

    Deployed Model

    +
    +
    +
    } + + {dataModelsData?.length > 0 ? ( +

    Other Data Models

    +
    + + {dataModelsData?.map( + (model: DataModelResponse, index: number) => { + return ( + + ); + } + )} +
    +
    + + ) : ( + + )} +
    + 1} + canNextPage={pageIndex < 10} + onPageChange={setPageIndex} + /> +
    + ) : ( + + )} +
    +
    + ); +}; + +export default DataModels; diff --git a/GUI/src/pages/Datasets/DatasetGroups.scss b/GUI/src/pages/Datasets/DatasetGroups.scss new file mode 100644 index 00000000..1c766276 --- /dev/null +++ b/GUI/src/pages/Datasets/DatasetGroups.scss @@ -0,0 +1,134 @@ +@import 'src/styles/tools/color'; +@import 'src/styles/tools/spacing'; + +.search-panel { + background-color: #f9f9f9; + border-radius: 10px; + border: solid 1px get-color(black-coral-1); + display: flex; + align-items: center; + gap: 10px; + padding: 15px; + margin-bottom: 1rem; + + @media (max-width: 1024px) { + flex-wrap: wrap; + } +} + +.dataset-group-card { + width: 100%; + border: 1px solid #ccc; + border-radius: 8px; + background-color: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 15px; + box-sizing: border-box; +} + +.grid-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + width: 100%; + + @media (max-width: 1024px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (max-width: 800px) { + grid-template-columns: repeat(1, 1fr); + } +} + +.banner { + background-color: #fff3f3; + border: 1px solid #cf0000; + border-radius: 5px; + margin-bottom: 10px; + padding: 20px 10%; + text-align: center; + color: #cf0000; +} + +.footer-button-group { + display: flex; + align-items: flex-end; + justify-content: flex-end; + gap: 10px; + margin-top: 25px; +} + +.skeleton-container { + background-color: #fff; + padding: 20px; + margin-top: 20px; +} + +.container { + min-height: 100vh; + padding-bottom: 100px; + box-sizing: border-box; +} + +.button-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + margin-top: 25px; + padding: 20px 50px 20px 0px; + background-color: #fff; + z-index: 10; + margin-right: 15px; +} + +.button-container-bottom { + bottom: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + margin-top: 25px; + padding: 20px 50px 20px 0px; + background-color: #fff; + z-index: 10; + margin-right: 15px; +} +.content-wrapper { + padding-bottom: 60px; +} + +.dataset-table-card { + padding: 20px 10%; + justify-content: center; + text-align: center; +} + +.dataset-no-data-view { + margin-bottom: 10px; + font-size: 20px; +} + +.align-center{ + align-items: center; +} + +.dataset-controls { + display: flex; + justify-content: flex-end; + margin-bottom: 1rem; + padding: 0 1rem; + + .filter-controls { + display: flex; + gap: 0.5rem; + align-items: center; + } +} diff --git a/GUI/src/pages/Datasets/index.tsx b/GUI/src/pages/Datasets/index.tsx new file mode 100644 index 00000000..e7ed28a8 --- /dev/null +++ b/GUI/src/pages/Datasets/index.tsx @@ -0,0 +1,132 @@ +import { FC, useState } from 'react'; +import './DatasetGroups.scss'; +import { useTranslation } from 'react-i18next'; +import { Button, FormSelect } from 'components'; +import Pagination from 'components/molecules/Pagination'; +import { useQuery } from '@tanstack/react-query'; +import { datasetQueryKeys } from 'utils/queryKeys'; +import { DatasetViewEnum } from 'enums/datasetEnums'; +import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; +import NoDataView from 'components/molecules/NoDataView'; +import SearchInput from 'components/FormElements/SearchInput'; +import DatasetCard from 'components/molecules/DatasetCard'; +import { getDatasetsOverview } from 'services/datasets'; +import { Dataset } from 'types/datasets'; + +const Datasets: FC = () => { + const { t } = useTranslation(); + + const [pageIndex, setPageIndex] = useState(1); + const view = (DatasetViewEnum.LIST); + const [sortOption, setSortOption] = useState("created_at desc"); + const [searchTerm, setSearchTerm] = useState('all'); + + const { data: datasets, isLoading } = useQuery({ + queryKey: datasetQueryKeys.DATASET_OVERVIEW(pageIndex, sortOption), + queryFn: () => getDatasetsOverview(pageIndex, sortOption), + }); + + const pageCount = datasets?.[0]?.totalPages ?? 1; + + const handleSearch = (term: string) => { + // Set the search term to 'all' if empty, otherwise use the provided term + setSearchTerm(term.trim() === '' ? 'all' : term); + // Reset to first page when searching to show most relevant results first + setPageIndex(1); + }; + + const handleSortChange = (selection: any) => { + setSortOption(selection?.value as string); + setPageIndex(1); + }; + + return ( +
    + {view === DatasetViewEnum.LIST && ( +
    +
    +
    {t('datasets.title')}
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + + {isLoading && ( +
    + +
    + )} + {datasets?.length > 0 && ( +
    + {datasets?.map( + (dataset: Dataset, index: number) => { + return ( + + ); + } + )} +
    + )} + + {!isLoading && datasets?.length === 0 && ( + + )} + + 1} + canNextPage={pageIndex < pageCount} + onPageChange={setPageIndex} + /> +
    +
    + )} +
    + ); +}; + +export default Datasets; diff --git a/GUI/src/pages/IntegratedAgencies/DatasetGroups.scss b/GUI/src/pages/IntegratedAgencies/DatasetGroups.scss new file mode 100644 index 00000000..17f9878b --- /dev/null +++ b/GUI/src/pages/IntegratedAgencies/DatasetGroups.scss @@ -0,0 +1,107 @@ +@import 'src/styles/tools/color'; +@import 'src/styles/tools/spacing'; + +.search-panel { + background-color: #f9f9f9; + border-radius: 10px; + border: solid 1px get-color(black-coral-1); + display: flex; + align-items: center; + gap: 10px; + padding: 15px; + margin-bottom: 1rem; + + @media (max-width: 1024px) { + flex-wrap: wrap; + } +} + +.dataset-group-card { + width: 100%; + border: 1px solid #ccc; + border-radius: 8px; + background-color: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 15px; + box-sizing: border-box; +} + +.grid-container { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + width: 100%; + + @media (max-width: 1024px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (max-width: 800px) { + grid-template-columns: repeat(1, 1fr); + } +} + +.banner { + background-color: #fff3f3; + border: 1px solid #cf0000; + border-radius: 5px; + margin-bottom: 10px; + padding: 20px 10%; + text-align: center; + color: #cf0000; +} + +.footer-button-group { + display: flex; + align-items: flex-end; + justify-content: flex-end; + gap: 10px; + margin-top: 25px; +} + +.skeleton-container { + background-color: #fff; + padding: 20px; + margin-top: 20px; +} + +.container { + min-height: 100vh; + padding-bottom: 100px; + box-sizing: border-box; +} + +.button-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + margin-top: 25px; + padding: 20px 50px 20px 0px; + background-color: #fff; + z-index: 10; + margin-right: 15px; +} + +.content-wrapper { + padding-bottom: 60px; +} + +.dataset-table-card { + padding: 20px 10%; + justify-content: center; + text-align: center; +} + +.dataset-no-data-view { + margin-bottom: 10px; + font-size: 20px; +} + +.align-center{ + align-items: center; +} diff --git a/GUI/src/pages/IntegratedAgencies/index.tsx b/GUI/src/pages/IntegratedAgencies/index.tsx new file mode 100644 index 00000000..a7f84186 --- /dev/null +++ b/GUI/src/pages/IntegratedAgencies/index.tsx @@ -0,0 +1,158 @@ +import { FC, useState } from 'react'; +import './DatasetGroups.scss'; +import { useTranslation } from 'react-i18next'; +import { Button, FormSelect } from 'components'; +import Pagination from 'components/molecules/Pagination'; +import { useQuery } from '@tanstack/react-query'; +import { integratedAgenciesQueryKeys } from 'utils/queryKeys'; +import { DatasetViewEnum } from 'enums/datasetEnums'; +import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; +import NoDataView from 'components/molecules/NoDataView'; +import SearchInput from 'components/FormElements/SearchInput'; +import { fetchAgencies } from 'services/agencies'; +import IntegratedAgencyCard from 'components/molecules/IntegratedAgencyCard'; +import { Agency } from 'types/agencies'; + +const IntegratedAgencies: FC = () => { + const { t } = useTranslation(); + + const [pageIndex, setPageIndex] = useState(1); + const view = (DatasetViewEnum.LIST); + const [sortOption, setSortOption] = useState("last_updated_timestamp desc"); + const [searchTerm, setSearchTerm] = useState('all'); + + + const { data: agencies, isLoading } = useQuery({ + queryKey: integratedAgenciesQueryKeys.INTEGRATED_AGENCIES_LIST(pageIndex,sortOption,searchTerm), + queryFn: () => fetchAgencies(pageIndex,sortOption,searchTerm), + }); + + const pageCount=agencies?.[0]?.totalPages??1; + + + const handleSearch = (term: string) => { + // Set the search term to 'all' if empty, otherwise use the provided term + setSearchTerm(term.trim() === '' ? 'all' : term); + // Reset to first page when searching to show most relevant results first + setPageIndex(1); + }; + + const handleSortChange = (selection: any) => { + setSortOption(selection?.value as string); + setPageIndex(1); + }; + + return ( +
    + {view === DatasetViewEnum.LIST && ( +
    +
    +
    {t('integratedAgencies.title')}
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + + {isLoading && ( +
    + +
    + )} + {agencies?.length > 0 && ( +
    + {agencies?.map( + (agenciesData : Agency, index: number) => { + return ( + + ); + } + )} +
    + ) } + + {!isLoading && agencies?.length===0 && ( + + )} + + 1} + canNextPage={pageIndex < pageCount} + onPageChange={setPageIndex} + /> +
    +
    + )} +
    + ); +}; + +export default IntegratedAgencies; diff --git a/GUI/src/pages/LoadingScreen/LoadingScreen.scss b/GUI/src/pages/LoadingScreen/LoadingScreen.scss new file mode 100644 index 00000000..c45e573a --- /dev/null +++ b/GUI/src/pages/LoadingScreen/LoadingScreen.scss @@ -0,0 +1,20 @@ +/* Loader container */ +.loader { + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border: 8px solid #f3f3f3; /* Light grey */ + border-top: 8px solid #3498db; /* Blue */ + border-radius: 50%; + width: 60px; + height: 60px; + animation: spin 1.5s linear infinite; + } + + /* Spin animation */ + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + \ No newline at end of file diff --git a/GUI/src/pages/LoadingScreen/LoadingScreen.tsx b/GUI/src/pages/LoadingScreen/LoadingScreen.tsx new file mode 100644 index 00000000..3f8add92 --- /dev/null +++ b/GUI/src/pages/LoadingScreen/LoadingScreen.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react'; +import './LoadingScreen.scss' + +const LoadingScreen: FC = () => { + return ( +
    +
    +
    + ); +}; + +export default LoadingScreen; \ No newline at end of file diff --git a/GUI/src/pages/TestModel/TestModel.scss b/GUI/src/pages/TestModel/TestModel.scss new file mode 100644 index 00000000..58cfbbad --- /dev/null +++ b/GUI/src/pages/TestModel/TestModel.scss @@ -0,0 +1,136 @@ +.testModalFormTextArea { + margin-top: 30px; +} + +.testModalClassifyButton { + text-align: right; + margin-top: 20px; +} + +.testModalList { + list-style: disc; + margin-left: 30px; +} + +.mt-20 { + margin-top: 20px; +} + +.classification-results { + margin-top: 1rem; + padding: 1rem; + border: 1px solid #e0e0e0; + border-radius: 8px; + background-color: #f9f9f9; + + h3 { + margin: 0 0 1rem 0; + color: #333; + } + + h4 { + margin: 0 0 0.75rem 0; + color: #555; + font-size: 1rem; + } + + .results-container { + display: flex; + flex-direction: column; + gap: 1.5rem; + } + + .top-prediction { + .prediction-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-radius: 8px; + background-color: #e8f5e8; + border: 2px solid #4caf50; + + .agency-name { + font-weight: 600; + color: #2e7d32; + font-size: 1.1rem; + } + + .confidence-score { + font-weight: 700; + color: #2e7d32; + font-size: 1.2rem; + } + } + } + + .predictions-list { + display: flex; + flex-direction: column; + gap: 0.75rem; + + .prediction-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem; + background-color: white; + border-radius: 6px; + border: 1px solid #ddd; + + &.highest { + border-color: #4caf50; + background-color: #f8fff8; + } + + .rank { + font-weight: 600; + color: #666; + min-width: 2rem; + } + + .agency-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25rem; + + .agency-name { + font-weight: 500; + color: #333; + } + + .confidence-bar-container { + width: 100%; + height: 4px; + background-color: #e0e0e0; + border-radius: 2px; + overflow: hidden; + + .confidence-bar { + height: 100%; + background-color: #4caf50; + transition: width 0.3s ease; + } + } + } + + .confidence-percentage { + font-weight: 600; + color: #555; + min-width: 4rem; + text-align: right; + } + } + } +} + +.classification-error { + margin-top: 1rem; + padding: 1rem; + background-color: #ffebee; + border: 1px solid #f44336; + border-radius: 6px; + color: #c62828; + text-align: center; +} \ No newline at end of file diff --git a/GUI/src/pages/TestModel/index.tsx b/GUI/src/pages/TestModel/index.tsx new file mode 100644 index 00000000..23e190f1 --- /dev/null +++ b/GUI/src/pages/TestModel/index.tsx @@ -0,0 +1,205 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { Button, FormSelect, FormTextarea } from 'components'; +import CircularSpinner from 'components/molecules/CircularSpinner/CircularSpinner'; +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + ClassifyTestModalPayloadType +} from 'types/testModelTypes'; + +import './TestModel.scss'; +import { dataModelsQueryKeys } from 'utils/queryKeys'; +import { classify, getAllModelVersions, loadModel } from 'services/datamodels'; +import { toLabelValueArray } from 'utils/commonUtilts'; +import { useDialog } from 'hooks/useDialog'; + +const TestModel: FC = () => { + const { t } = useTranslation(); + const isLoading = false; + const [modelLoadingStatus, setModelLoadingStatus] = useState(""); + const [color, setColor] = useState("black"); + const [isClassifyEnabled, setIsClassifyEnabled] = useState(false); + const [classificationResult, setClassificationResult] = useState([]); + + const { open } = useDialog(); + + const { data: modelVersions } = useQuery({ + queryKey: dataModelsQueryKeys.GET_ALL_DATA_MODELS_VERSIONS(), + queryFn: () => getAllModelVersions(), + }); + + const [testModel, setTestModel] = useState({ + modelId: null, + text: '', + }); + + const handleChange = (key: string, value: string | number) => { + setTestModel((prev) => ({ + ...prev, + [key]: value, + })); + }; + + const mutation = useMutation({ + mutationFn: loadModel, + onSuccess: () => { + setModelLoadingStatus(t('dataModels.loadDataModel.loaded') ?? ""); + setColor("#2c7a4c"); + setIsClassifyEnabled(true); + }, + onError: () => { + open({ + title: t('dataModels.loadDataModel.errorTitle'), + content: t('dataModels.loadDataModel.errorDesc'), + }); + setModelLoadingStatus(t('dataModels.loadDataModel.errorTitle') ?? ""); + setColor("#d73e3e"); + }, + }); + + const classifyMutation = useMutation({ + mutationFn: classify, + onSuccess: (data) => { + setClassificationResult(data); + }, + onError: () => { + open({ + title: t('testModels.error'), + content: t('testModels.errorDesc'), + }); + }, + }); + +const processClassificationResult = (result: any) => { + if (!result || !Array.isArray(result) || result.length === 0) return []; + + // Get the first array (which contains the classification results) + const resultData = result[0]; + + // Check if resultData is an array of classification objects + if (!Array.isArray(resultData)) return []; + + return resultData.map((item: any, index: number) => { + return { + rank: index + 1, + agencyId: item.agency_id, + agencyName: item.agency_name?.replace(/_/g, ' ') || `Agency ${item.agency_id}`, + confidence: item.confidence || 0 + }; + }).sort((a, b) => b.confidence - a.confidence); // Sort by confidence descending +}; + + const processedResults = classificationResult ? processClassificationResult(classificationResult) : []; + + + return ( +
    + {isLoading ? ( + + ) : ( +
    +
    +
    {t('testModels.title')}
    +
    +
    +

    {t('testModels.selectionLabel')}

    +
    + + { + handleChange('modelId', selection?.value as string); + }} + value={testModel?.modelId === null ? t('testModels.errors.modelNotExist') : undefined} defaultValue={testModel?.modelId ?? undefined} + /> + +
    {modelLoadingStatus}
    +
    +
    + +
    +

    {t('testModels.classifyTextLabel')}

    + handleChange('text', e.target.value)} + showMaxLength={true} + /> +
    +
    + +
    + + {processedResults.length > 0 && ( +
    +

    {t('testModels.results') || 'Classification Results'}

    +
    +
    +

    {t('testModels.topPrediction') || 'Top Prediction'}

    +
    +
    + {processedResults[0].agencyName} +
    +
    + {(processedResults[0].confidence).toFixed(10)} +
    +
    +
    + {processedResults.length > 1 && ( +
    +

    {t('testModels.allPredictions') || 'All Predictions'}

    +
    + {processedResults.map((result, index) => ( +
    +
    #{index + 1}
    +
    + + {result.agencyName} + +
    +
    +
    +
    +
    + {(result.confidence).toFixed(10)} +
    +
    + ))} +
    +
    + )} +
    +
    + )} + + {/* Error State */} + {classifyMutation.isError && ( +
    +

    {t('testModels.classificationFailed') || 'Classification failed. Please try again.'}

    +
    + )} +
    + )} +
    + ); +}; + +export default TestModel; \ No newline at end of file diff --git a/GUI/src/pages/TrainingSessions/index.tsx b/GUI/src/pages/TrainingSessions/index.tsx new file mode 100644 index 00000000..b9c23314 --- /dev/null +++ b/GUI/src/pages/TrainingSessions/index.tsx @@ -0,0 +1,38 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import TrainingSessionCard from 'components/molecules/TrainingSessionCard'; +import NoDataView from 'components/molecules/NoDataView'; +import { useTrainingSessions } from 'hooks/useTrainingSessions'; + +const TrainingSessions: FC = () => { + const { t } = useTranslation(); + const progresses = useTrainingSessions(); + + return ( +
    +
    +
    +
    {t('trainingSessions.title')}
    +
    + {progresses?.length === 0 && ( +
    + +
    + )} + {progresses?.map((session) => ( + + ))} +
    +
    + ); +}; + +export default TrainingSessions; diff --git a/GUI/src/pages/Unauthorized/unauthorized.scss b/GUI/src/pages/Unauthorized/unauthorized.scss new file mode 100644 index 00000000..3c1bb0ff --- /dev/null +++ b/GUI/src/pages/Unauthorized/unauthorized.scss @@ -0,0 +1,30 @@ +.unauthorized-container { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + background-color: #f0f2f5; + padding: 20px; + box-sizing: border-box; + } + + .unauthorized-card { + background-color: #fff; + padding: 40px; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 400px; + width: 100%; + } + + .unauthorized-header { + font-size: 2.5em; + color: #333; + margin-bottom: 20px; + } + + .unauthorized-message { + font-size: 1.2em; + color: #555; + } diff --git a/GUI/src/pages/Unauthorized/unauthorized.tsx b/GUI/src/pages/Unauthorized/unauthorized.tsx new file mode 100644 index 00000000..088fd80b --- /dev/null +++ b/GUI/src/pages/Unauthorized/unauthorized.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react'; +import './unauthorized.scss'; +import { useTranslation } from 'react-i18next'; + +const Unauthorized: FC = () => { + const { t } = useTranslation(); + return ( +
    +
    +

    {t('global.unAuthorized')}

    +

    {t('global.unAuthorizedDesc')}

    +
    +
    + ); +}; + +export default Unauthorized; diff --git a/GUI/src/pages/UserManagement/SettingsUsers.scss b/GUI/src/pages/UserManagement/SettingsUsers.scss new file mode 100644 index 00000000..37e5f63b --- /dev/null +++ b/GUI/src/pages/UserManagement/SettingsUsers.scss @@ -0,0 +1,48 @@ +@import 'src/styles/tools/spacing'; +@import 'src/styles/tools/color'; +@import 'src/styles/settings/variables/other'; +@import 'src/styles/settings/variables/typography'; + +.multiSelect { + $self: &; + display: flex; + align-items: center; + gap: get-spacing(paldiski); + width: 100%; + &::placeholder { + color: get-color(black-coral-6); + font-size: small; + } + + &__label { + flex: 0 0 185px; + font-size: $veera-font-size-100; + line-height: 24px; + } + + + &__wrapper { + width: 390px; + flex: 1; + display: block; + flex-direction: column; + gap: 7px; + position: relative; + border: 0.15px solid get-color(black-coral-6); + border-radius: $veera-radius-s; + } +} + +.footer-button-wrapper { + display: flex; + gap: 10px; +} + +.button-wrapper { + display: flex; + gap: 10px; +} + +.error-span { + color: get-color(jasper-10); +} \ No newline at end of file diff --git a/GUI/src/pages/UserManagement/UserManagement.scss b/GUI/src/pages/UserManagement/UserManagement.scss new file mode 100644 index 00000000..969be2a6 --- /dev/null +++ b/GUI/src/pages/UserManagement/UserManagement.scss @@ -0,0 +1,28 @@ + +.button { + background-color: #007bff; + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; +} + +.button:hover { + background-color: #0056b3; +} + +.form-group { + margin-bottom: 20px; +} + +.table-header { + display: flex; + width: 100%; + justify-content: end; +} + +.action-button-container { + display: flex; + gap: 10px; +} diff --git a/GUI/src/pages/UserManagement/UserModal.tsx b/GUI/src/pages/UserManagement/UserModal.tsx new file mode 100644 index 00000000..cd7f51c3 --- /dev/null +++ b/GUI/src/pages/UserManagement/UserModal.tsx @@ -0,0 +1,298 @@ +import { useForm, Controller, useWatch } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { AxiosError } from 'axios'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { Button, Dialog, FormInput, Track } from 'components'; +import { User, UserDTO } from 'types/user'; +import { checkIfUserExists, createUser, editUser } from 'services/users'; +import { useToast } from 'hooks/useToast'; +import Select, { components } from 'react-select'; +import './SettingsUsers.scss'; +import { FC, useMemo, useState } from 'react'; +import { ROLES } from 'enums/roles'; +import { userManagementQueryKeys } from 'utils/queryKeys'; +import { ButtonAppearanceTypes, ToastTypes } from 'enums/commonEnums'; +import { FaChevronDown, FaChevronUp } from 'react-icons/fa'; + +type UserModalProps = { + onClose: () => void; + user?: User; + isModalOpen?: boolean; +}; + +const DropdownIndicator = (props: any) => { + return ( + + {props.selectProps.menuIsOpen ? : } + + ); +}; + +const UserModal: FC = ({ onClose, user, isModalOpen }) => { + const { t } = useTranslation(); + const toast = useToast(); + const queryClient = useQueryClient(); + const [isValidIdentification, setIsValidIdentification] = + useState(false); + + const { + register, + control, + handleSubmit, + formState: { errors, isDirty }, + } = useForm({ + defaultValues: { + useridcode: user?.useridcode, + authorities: user?.authorities, + csaTitle: user?.csaTitle, + csaEmail: user?.csaEmail, + fullName: user?.firstName && user?.lastName ?`${user?.firstName} ${user?.lastName}`:"", + }, + }); + + const watchedValues = useWatch({ + control }); + + const roles = useMemo( + () => [ + { label: t('roles.ROLE_ADMINISTRATOR'), value: ROLES.ROLE_ADMINISTRATOR }, + { + label: t('roles.ROLE_MODEL_TRAINER'), + value: ROLES.ROLE_MODEL_TRAINER, + }, + ], + [t] + ); + + const userCreateMutation = useMutation({ + mutationFn: (data: UserDTO) => createUser(data), + onSuccess: async () => { + await queryClient.invalidateQueries( + userManagementQueryKeys.getAllEmployees() + ); + toast.open({ + type: ToastTypes.SUCCESS, + title: t('global.notification'), + message: t('toast.success.newUserAdded'), + }); + onClose(); + }, + onError: (error: AxiosError) => { + toast.open({ + type: ToastTypes.ERROR, + title: t('global.notificationError'), + message: error?.message ?? '', + }); + }, + }); + + const userEditMutation = useMutation({ + mutationFn: ({ + id, + userData, + }: { + id: string | number; + userData: UserDTO; + }) => editUser(id, userData), + onSuccess: async () => { + await queryClient.invalidateQueries( + userManagementQueryKeys.getAllEmployees() + ); + toast.open({ + type: ToastTypes.SUCCESS, + title: t('global.notification'), + message: t('toast.success.userUpdated'), + }); + onClose(); + }, + onError: (error: AxiosError) => { + toast.open({ + type: ToastTypes.ERROR, + title: t('global.notificationError'), + message: error?.message ?? '', + }); + }, + }); + + const checkIfUserExistsMutation = useMutation({ + mutationFn: ({ userData }: { userData: UserDTO }) => + checkIfUserExists(userData), + onSuccess: async (data) => { + if (data.response === 'true') { + setIsValidIdentification(false); + toast.open({ + type: ToastTypes.ERROR, + title: t('global.notificationError'), + message: t('userManagement.addUser.userExists'), + }); + } else { + createNewUser(); + } + }, + onError: (error: AxiosError) => { + toast.open({ + type: ToastTypes.ERROR, + title: t('global.notificationError'), + message: error?.message, + }); + }, + }); + + const createNewUser = handleSubmit((userData) => + userCreateMutation.mutate(userData) + ); + + const handleUserSubmit = handleSubmit((data) => { + if (user) userEditMutation.mutate({ id: user.useridcode, userData: data }); + else checkIfUserExistsMutation.mutate({ userData: data }); + }); + + const hasChangedFields = () => { + return ( + watchedValues.useridcode !== user?.useridcode || + watchedValues.authorities?.join(',') !== user?.authorities?.join(',') || + watchedValues !== user?.displayName || + watchedValues.csaTitle !== user?.csaTitle || + watchedValues.csaEmail !== user?.csaEmail); + }; + + return ( + + + +
    + } + > + + + {errors?.fullName && ( + {errors?.fullName?.message} + )} + + ( +
    + +
    + + ), + cell: ({ row }) => ( + + ), + meta: { size: 40 }, + }); + + // Incremental ID column + const incrementalIdColumn = columnHelper.display({ + id: 'rowNumber', + header: t('datasets.detailedView.table.id') || 'Item ID', + cell: ({ row }) => { + const rowNumber = (pagination.pageIndex * pagination.pageSize) + row.index + 1; + return {rowNumber}; + }, + meta: { size: 60 }, + }); + + const questionColumn = columnHelper.accessor('dataItem', { + header: t('datasets.detailedView.table.data') || 'Data', + id: 'dataItem', + }); + + const agencyColumn = columnHelper.accessor('agencyName', { + header: t('datasets.detailedView.table.client') || 'Client Name', + id: 'agencyName', + }); + + // Action columns + const editColumn = columnHelper.display({ + id: 'edit', + cell: editView, + meta: { size: '1%' }, + }); + + const deleteColumn = columnHelper.display({ + id: 'delete', + cell: deleteView, + meta: { size: '1%' }, + }); + + return [selectColumn, incrementalIdColumn, questionColumn, agencyColumn, editColumn, deleteColumn]; + }, [editView, deleteView, pagination.pageIndex, pagination.pageSize, t]); + + const editDataRecord = (dataRow: SelectedRowPayload) => { + const originalRow = originalDataset.find((row: any) => row.itemId === dataRow.itemId); + + const hasChanges = originalRow && ( + originalRow.dataItem !== dataRow.dataItem || + originalRow.agencyId !== dataRow.agencyId + ); + + if (hasChanges) { + setEditedRows((prev) => { + const exists = prev.find((row) => row.itemId === dataRow.itemId); + const newEditedRows = exists + ? prev.map((row) => (row.itemId === dataRow.itemId ? dataRow : row)) + : [...prev, dataRow]; + + return newEditedRows; + }); + } + + setIsUpdateModalOpen(false); + }; + + const deleteDataRecord = (dataRow: SelectedRowPayload) => { + if (!dataRow) return; + setUpdatedDataset((prev: { itemId: number; dataItem: string; agencyName: string; agencyId: string }[] | undefined) => prev?.filter((row: { itemId: number }) => row.itemId !== dataRow.itemId)); + setDeletedRowIds((prev) => [...prev, dataRow.itemId]); + close(); + }; + + const deleteMutation = useMutation({ + mutationFn: deleteDataset, + onSuccess: () => { + navigate('/datasets'); + + }, + onError: () => { + open({ + title: t('datasets.detailedView.datasetUpdateUnsuccessfulTitle'), + content: t('datasets.detailedView.datasetUpdateUnsuccessfulDesc'), + }); + }, + }); + + const updateMutation = useMutation({ + mutationFn: updateDataset, + onSuccess: async () => { + setTimeout(() => { + setIsUpdating(false); + setIsProgressModalOpen(false); + setEditedRows([]); + setDeletedRowIds([]); + }, 3000); + await Promise.all([ + refetchMetadata(), + refetchDataset() + ]); + }, + onError: () => { + setIsUpdating(false); + setIsProgressModalOpen(false); + setEditedRows([]); + setDeletedRowIds([]); + open({ + title: t('datasets.detailedView.datasetUpdateUnsuccessfulTitle'), + content: t('datasets.detailedView.datasetUpdateUnsuccessfulDesc'), + }); + }, + }); + + + const minorUpdate = () => { + setIsProgressModalOpen(true); + + // Create payload inside the function + const updatedDataItems: SelectedRowPayload[] = editedRows.filter((row) => { + return !deletedRowIds.includes(row.itemId); + }); + + const updatePayload = { + updatedDataItems, + deletedRows: deletedRowIds, + updatedRowsLength: updatedDataItems.length, + deletedRowsLength: deletedRowIds.length, + }; + + // Store payload in a ref or state if you need to access it in the dialog + setUpdatePayload(updatePayload); + }; + + const handleDeleteDataset = () => { + open({ + title: t('datasets.detailedView.confirmDeleteDatasetTitle'), + content: t('datasets.detailedView.confirmDeleteDatasetDesc'), + footer: ( +
    + + +
    + ), + }); + }; + + return ( +
    + {isMetadataLoading && } + {metadata && !isMetadataLoading && ( +
    +
    +
    + + + +
    {t('datasets.detailedView.dataset')} {`V${metadata?.major}.${metadata?.minor}`}
    +
    +
    + +
    +
    +

    + {t('datasets.detailedView.version') ?? ''} : {`V${metadata?.major}.${metadata?.minor}`} +

    +
    +
    +

    {t('datasets.detailedView.connectedModels') ?? ''} :

    {metadata?.connectedModels?.join(', ') ?? ''}

    +

    + {t('datasets.detailedView.noOfItems') ?? ''} : {metadata?.totalDataCount ?? "-"} +

    +
    + +
    +
    +
    + )} +
    +
    +
    + {hasActiveFilters && ( + + )} +
    + {/* Bulk actions */} + {selectedRowsCount > 0 && ( +
    + + {selectedRowsCount} {t('datasets.detailedView.itemsSelected') || 'items selected'} + + +
    + )} +
    + {datasetIsLoading && } + {!datasetIsLoading && updatedDataset && updatedDataset?.length > 0 && ( + []} + pagination={pagination} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + dropdownFilters={[ + { + columnId: 'agencyName', + options: agencies?.map((a: { agencyName: string; agencyId: number }) => ({ + label: a.agencyName, + value: a.agencyId, + agencyId: a.agencyId, + })) ?? [], + }, + ]} + showPageSizeSelector={true} + onSelect={(value) => { + setSelectedAgencyId(value); + setPagination({ + pageIndex: 0, + pageSize: pagination.pageSize, + }); + setUpdatedDataset([]); + }} + setPagination={(state: PaginationState) => { + if ( + state.pageIndex === pagination.pageIndex && + state.pageSize === pagination.pageSize + ) + return; + setPagination(state); + }} + pagesCount={dataset?.[0]?.totalPages ?? 0} + isClientSide={false} + /> + )} + { + updatedDataset?.length === 0 && ( + + ) + } +
    + + +
    +
    + {isUpdateModalOpen && ( + setIsUpdateModalOpen(false)} + isOpen={ + isUpdateModalOpen} + > +

    {t('datasets.detailedView.editDataRowDesc')}

    + + ({ + label: a.agencyName, + value: a.agencyId, + agencyId: a.agencyId, + })) ?? []} + onSubmit={editDataRecord as (data: SelectedRowPayload) => void} + setPatchUpdateModalOpen={setIsUpdateModalOpen as React.Dispatch>} + /> +
    + )} + {isProgressModalOpen && ( + setIsProgressModalOpen(false)} + isOpen={isProgressModalOpen} + footer={ +
    + + +
    + } + > + {isUpdating ? ( +
    +

    {t('datasets.detailedView.dataBeingUpdated')}

    +
    +
    +
    +
    + ) : ( +
    +

    {t('datasets.detailedView.confirmUpdateDatasetDesc')}

    + {updatePayload && ( +
    +

    Items to update: {updatePayload.updatedRowsLength}

    +

    Items to delete: {updatePayload.deletedRowsLength}

    +
    + )} +
    + )} +
    + )} +
    + ); +}; + +export default ViewDataset; \ No newline at end of file diff --git a/GUI/src/services/agencies.ts b/GUI/src/services/agencies.ts new file mode 100644 index 00000000..a2fd3824 --- /dev/null +++ b/GUI/src/services/agencies.ts @@ -0,0 +1,49 @@ +import { integratedAgenciesEndPoints } from 'utils/endpoints'; +import apiDev from './api-dev'; +import { OVERVIEW_PAGE_SIZE } from 'utils/constants'; + +export const fetchAgencies = async ( + pageIndex: number, + sortOption:string, + agencyName: string = 'all' + + ) => { + const [sortBy, sortType] = sortOption.split(' '); + + const { data } = await apiDev.get(integratedAgenciesEndPoints.GET_INTEGRATED_AGENCIES(), { + params:{ + page: pageIndex, + pageSize: OVERVIEW_PAGE_SIZE, + sortBy: sortBy, + sortType: sortType, + agencyName + } + }); + return data?.response ?? []; + }; + + export async function enableAgncy(agencyId: string) { + const { data } = await apiDev.post('global-classifier/agencies/enable', { + "agencyId": agencyId + }); + return data; +} + + export async function disableAgncy(agencyId: string) { + const { data } = await apiDev.post('global-classifier/agencies/disable', { + "agencyId": agencyId + }); + return data; +} + + export async function resync(agencyId: string) { + const { data } = await apiDev.post('global-classifier/agencies/data/resync', { + "agencyId": agencyId + }); + return data; +} + +export const fetchAllAgencies = async () => { + const { data } = await apiDev.get(integratedAgenciesEndPoints.GET_ALL_AGENCIES()); + return data?.response ?? []; + }; \ No newline at end of file diff --git a/GUI/src/services/api-dev.ts b/GUI/src/services/api-dev.ts new file mode 100644 index 00000000..d85bd9a8 --- /dev/null +++ b/GUI/src/services/api-dev.ts @@ -0,0 +1,39 @@ +import axios, { AxiosError } from 'axios'; + +const instance = axios.create({ + baseURL: import.meta.env.REACT_APP_RUUTER_PRIVATE_API_URL, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + withCredentials: true, +}); + +instance.interceptors.response.use( + (axiosResponse) => { + return axiosResponse; + }, + (error: AxiosError) => { + if (error.response?.status === 401) { + window.location.href = import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN + } + return Promise.reject(new Error(error.message)); + } +); + +instance.interceptors.request.use( + (axiosRequest) => { + return axiosRequest; + }, + (error: AxiosError) => { + if (error.response?.status === 401) { + // To be added: handle unauthorized requests + } + if (error.response?.status === 403) { + // To be added: handle unauthorized requests + } + return Promise.reject(new Error(error.message)); + } +); + +export default instance; diff --git a/GUI/src/services/api-external.ts b/GUI/src/services/api-external.ts new file mode 100644 index 00000000..b55ddb35 --- /dev/null +++ b/GUI/src/services/api-external.ts @@ -0,0 +1,36 @@ +import axios, { AxiosError } from 'axios'; + +const instance = axios.create({ + baseURL: import.meta.env.REACT_APP_EXTERNAL_API_URL, + headers: { + Accept: 'application/json', + 'Content-Type': 'multipart/form-data', + }, + withCredentials: true, +}); + +instance.interceptors.response.use( + (axiosResponse) => { + return axiosResponse; + }, + (error: AxiosError) => { + return Promise.reject(new Error(error.message)); + } +); + +instance.interceptors.request.use( + (axiosRequest) => { + return axiosRequest; + }, + (error: AxiosError) => { + if (error.response?.status === 401) { + // To be added: handle unauthorized requests + } + if (error.response?.status === 403) { + // To be added: handle unauthorized requests + } + return Promise.reject(new Error(error.message)); + } +); + +export default instance; diff --git a/GUI/src/services/api-public.ts b/GUI/src/services/api-public.ts new file mode 100644 index 00000000..c986b263 --- /dev/null +++ b/GUI/src/services/api-public.ts @@ -0,0 +1,39 @@ +import axios, { AxiosError } from 'axios'; + +const instance = axios.create({ + baseURL: import.meta.env.REACT_APP_RUUTER_API_URL, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + withCredentials: false, +}); + +instance.interceptors.response.use( + (axiosResponse) => { + return axiosResponse; + }, + (error: AxiosError) => { + if (error.response?.status === 401) { + window.location.href = import.meta.env.REACT_APP_CUSTOMER_SERVICE_LOGIN + } + return Promise.reject(new Error(error.message)); + } +); + +instance.interceptors.request.use( + (axiosRequest) => { + return axiosRequest; + }, + (error: AxiosError) => { + if (error.response?.status === 401) { + // To be added: handle unauthorized requests + } + if (error.response?.status === 403) { + // To be added: handle unauthorized requests + } + return Promise.reject(new Error(error.message)); + } +); + +export default instance; diff --git a/GUI/src/services/api.ts b/GUI/src/services/api.ts new file mode 100644 index 00000000..3ce245ae --- /dev/null +++ b/GUI/src/services/api.ts @@ -0,0 +1,36 @@ +import axios, { AxiosError } from 'axios'; + +const instance = axios.create({ + baseURL: import.meta.env.BASE_URL, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + withCredentials: true, +}); + +instance.interceptors.response.use( + (axiosResponse) => { + return axiosResponse; + }, + (error: AxiosError) => { + return Promise.reject(new Error(error.message)); + } +); + +instance.interceptors.request.use( + (axiosRequest) => { + return axiosRequest; + }, + (error: AxiosError) => { + if (error.response?.status === 401) { + // To be added: handle unauthorized requests + } + if (error.response?.status === 403) { + // To be added: handle unauthorized requests + } + return Promise.reject(new Error(error.message)); + } +); + +export default instance; diff --git a/GUI/src/services/datamodels.ts b/GUI/src/services/datamodels.ts new file mode 100644 index 00000000..c6a8843d --- /dev/null +++ b/GUI/src/services/datamodels.ts @@ -0,0 +1,120 @@ +import { dataModelsEndpoints, testModelsEndpoints } from 'utils/endpoints'; +import apiDev from './api-dev'; +import apiPublic from './api-public'; +import { ClassifyTestModalPayloadType, ClassifyTestModalResponseType } from 'types/testModelTypes'; +import { OVERVIEW_PAGE_SIZE } from 'utils/constants'; + +export async function getDataModelsOverview( + pageNum: number, + modelStatus: string, + trainingStatus: string, + deploymentEnvironment: string, + sort: string, +) { + const { data } = await apiDev.get(dataModelsEndpoints.GET_OVERVIEW(), { + params: { + page: pageNum, + modelStatus, + trainingStatus, + deploymentEnvironment, + sortBy: sort?.split(" ")?.[0], + sortType: sort?.split(" ")?.[1], + pageSize: OVERVIEW_PAGE_SIZE, + }, + }); + return data?.response ?? []; +} + +export async function getDeploymentEnvironments() { + const { data } = await apiDev.get(dataModelsEndpoints.GET_DEPLOYMENT_ENVIRONMENTS()); + return data?.response ?? []; +} + +export async function getProductionDataModel() { + const { data } = await apiDev.get(dataModelsEndpoints.GET_PRODUCTION_DATA_MODEL()); + return data?.response?.[0] ?? null; +} + +export async function getDataModelMetadata( + modelId: number | string, +) { + const { data } = await apiDev.get(dataModelsEndpoints.GET_MODEL_METADATA(), { + params: { + modelId + }, + }); + return data?.response?.[0] ?? []; +} + +export async function createDataModel(payload: { + modelName: string; + deploymentEnv: string; + baseModels: string[]; + connectedDsId: string | number; + connectedDsMajorVersion: string | number; + connectedDsMinorVersion: string | number; +}) { + const { data } = await apiDev.post(dataModelsEndpoints.CREATE_MODEL(), payload); + return data?.response ?? {}; +} + +export async function configureDataModel(payload: { + modelGroupKey: string; + modelName: string; + deploymentEnv: string; + baseModels: string[]; + connectedDsId: string | number; + connectedDsMajorVersion: string | number; + connectedDsMinorVersion: string | number; + updateType: string; +},) { + let endpoint = ""; + if (payload.updateType === 'major') { + endpoint = dataModelsEndpoints.CREATE_MAJOR_VERSION(); + } else if (payload.updateType === 'minor') { + endpoint = dataModelsEndpoints.CREATE_MINOR_VERSION(); + } + const { data } = await apiDev.post(endpoint, payload); + return data?.response ?? {}; +} + + +export async function deployDataModel(payload: { + modelId: string | number; + currentEnv: string; + targetEnv: string; +},) { + const { data } = await apiPublic.post(dataModelsEndpoints.DEPLOY_MODEL(), payload); + return data?.response ?? {}; +} + +export async function deleteDataModel(modelId: number | string | null) { + const { data } = await apiDev.post(dataModelsEndpoints.DELETE_MODEL(), { + modelId, + }); + return data?.response ?? {}; +} + +export async function getAllModelVersions() { + const { data } = await apiDev.get(dataModelsEndpoints.GET_ALL_DATAMODELS_VERSIONS()); + return data?.response ?? []; +} + +export async function loadModel(modelId: number | string | null) { + const { data } = await apiDev.post(dataModelsEndpoints.LOAD_MODEL(), { + modelId, + }); + return data?.response ?? []; +} + +export async function classify(data: ClassifyTestModalPayloadType) { + const response = await apiDev.post( + testModelsEndpoints.CLASSIFY_TEST_MODELS(), + { modelId: data.modelId, text: data.text }, + ); + return response?.data?.response as ClassifyTestModalResponseType ?? []; +} +export async function getDataModelsProgress() { + const { data } = await apiDev.get(dataModelsEndpoints.GET_DATA_MODEL_PROGRESS()); + return data?.response?.data; +} \ No newline at end of file diff --git a/GUI/src/services/datasets.ts b/GUI/src/services/datasets.ts new file mode 100644 index 00000000..1e4d668b --- /dev/null +++ b/GUI/src/services/datasets.ts @@ -0,0 +1,72 @@ +import { dataModelsEndpoints, datasetsEndpoints } from 'utils/endpoints'; +import apiDev from './api-dev'; +import { DATASET_PAGE_SIZE, OVERVIEW_PAGE_SIZE } from 'utils/constants'; + +export async function getDatasetsOverview( + pageNum: number, + sort: string +) { + const { data } = await apiDev.get(datasetsEndpoints.GET_OVERVIEW(), { + params: { + page: pageNum, + generationStatus: "all", + sortBy: sort?.split(" ")?.[0], + sortType: sort?.split(" ")?.[1], + pageSize: OVERVIEW_PAGE_SIZE, + }, + }); + return data?.response ?? []; +} + +export async function getDatasetMetadata( + datasetId: number | string) { + const { data } = await apiDev.get(datasetsEndpoints.GET_METADATA(), { + params: { + datasetId + }, + }); + return data?.response?.response?.[0] ?? []; +} + +export async function getDatasetData( + datasetVersionId: number | string, + pageNum?: number, + clientId?: string, + pageSize?: number + +) { + const { data } = await apiDev.get(datasetsEndpoints.GET_DATASETS_DATA(), { + params: { + datasetVersionId, + pageNum: pageNum ?? 1, + pageSize: pageSize ?? DATASET_PAGE_SIZE, + clientId: clientId ?? "all", + }, + }); + return data?.response ?? []; +} + +export async function getAllDatasetVersions() { + const { data } = await apiDev.get(datasetsEndpoints.GET_ALL_DATASET_VERSIONS()); + return data?.response ?? []; +} + +export async function getDataGenerationProgress() { + const { data } = await apiDev.get(datasetsEndpoints.GET_DATA_GENERATION_PROGRESS()); + return data?.response?.data; +} + +export async function updateDataset(payload: { + updatedDataItems: any[]; + deletedRows: (string | number)[]; + updatedRowsLength: number; + deletedRowsLength: number; +}) { + const { data } = await apiDev.post(datasetsEndpoints.UPDATE_DATASET(), payload); + return data?.response ?? {}; +} + +export async function deleteDataset(datasetVersionId: number | string) { + const { data } = await apiDev.post(datasetsEndpoints.DELETE_DATASET(), { datasetVersionId }); + return data?.response ?? {}; +} \ No newline at end of file diff --git a/GUI/src/services/sse-service.ts b/GUI/src/services/sse-service.ts new file mode 100644 index 00000000..2d1f24f4 --- /dev/null +++ b/GUI/src/services/sse-service.ts @@ -0,0 +1,30 @@ +const notificationNodeUrl = import.meta.env.REACT_APP_NOTIFICATION_NODE_URL; + +const sse = (url: string,module:string, onMessage: (data: T) => void): EventSource => { + if (!notificationNodeUrl) { + console.error('Notification node url is not defined'); + throw new Error('Notification node url is not defined'); + } + const eventSource = new EventSource( + `${notificationNodeUrl}/sse/${module}/notifications${url}` + ); + + eventSource.onmessage = (event: MessageEvent) => { + if (event.data !== undefined && event.data !== 'undefined') { + const response = JSON.parse(event.data); + if (response !== undefined) { + onMessage(response as T); + } + } + }; + + eventSource.onopen = () => { + console.log('SSE connection Opened'); + }; + + eventSource.onerror = () => {}; + + return eventSource; +}; + +export default sse; diff --git a/GUI/src/services/users.ts b/GUI/src/services/users.ts new file mode 100644 index 00000000..2ecb18b5 --- /dev/null +++ b/GUI/src/services/users.ts @@ -0,0 +1,48 @@ +import apiDev from './api-dev'; +import { User, UserDTO } from 'types/user'; + +export async function createUser(userData: UserDTO) { + const authorities = userData.authorities.map((e) => (e as any).value).filter(item => item); + const fullName = userData.fullName?.trim(); + const nameLength = fullName?.split(" ")?.length; + const { data } = await apiDev.post('global-classifier/accounts/add', { + "firstName": fullName?.split(' ').slice(0, 1).join(' ') ?? '', + "lastName": fullName?.split(' ').slice(1, nameLength).join(' ') ?? '', + "userIdCode": userData.useridcode, + "displayName": userData.fullName, + "csaTitle": userData.csaTitle, + "csa_email": userData.csaEmail, + "roles": authorities.length === 0 ? Object.values(userData.authorities) : authorities + }); + return data; +} + +export async function checkIfUserExists(userData: UserDTO) { + const { data } = await apiDev.post('global-classifier/accounts/exists', { + "userIdCode": userData.useridcode + }); + return data; +} + +export async function editUser(id: string | number, userData: UserDTO) { + const authorities = userData.authorities.map((e: any) => e.value).filter(item => item); + const fullName = userData.fullName?.trim(); + const nameLength = fullName?.split(" ")?.length; + const { data } = await apiDev.post('global-classifier/accounts/edit', { + "firstName": fullName?.split(' ').slice(0, 1).join(' ') ?? '', + "lastName": fullName?.split(' ').slice(1, nameLength).join(' ') ?? '', + "userIdCode": id, + "displayName": userData.fullName, + "csaTitle": userData.csaTitle, + "csa_email": userData.csaEmail, + "roles": authorities.length === 0 ? Object.values(userData.authorities) : authorities + }); + return data; +} + +export async function deleteUser(id: string | number) { + const { data } = await apiDev.post('global-classifier/accounts/delete', { + "userIdCode": id, + }); + return data; +} diff --git a/GUI/src/static/icons/link-external-blue.svg b/GUI/src/static/icons/link-external-blue.svg new file mode 100644 index 00000000..9bd1d1fb --- /dev/null +++ b/GUI/src/static/icons/link-external-blue.svg @@ -0,0 +1,8 @@ + diff --git a/GUI/src/static/icons/link-external-white.svg b/GUI/src/static/icons/link-external-white.svg new file mode 100644 index 00000000..a391216d --- /dev/null +++ b/GUI/src/static/icons/link-external-white.svg @@ -0,0 +1 @@ + diff --git a/GUI/src/store/index.ts b/GUI/src/store/index.ts new file mode 100644 index 00000000..564d3215 --- /dev/null +++ b/GUI/src/store/index.ts @@ -0,0 +1,16 @@ +import { create } from 'zustand'; +import { UserInfo } from 'types/userInfo'; + +interface StoreState { + userInfo: UserInfo | null; + userId: string; + setUserInfo: (info: UserInfo) => void; +} + +const useStore = create((set) => ({ + userInfo: null, + userId: '', + setUserInfo: (data) => set({ userInfo: data, userId: data?.userIdCode || '' }), +})); + +export default useStore; diff --git a/GUI/src/styles/components/_vertical-tabs.scss b/GUI/src/styles/components/_vertical-tabs.scss new file mode 100644 index 00000000..dd48d096 --- /dev/null +++ b/GUI/src/styles/components/_vertical-tabs.scss @@ -0,0 +1,119 @@ +.vertical-tabs { + display: flex; + border-radius: 4px; + border: 1px solid get-color(black-coral-2); + background-color: get-color(white); + + &__list { + display: flex; + flex-direction: column; + width: 288px; + flex-shrink: 0; + background-color: get-color(black-coral-0); + border-radius: 4px 0 0 4px; + border-right: 1px solid get-color(black-coral-7); + } + + &__list-search { + padding: get-spacing(haapsalu); + } + + &__body { + flex: 1; + display: flex; + flex-direction: column; + + &[data-state="inactive"] { + display: none; + } + } + + &__body-placeholder { + flex: 1; + position: relative; + + p { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } + + &__content-header { + padding: get-spacing(haapsalu); + background-color: get-color(extra-light); + border-bottom: 1px solid get-color(black-coral-2); + border-top-right-radius: 4px; + } + + &__content-footer { + display: flex; + justify-content: flex-end; + padding: get-spacing(haapsalu); + border-top: 1px solid get-color(black-coral-2); + border-bottom-right-radius: 4px; + } + + &__content { + flex: 1; + padding: get-spacing(haapsalu); + } + + &__trigger { + cursor: pointer; + padding: get-spacing(haapsalu); + font-size: $veera-font-size-100; + line-height: 24px; + color: get-color(sapphire-blue-20); + + &.active { + background-color: #FCEEEE; + box-shadow: inset 0 0 0 1px get-color(jasper-10); + animation: blink-animation 0.5s steps(100, start) 3; + -webkit-animation: blink-animation 0.5s steps(100, start) 3; + @keyframes blink-animation { + to { + background: get-color(jasper-8); + } + } + @-webkit-keyframes blink-animation { + to { + background: #FCEEEE; + } + } + } + + &[aria-selected=true] { + color: get-color(sapphire-blue-10); + background-color: get-color(white); + + &.active { + background-color: get-color(white); + box-shadow: none; + } + } + + &:first-child { + border-top-left-radius: 4px; + } + } + + &__group-header { + text-transform: uppercase; + font-weight: 700; + font-size: $veera-font-size-100; + line-height: 24px; + padding: 16px 16px 4px; + color: get-color(black-coral-20); + border-bottom: 1px solid get-color(black-coral-2); + } + + &__sub-group-header { + font-weight: 700; + line-height: 24px; + padding: 16px 16px 4px; + color: get-color(black-coral-20); + border-bottom: 1px solid get-color(black-coral-2); + } +} diff --git a/GUI/src/styles/generic/_base.scss b/GUI/src/styles/generic/_base.scss new file mode 100644 index 00000000..429c49ed --- /dev/null +++ b/GUI/src/styles/generic/_base.scss @@ -0,0 +1,169 @@ +html, body, #root, #overlay-root { + height: 100%; + overflow: hidden; +} + +html { + scroll-behavior: smooth; + @media screen and (prefers-reduced-motion: reduce) { + & { + scroll-behavior: auto; + } + } +} + +body { + font-family: $font-family-base; + font-size: $veera-font-size-100; + line-height: $veera-line-height-500; + background-color: get-color(black-coral-0); + + @include veera-breakpoint-down(sm) { + background-color: get-color(black-coral-0); + } + +} + +.container { + padding: 0px; +} + +.title_container{ + display: flex; + justify-content: space-between; + align-items: center; + margin: 0px 0px 30px 0px; +} + +.bordered-card{ + background-color: rgb(252, 237, 208); + border-radius: 10px; + padding: 10px 20px; +} + +.title { + font-size: 1.5rem; + color: #000; + font-weight: 300; +} + +.title-sm { + font-size: 1rem; + color: #000; + font-weight: 300; + display: flex; + justify-content: space-between; + align-items: center; + margin: 20px 0px; +} + +.title-m { + font-size: 1.3rem; + color: #000; + font-weight: 300; + display: flex; + justify-content: space-between; + align-items: center; + margin: 15px 0px; +} + +.warning{ + font-size: 13px; + color: rgb(223, 116, 2); + margin-top: 20px; +} + +.flex-between { + display: flex; + justify-content: space-between; +} + +.flex-grid { + display: flex; + gap: 10px; +} + +.text-center { + text-align: center; +} + +.error { + color: rgb(222, 10, 10); + } + +a, +input, +select, +textarea, +button { + font-family: inherit; + transition: background-color 0.25s, color 0.25s, border-color 0.25s, box-shadow 0.25s; +} + +p { +margin-bottom: 10px; +} + +a { + color: get-color(sapphire-blue-10); + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} + +.link { + color: get-color(sapphire-blue-10); + &:hover { + text-decoration: underline; + cursor: pointer; + } +} + +img { + max-width: 100%; + height: auto; +} + +button { + font-family: inherit; +} + +h1, .h1 { + font-size: $veera-font-size-350; + font-weight: $veera-font-weight-alpha; +} + +h2, .h2 { + font-size: $veera-font-size-300; +} + +h3, .h3 { + font-size: $veera-font-size-250; +} + +h4, .h4 { + font-size: $veera-font-size-220; +} + +h5, .h5 { + font-size: $veera-font-size-200; +} + +h6, .h6 { + font-size: $veera-font-size-100; + font-weight: $veera-font-weight-delta; +} + +.justify-end{ + justify-content: end; +} + +.text-18{ + font-size: 18px; +} + +.text-20{ + font-size: 20px; +} \ No newline at end of file diff --git a/GUI/src/styles/generic/_fonts.scss b/GUI/src/styles/generic/_fonts.scss new file mode 100644 index 00000000..3602e250 --- /dev/null +++ b/GUI/src/styles/generic/_fonts.scss @@ -0,0 +1,15 @@ +@use '@fontsource/roboto/scss/mixins' as Roboto; + +$subsets: (latin, latin-ext); + +@include Roboto.fontFace($weight: 300, $unicodeMap: $subsets); + +@include Roboto.fontFace($weight: 400, $unicodeMap: $subsets); + +@include Roboto.fontFace($weight: 400, $style: italic, $unicodeMap: $subsets); + +@include Roboto.fontFace($weight: 500, $unicodeMap: $subsets); + +@include Roboto.fontFace($weight: 700, $unicodeMap: $subsets); + +@include Roboto.fontFace($weight: 700, $style: italic, $unicodeMap: $subsets); diff --git a/GUI/src/styles/generic/_reset.scss b/GUI/src/styles/generic/_reset.scss new file mode 100644 index 00000000..def28896 --- /dev/null +++ b/GUI/src/styles/generic/_reset.scss @@ -0,0 +1,145 @@ +html, +body, +div, +span, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +abbr, +code, +em, +img, +small, +strong, +sub, +sup, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +footer, +header, +nav, +section, +time, +audio, +video { + padding: 0; + border: 0; + margin: 0; + background: transparent; + font-size: 100%; + font-weight: inherit; + vertical-align: baseline; +} + +article, +aside, +figure, +footer, +header, +nav, +section { + display: block; +} + +html { + box-sizing: border-box; + overflow-y: scroll; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +img, +object { + max-width: 100%; +} + +ul { + list-style: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +th { + font-weight: bold; + vertical-align: bottom; +} + +td { + font-weight: normal; + vertical-align: top; +} + +input, +select { + vertical-align: middle; +} + +input[type='radio'] { + vertical-align: text-bottom; +} + +input[type='checkbox'] { + vertical-align: bottom; +} + +strong { + font-weight: bold; +} + +label, +input[type='file'] { + cursor: pointer; +} + +input, +select, +textarea { + border: 0; + margin: 0; +} + +button, +input[type='button'], +input[type='submit'] { + padding: 0; + border: 0; + border-radius: 0; + margin: 0; + background: transparent; + cursor: pointer; + text-align: left; +} + +button::-moz-focus-inner { + padding: 0; + border: 0; +} diff --git a/GUI/src/styles/main.scss b/GUI/src/styles/main.scss new file mode 100644 index 00000000..dbc5c667 --- /dev/null +++ b/GUI/src/styles/main.scss @@ -0,0 +1,21 @@ +// Settings - Sass variables and mixins +@import 'settings/variables/colors'; +@import 'settings/variables/typography'; +@import 'settings/variables/breakpoints'; +@import 'settings/variables/grid'; +@import 'settings/variables/spacing'; +@import 'settings/variables/other'; +@import 'settings/mixins'; +@import 'settings/utility-classes'; + +// Tools - Sass functions +@import 'tools/color'; +@import 'tools/spacing'; + +// Generic - global CSS styling and CSS at-rules +@import 'generic/reset'; +@import 'generic/fonts'; +@import 'generic/base'; + +// Components +@import 'components/vertical-tabs'; diff --git a/GUI/src/styles/settings/_mixins.scss b/GUI/src/styles/settings/_mixins.scss new file mode 100644 index 00000000..d58dea85 --- /dev/null +++ b/GUI/src/styles/settings/_mixins.scss @@ -0,0 +1,23 @@ +@mixin veera-screenreader-text { + position: absolute; + overflow: hidden; + clip: rect(0 0 0 0); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + border: none; + white-space: nowrap; +} + +@mixin veera-breakpoint-down($breakpoint) { + @media (max-width: #{map-get($veera-grid-breakpoints, $breakpoint)}) { + @content; + } +} + +@mixin veera-breakpoint-up($breakpoint) { + @media (min-width: #{map-get($veera-grid-breakpoints, $breakpoint) + 1px}) { + @content; + } +} diff --git a/GUI/src/styles/settings/_utility-classes.scss b/GUI/src/styles/settings/_utility-classes.scss new file mode 100644 index 00000000..e0cc532d --- /dev/null +++ b/GUI/src/styles/settings/_utility-classes.scss @@ -0,0 +1,3 @@ +.veera-screenreader-text { + @include veera-screenreader-text; +} diff --git a/GUI/src/styles/settings/variables/_breakpoints.scss b/GUI/src/styles/settings/variables/_breakpoints.scss new file mode 100644 index 00000000..bea2970d --- /dev/null +++ b/GUI/src/styles/settings/variables/_breakpoints.scss @@ -0,0 +1,9 @@ +// Grid breakpoints +$veera-grid-breakpoints: ( + xs: 0, + sm: 601px, + md: 801px, + lg: 1025px, + xl: 1281px, + xxl: 1601px, +); diff --git a/GUI/src/styles/settings/variables/_colors.scss b/GUI/src/styles/settings/variables/_colors.scss new file mode 100644 index 00000000..0b7797cc --- /dev/null +++ b/GUI/src/styles/settings/variables/_colors.scss @@ -0,0 +1,155 @@ +// Color codes +$veera-colors: ( + // Black coral + black-coral-0: #f0f0f2, + black-coral-1: #e1e2e5, + black-coral-2: #d2d3d8, + black-coral-3: #c4c5cb, + black-coral-4: #b5b6be, + black-coral-5: #a6a8b1, + black-coral-6: #9799a4, + black-coral-7: #898b97, + black-coral-8: #7a7c8a, + black-coral-9: #6b6e7d, + black-coral-10: #5d6071, + black-coral-11: #555867, + black-coral-12: #4d4f5d, + black-coral-13: #444653, + black-coral-14: #3c3e48, + black-coral-15: #33353e, + black-coral-16: #2b2c34, + black-coral-17: #22232a, + black-coral-18: #1a1b1f, + black-coral-19: #111215, + black-coral-20: #09090b, + + // Dark tangerine + dark-tangerine-0: #fff8e9, + dark-tangerine-1: #fff1d3, + dark-tangerine-2: #ffeabe, + dark-tangerine-3: #ffe4a8, + dark-tangerine-4: #ffdd92, + dark-tangerine-5: #ffd67d, + dark-tangerine-6: #ffcf67, + dark-tangerine-7: #ffc951, + dark-tangerine-8: #ffc23c, + dark-tangerine-9: #ffbb26, + dark-tangerine-10: #ffb511, + dark-tangerine-11: #e8a510, + dark-tangerine-12: #d1950e, + dark-tangerine-13: #ba840d, + dark-tangerine-14: #a3740b, + dark-tangerine-15: #8c630a, + dark-tangerine-16: #745308, + dark-tangerine-17: #5d4207, + dark-tangerine-18: #463205, + dark-tangerine-19: #2f2104, + dark-tangerine-20: #181102, + + // Jasper + jasper-0: #fbeded, + jasper-1: #f7dbdb, + jasper-2: #f4caca, + jasper-3: #f0b8b8, + jasper-4: #eca7a7, + jasper-5: #e99595, + jasper-6: #e58484, + jasper-7: #e17272, + jasper-8: #de6161, + jasper-9: #da4f4f, + jasper-10: #d73e3e, + jasper-11: #c43939, + jasper-12: #b03333, + jasper-13: #9d2e2e, + jasper-14: #892828, + jasper-15: #762222, + jasper-16: #621d1d, + jasper-17: #4f1717, + jasper-18: #3b1111, + jasper-19: #280c0c, + jasper-20: #140606, + + // Orange + orange-0: #fff3e7, + orange-1: #ffe7d0, + orange-2: #ffdcb9, + orange-3: #ffd0a2, + orange-4: #ffc58b, + orange-5: #ffb973, + orange-6: #ffae5c, + orange-7: #ffa245, + orange-8: #ff972e, + orange-9: #ff8b17, + orange-10: #ff8000, + orange-11: #e87500, + orange-12: #d16900, + orange-13: #ba5e00, + orange-14: #a35200, + orange-15: #8c4600, + orange-16: #743b00, + orange-17: #5d2f00, + orange-18: #462300, + orange-19: #2f1800, + orange-20: #180c00, + + // Sapphire blue + sapphire-blue-0: #e7f0f6, + sapphire-blue-1: #d0e1ee, + sapphire-blue-2: #B9D2E5, + sapphire-blue-3: #a2c3dd, + sapphire-blue-4: #8bb4d5, + sapphire-blue-5: #73a5cc, + sapphire-blue-6: #5c96c4, + sapphire-blue-7: #4587bc, + sapphire-blue-8: #2e78b3, + sapphire-blue-9: #1769ab, + sapphire-blue-10: #005aa3, + sapphire-blue-11: #005295, + sapphire-blue-12: #004a86, + sapphire-blue-13: #004277, + sapphire-blue-14: #003a68, + sapphire-blue-15: #003259, + sapphire-blue-16: #00294b, + sapphire-blue-17: #00213c, + sapphire-blue-18: #00192d, + sapphire-blue-19: #00111e, + sapphire-blue-20: #00090f, + + // Sea green + sea-green-0: #ecf4ef, + sea-green-1: #d9e9df, + sea-green-2: #c6ded0, + sea-green-3: #b3d3c0, + sea-green-4: #a0c8b0, + sea-green-5: #8ebda1, + sea-green-6: #7bb291, + sea-green-7: #68a781, + sea-green-8: #559c72, + sea-green-9: #429162, + sea-green-10: #308653, + sea-green-11: #2c7a4c, + sea-green-12: #286e44, + sea-green-13: #23623d, + sea-green-14: #1f5635, + sea-green-15: #1b4a2e, + sea-green-16: #163d26, + sea-green-17: #12311f, + sea-green-18: #0e2517, + sea-green-19: #091910, + sea-green-20: #050d08, + + // Other + white: #ffffff, + black: #000000, + extra-light: #f9f9f9 +); + +// CSS variables +:root { + @each $name, $color in $veera-colors { + --veera-color-#{'' + $name}: #{$color}; + } + @each $name, $color in $veera-colors { + --veera-color-rgb-#{'' + $name}: #{red($color) green($color) blue($color)}; + } +} diff --git a/GUI/src/styles/settings/variables/_grid.scss b/GUI/src/styles/settings/variables/_grid.scss new file mode 100644 index 00000000..b52bf253 --- /dev/null +++ b/GUI/src/styles/settings/variables/_grid.scss @@ -0,0 +1,3 @@ +// Grid settings +$veera-grid-columns: 12; +$veera-grid-gutter-width: 16px; diff --git a/GUI/src/styles/settings/variables/_other.scss b/GUI/src/styles/settings/variables/_other.scss new file mode 100644 index 00000000..8d5cea3c --- /dev/null +++ b/GUI/src/styles/settings/variables/_other.scss @@ -0,0 +1,16 @@ +// Border radii +$veera-radius-pill: 999px; +$veera-radius-l: 8px; +$veera-radius-m: 6px; +$veera-radius-s: 4px; +$veera-radius-xs: 2px; + +// Border widths +$veera-border-width: 2px; + +// Shadows +$veera-shadow-beta-blur: 15px; + +$veera-shadow-alpha: 0 1px 5px 0 rgba(var(--veera-color-rgb-black) / .15); +$veera-shadow-beta: 0 4px $veera-shadow-beta-blur 0 rgba(var(--veera-color-rgb-black) / .15); +$veera-shadow-gamma: 0 0 20px 0 rgba(var(--veera-color-rgb-sapphire-blue-16) / .1); diff --git a/GUI/src/styles/settings/variables/_spacing.scss b/GUI/src/styles/settings/variables/_spacing.scss new file mode 100644 index 00000000..42e1c7ee --- /dev/null +++ b/GUI/src/styles/settings/variables/_spacing.scss @@ -0,0 +1,21 @@ +$veera-spacing-unit: 4px; + +$veera-spacing-patterns: ( + 'loksa': $veera-spacing-unit, + 'paldiski': 8px, + 'rapla': 10px, + 'elva': 12px, + 'haapsalu': 16px, + 'valga': 20px, + 'kuressaare': 24px, + 'viljandi': 32px, + 'parnu': 48px, + 'narva': 60px, +); + +:root { + --veera-spacing-unit: #{$veera-spacing-unit}; + @each $name, $size in $veera-spacing-patterns { + --veera-spacing-#{'' + $name}: #{$size}; + } +} diff --git a/GUI/src/styles/settings/variables/_typography.scss b/GUI/src/styles/settings/variables/_typography.scss new file mode 100644 index 00000000..a4f9eb5c --- /dev/null +++ b/GUI/src/styles/settings/variables/_typography.scss @@ -0,0 +1,22 @@ +$font-family-base: Roboto, Arial, Helvetica, sans-serif; + +// Font sizes +$veera-font-size-50: 10px; +$veera-font-size-70: 12px; +$veera-font-size-80: 14px; +$veera-font-size-100: 16px; +$veera-font-size-200: 18px; +$veera-font-size-220: 20px; +$veera-font-size-250: 24px; +$veera-font-size-300: 28px; +$veera-font-size-350: 32px; +$veera-font-size-400: 36px; +$veera-font-size-500: 48px; + +$veera-line-height-100: 1; +$veera-line-height-500: 1.5; + +$veera-font-weight-alpha: 300; +$veera-font-weight-beta: 400; +$veera-font-weight-gamma: 500; +$veera-font-weight-delta: 700; diff --git a/GUI/src/styles/tools/_color.scss b/GUI/src/styles/tools/_color.scss new file mode 100644 index 00000000..d1f89647 --- /dev/null +++ b/GUI/src/styles/tools/_color.scss @@ -0,0 +1,4 @@ +// Returns variable as a CSS variable +@function get-color($color-name) { + @return var(--veera-color-#{'' + $color-name}); +} diff --git a/GUI/src/styles/tools/_spacing.scss b/GUI/src/styles/tools/_spacing.scss new file mode 100644 index 00000000..20ffb38e --- /dev/null +++ b/GUI/src/styles/tools/_spacing.scss @@ -0,0 +1,4 @@ + // Returns variable as a CSS variable +@function get-spacing($spacing-name) { + @return var(--veera-spacing-#{$spacing-name}); +} diff --git a/GUI/src/types/agencies.ts b/GUI/src/types/agencies.ts new file mode 100644 index 00000000..ffe8777a --- /dev/null +++ b/GUI/src/types/agencies.ts @@ -0,0 +1,16 @@ +export interface Agency { + id?: string; + agencyId: string; + agencyName: string; + groupKey: string; + isLatest: boolean; + deploymentStatus: "undeployed" | "deployed" | "deploying" | "failed"; + isEnabled: boolean; + agencyDataHash: string; + enableAllowed: boolean; + lastModelTrained: string; + lastUpdatedTimestamp: Date | null |undefined; + lastTrainedTimestamp: Date | null |undefined; + syncStatus: "Unavailable_in_CKB" | "Available_in_CKB" | "Syncing" | "Failed"; + createdAt: string; + } \ No newline at end of file diff --git a/GUI/src/types/authorities.ts b/GUI/src/types/authorities.ts new file mode 100644 index 00000000..50afc258 --- /dev/null +++ b/GUI/src/types/authorities.ts @@ -0,0 +1,8 @@ +export enum AUTHORITY { + ADMINISTRATOR = 'ROLE_ADMINISTRATOR', + SERVICE_MANAGER = 'ROLE_SERVICE_MANAGER', + CUSTOMER_SUPPORT_AGENT = 'ROLE_CUSTOMER_SUPPORT_AGENT', + CHATBOT_TRAINER = 'ROLE_CHATBOT_TRAINER', + ANALYST = 'ROLE_ANALYST', + UNAUTHENTICATED = 'ROLE_UNAUTHENTICATED', +} diff --git a/GUI/src/types/common.ts b/GUI/src/types/common.ts new file mode 100644 index 00000000..dce11644 --- /dev/null +++ b/GUI/src/types/common.ts @@ -0,0 +1,6 @@ +export interface Columns { + accessorKey?: string; + header?: string; + id?: string; + meta?: {} +} diff --git a/GUI/src/types/dataModels.ts b/GUI/src/types/dataModels.ts new file mode 100644 index 00000000..c55b8865 --- /dev/null +++ b/GUI/src/types/dataModels.ts @@ -0,0 +1,165 @@ +export type DataModel = { + modelId: number; + modelName: string; + dgName?: string; + datasetId: string | number; + baseModels: string[]; + deploymentEnvironment: string; + version?: string; + trainingResults?: TrainingResultsResponse | null; +}; + +export type TrainingProgressData = { + id: string; + modelName: string; + majorVersion: number; + minorVersion: number; + latest: boolean; + trainingStatus: string; + progressPercentage: number; + trainingMessage?:string; +}; + +export type SSEEventData = { + sessionId: string; + trainingStatus: string; + progressPercentage: number; +}; + +export type UpdatedDataModelPayload = { + modelId: number; + connectedDgId: string | null | undefined; + deploymentEnv: string | null | undefined; + baseModels: string | null | undefined; + maturityLabel: string | null | undefined; + updateType: string | undefined; +}; + +export type CreateDataModelPayload = { + modelName: string | undefined; + dgId: string | number | undefined; + baseModels: string[] | undefined; + deploymentPlatform: string | undefined; + maturityLabel: string | undefined; +}; + +export type FilterData = { + modelNames: string[]; + modelVersions: string[]; + deploymentsEnvs: string[]; + datasetGroups: Array<{ id: number; name: string }>; + trainingStatuses: string[]; + maturityLabels: string[]; +}; + +export type DataModelResponse = { + modelId: number | string; + modelName: string; + major: number; + minor: number; + latest: boolean; + connectedDsMajorVersion?: string; + connectedDsMinorVersion?: string; + dataModelName: string; + lastTrained: string; + trainingStatus: string; + deploymentEnv: string; + modelStatus: string; + trainingResults?: string | null; +}; + +export type ClassMetrics = { + f1?: number; + recall?: number; + accuracy?: number; + precision?: number; +}; + +export type ModelPerformance = { + model_type?: string; + class_metrics: { + [className: string]: ClassMetrics; + }; +}; + +export type ModelResultsProps = { + models?: ModelPerformance[]; +}; + +export type DataModelsFilters = { + modelName: string; + modelStatus: string; + trainingStatus: string; + deploymentEnvironment: string; + sort: 'createdAt desc' | 'createdAt asc' | 'modelName asc' | 'modelName desc'; +}; + +export type ErrorsType = { + modelName?: string; + dgName?: string; + deploymentEnvironment?: string; + baseModels?: string; + datasetId?: string; +}; + +export type TrainingResults = { + best_model_info: { + model_type: string; + class_metrics: { + [className: string]: { + f1: number; + recall: number; + accuracy: number; + precision: number; + }; + }; + overall_metrics: { + recall: number; + roc_auc: number; + accuracy: number; + f1_score: number; + precision: number; + }; + }; + training_summary: { + model_id: number; + timestamp: string; + dataset_id: string; + failed_models: number; + best_overall_f1: number; + successful_models: number; + best_overall_model: string; + total_models_attempted: number; + }; + models_performance: Array<{ + status: string; + best_epoch: number; + model_name: string; + model_type: string; + best_val_f1: number; + class_metrics: { + [className: string]: { + f1: number; + recall: number; + accuracy: number; + precision: number; + }; + }; + num_parameters: number; + overall_metrics: { + f1: number; + recall: number; + roc_auc: number; + accuracy: number; + precision: number; + }; + training_time_seconds: number; + inference_time_seconds: number; + }>; +}; + +export type TrainingResultsResponse = { + type: "jsonb"; + value: string; + null: boolean; +}; \ No newline at end of file diff --git a/GUI/src/types/dataModelsEnums.ts b/GUI/src/types/dataModelsEnums.ts new file mode 100644 index 00000000..d9f51b0a --- /dev/null +++ b/GUI/src/types/dataModelsEnums.ts @@ -0,0 +1,24 @@ +export enum TrainingStatus { + NOT_TRAINED = 'not trained', + TRAINING_INPROGRESS = 'training in-progress', + TRAINED = 'trained', + RETRAINING_NEEDED = 'retraining needed', + UNTRAINABLE = 'untrainable', +} + +export enum DeploymentEnvironments { + PRODUCTION = 'production', + UNDEPLOYED = 'undeployed', + TESTING = 'testing', +} + +export enum UpdateType { + MAJOR = 'major', + MINOR = 'minor', + MATURITY_LABEL = 'maturityLabel', +} + +export enum TrainingSessionsStatuses { + TRAINING_SUCCESS_STATUS = 'Model Trained And Deployed', + TRAINING_FAILED_STATUS = 'Training Failed' +} \ No newline at end of file diff --git a/GUI/src/types/datasets.ts b/GUI/src/types/datasets.ts new file mode 100644 index 00000000..47d12ca3 --- /dev/null +++ b/GUI/src/types/datasets.ts @@ -0,0 +1,67 @@ + + +export interface LinkedModel { + modelId: number; + modelName: string; + modelVersion: string; + trainingTimestamp: number; +} + +export interface Operation { + dgId: number | undefined; + operationType: 'enable' | 'disable'; +} + +export type Dataset = { + id: number |string; + major: number; + minor: number; + createdAt: string; + generationStatus: string; + lastModelTrained: string; + lastTrained: string; + totalPages: number; +}; + +export type MinorPayLoad = { + dgId: number; + s3FilePath: any; +}; + +type DataPayload = Record; + +export type DatasetDetails = { + dgId: number; + numPages: number; + dataPayload: DataPayload[]; + fields: string[]; +}; + +export type FilterData = { + datasetGroupName: string; + version: string; + validationStatus: string; + sort: 'last_updated_timestamp desc' | 'last_updated_timestamp asc' | 'name asc' | 'name desc'; +}; + + +export type SelectedRowPayload = {itemId:string |number, dataItem: string; agencyName: string; agencyId?: number | string } + +export type ValidationProgressData = { + id: string; + groupName: string; + majorVersion: number; + minorVersion: number; + patchVersion: number; + latest: boolean; + generationStatus: string; + generationMessage?: string; + progressPercentage: number; +}; + +export type SSEEventData = { + sessionId: string; + generationStatus: string; + generationMessage?: string; + progressPercentage: number; +}; diff --git a/GUI/src/types/mainNavigation.ts b/GUI/src/types/mainNavigation.ts new file mode 100644 index 00000000..d53f6993 --- /dev/null +++ b/GUI/src/types/mainNavigation.ts @@ -0,0 +1,14 @@ +import { ReactNode } from 'react'; + +export interface MenuItem { + id?: string; + label: string; + path: string | null; + target?: '_blank' | '_self'; + children?: MenuItem[]; + icon?: ReactNode; +} + +export interface MainNavigation { + data: MenuItem[]; +} diff --git a/GUI/src/types/router.ts b/GUI/src/types/router.ts new file mode 100644 index 00000000..17fb31f5 --- /dev/null +++ b/GUI/src/types/router.ts @@ -0,0 +1,4 @@ +export interface RouterResponse { + data: Record | null; + error: string | null; +} diff --git a/GUI/src/types/service.ts b/GUI/src/types/service.ts new file mode 100644 index 00000000..2aa95172 --- /dev/null +++ b/GUI/src/types/service.ts @@ -0,0 +1,6 @@ +export interface Service { + id: string; + name: string; + type: 'POST' | 'GET'; + state?: 'active' | 'inactive' | 'draft'; +} diff --git a/GUI/src/types/session.ts b/GUI/src/types/session.ts new file mode 100644 index 00000000..b9af48f4 --- /dev/null +++ b/GUI/src/types/session.ts @@ -0,0 +1,7 @@ +export interface Session { + readonly id: number; + key: string; + value: string; + deleted: boolean; + created: Date | string; +} diff --git a/GUI/src/types/testModel.ts b/GUI/src/types/testModel.ts new file mode 100644 index 00000000..096ea869 --- /dev/null +++ b/GUI/src/types/testModel.ts @@ -0,0 +1,6 @@ + +export type PredictionInput = { + predictedClasses: string[]; + averageConfidence: number; + predictedProbabilities: number[]; +}; \ No newline at end of file diff --git a/GUI/src/types/testModelTypes.ts b/GUI/src/types/testModelTypes.ts new file mode 100644 index 00000000..283dd011 --- /dev/null +++ b/GUI/src/types/testModelTypes.ts @@ -0,0 +1,22 @@ +export type TestModelType = { + majorVersion: number; + minorVersion: number; + modelId: number; + modelName: string; + }; + + export type TestModalDropdownSelectionType = { + label: string; + value: number; + }; + + export type ClassifyTestModalPayloadType = { + modelId: number | null; + text: string; + }; + + export type ClassifyTestModalResponseType = { + predictedClasses: string[]; + averageConfidence: number; + predictedProbabilities: number[]; + }; \ No newline at end of file diff --git a/GUI/src/types/user.ts b/GUI/src/types/user.ts new file mode 100644 index 00000000..e9ba4380 --- /dev/null +++ b/GUI/src/types/user.ts @@ -0,0 +1,17 @@ +import { ROLES } from "enums/roles"; + +export interface User { + login?: string; + fullName?: string; + firstName: string; + lastName: string; + useridcode: string; + displayName: string; + csaTitle: string; + csaEmail: string; + authorities: ROLES[]; + customerSupportStatus: 'online' | 'idle' | 'offline'; +} + +export interface UserDTO extends Pick { +} diff --git a/GUI/src/types/userInfo.ts b/GUI/src/types/userInfo.ts new file mode 100644 index 00000000..c1ca7fc2 --- /dev/null +++ b/GUI/src/types/userInfo.ts @@ -0,0 +1,16 @@ +export interface UserInfo { + JWTCreated: string; + JWTExpirationTimestamp: string; + firstName: string; + lastName: string; + loggedInDate: string; + loginExpireDate: string; + authMethod: string; + fullName: string; + authorities: string[]; + displayName: string; + userIdCode: string; + email: string; + csaEmail: string; + csaTitle: string; +} diff --git a/GUI/src/utils/commonUtilts.ts b/GUI/src/utils/commonUtilts.ts new file mode 100644 index 00000000..d3b3fc2b --- /dev/null +++ b/GUI/src/utils/commonUtilts.ts @@ -0,0 +1,90 @@ +import { rankItem } from '@tanstack/match-sorter-utils'; +import { FilterFn } from '@tanstack/react-table'; +import moment from 'moment'; + +type FormattedOption = { + label: string; + value: string; +}; + +// convert flat array to label, value pairs +export const formattedArray = (data: string[]|undefined): FormattedOption[]|undefined => { + return data?.map((name) => ({ + label: name, + value: name, + })); +}; + +export const toLabelValueArray = ( + data: T[] | undefined, + valueField: keyof T, + labelField: keyof T +): { label: string; value: string }[] | undefined => { + return data?.map((item) => ({ + label: String(item[labelField]), + value: String(item[valueField]), + })); +}; + + +export const convertTimestampToDateTime = (timestamp: number) => { + return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss'); +}; + +// determines version numbers for filter +export const parseVersionString = (version: string) => { + const parts = version.split('.'); + + return { + major: parts[0] !== 'x' ? parseInt(parts[0], 10) : -1, + minor: parts[1] !== 'x' ? parseInt(parts[1], 10) : -1, + patch: parts[2] !== 'x' ? parseInt(parts[2], 10) : -1, + }; +}; + +export const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta({ + itemRank, + }); + return itemRank.passed; +}; + +export const formatDate = (date: Date, format: string) => { + return moment(date).format(format); +}; + +export const formatDateTime = (date: string) => { + const momentDate = moment(date); + const formattedDate = momentDate.format('DD/MM/YYYY'); + const formattedTime = momentDate.format('h.mm A'); + + return { + formattedDate, + formattedTime, + }; +}; + +export const formatClassHierarchyArray = (array: string | string[]) => { + let formattedArray: string[]; + if (typeof array === 'string') { + try { + const cleanedInput = array.trim(); + formattedArray = JSON.parse(cleanedInput); + } catch (error) { + console.error('Error parsing input string:', error); + return ''; + } + } else { + formattedArray = array; + } + + return formattedArray + .map((item, index) => + index === formattedArray?.length - 1 ? item : item + ' ->' + ) + .join(' '); +}; + +export const areArraysEqual = (a: string[] = [], b: string[] = []) => + a.length === b.length && a.every((v, i) => v === b[i]); diff --git a/GUI/src/utils/constants.ts b/GUI/src/utils/constants.ts new file mode 100644 index 00000000..1f4eddca --- /dev/null +++ b/GUI/src/utils/constants.ts @@ -0,0 +1,14 @@ +export const MESSAGE_FILE_SIZE_LIMIT = 10_000_000; + +export enum RUUTER_ENDPOINTS { + SEND_ATTACHMENT= '/attachments/add' +} + +export enum AUTHOR_ROLES { + END_USER = 'end-user', + BACKOFFICE_USER = 'backoffice-user', +} + +export const OVERVIEW_PAGE_SIZE = 12; +export const DATASET_PAGE_SIZE = 10; +export const USER_MANAGEMENT_PAGE_SIZE = 10; \ No newline at end of file diff --git a/GUI/src/utils/dataModelsUtils.ts b/GUI/src/utils/dataModelsUtils.ts new file mode 100644 index 00000000..fae62c82 --- /dev/null +++ b/GUI/src/utils/dataModelsUtils.ts @@ -0,0 +1,60 @@ +import { DataModel } from 'types/dataModels'; + +export const validateDataModel = (dataModel: Partial) => { + const { modelName, datasetId, deploymentEnvironment, baseModels } = dataModel; + const newErrors: any = {}; + + if (!modelName?.trim()) newErrors.modelName = 'Model Name is required'; + if (!deploymentEnvironment?.trim()) newErrors.platform = 'Deployment environent is required'; + if (datasetId === 0) newErrors.dgId = 'Dataset group is required'; + + if (baseModels?.length === 0) + newErrors.baseModels = 'At least one Base Model is required'; + + return newErrors; +}; + +export const customFormattedArray = >( + data: T[] |undefined, + attributeName: keyof T +) => { + return data?.map((item) => ({ + label: `${item[attributeName]}`, + value: item.id, + })); +}; + +export const extractedArray = >( + data: T[], + attributeName: keyof T +): string[] => { + return data?.map((item) => item[attributeName]); +}; + +export const dgArrayWithVersions = >( + data: T[], + attributeName: keyof T +) => { + return data?.map((item) => ({ + label: `${item[attributeName]} (${item.majorVersion}.${item.minorVersion}.${item.patchVersion})`, + value: item.dgId, + })); +}; + +export const getChangedAttributes = ( + original: Partial, + updated: Partial +): Partial> => { + const changes: Partial> = {}; + + (Object.keys(original) as (keyof DataModel)[]).forEach((key) => { + if (original[key] !== updated[key]) { + changes[key] = updated[key] as string | null; + }else { + changes[key] = null; + } + }); + + return changes; +}; + diff --git a/GUI/src/utils/dataTableUtils.ts b/GUI/src/utils/dataTableUtils.ts new file mode 100644 index 00000000..447ff22e --- /dev/null +++ b/GUI/src/utils/dataTableUtils.ts @@ -0,0 +1,44 @@ +import { CellContext, createColumnHelper } from '@tanstack/react-table'; + +export const generateDynamicColumns = ( + columnsData: string[], + editView?: (props: CellContext) => JSX.Element, + deleteView?: (props: CellContext) => JSX.Element +) => { + const columnHelper = createColumnHelper(); // Specify the type for better type checking + const dynamicColumns = columnsData?.map((col) => { + return columnHelper.accessor(col, { + header: col ?? '', + id: col, + }); + }); + + const staticColumns = []; + + if (editView) { + staticColumns.push( + columnHelper.display({ + id: 'edit', + cell: editView, + meta: { + size: '1%', + }, + }) + ); + } + + if (deleteView) { + staticColumns.push( + columnHelper.display({ + id: 'delete', + cell: deleteView, + meta: { + size: '1%', + }, + }) + ); + } + + if (dynamicColumns) return [...dynamicColumns, ...staticColumns]; + else return staticColumns; +}; diff --git a/GUI/src/utils/endpoints.ts b/GUI/src/utils/endpoints.ts new file mode 100644 index 00000000..6a1850ad --- /dev/null +++ b/GUI/src/utils/endpoints.ts @@ -0,0 +1,83 @@ +export const userManagementEndpoints = { + FETCH_USERS: (): string => `/global-classifier/accounts/users`, + ADD_USER: (): string => `/global-classifier/accounts/add`, + CHECK_ACCOUNT_AVAILABILITY: (): string => `/global-classifier/accounts/exists`, + EDIT_USER: (): string => `/global-classifier/accounts/edit`, + DELETE_USER: (): string => `/global-classifier/accounts/delete`, + FETCH_USER_ROLES: (): string => `/global-classifier/accounts/user-role`, +}; + +export const integratedAgenciesEndPoints = { + GET_INTEGRATED_AGENCIES: (): string => + `/global-classifier/agencies/list`, + GET_ALL_AGENCIES: (): string => + `/global-classifier/agencies/all`, +}; + +export const datasetsEndpoints = { + GET_OVERVIEW: (): string => '/global-classifier/datasets/list', + GET_METADATA: (): string => `/global-classifier/datasets/metadata`, + GET_DATASETS_DATA: (): string => '/global-classifier/datasets/data', + GET_ALL_DATASET_VERSIONS: (): string => '/global-classifier/datasets/versions', + GET_DATA_GENERATION_PROGRESS: (): string => `/global-classifier/datasets/progress`, + UPDATE_DATASET: (): string => `/global-classifier/datasets/update`, + DELETE_DATASET: (): string => `/global-classifier/datasets/delete`, + + + + GET_DATASET_FILTERS: (): string => + '/global-classifier/datasetgroup/overview/filters', + GET_DATASETS: (): string => `/global-classifier/datasetgroup/group/data`, + EXPORT_DATASETS: (): string => `/datasetgroup/data/download`, + DATASET_GROUP_MINOR_UPDATE: (): string => + `/global-classifier/datasetgroup/update/minor`, + DATASET_GROUP_MAJOR_UPDATE: (): string => + `/global-classifier/datasetgroup/update/major`, +}; + +export const correctedTextEndpoints = { + GET_CORRECTED_WORDS: ( + pageNumber: number, + pageSize: number, + platform: string, + sortType: string + ) => + `/global-classifier/inference/corrected-metadata?pageNum=${pageNumber}&pageSize=${pageSize}&platform=${platform}&sortType=${sortType}`, + EXPORT_CORRECTED_TEXTS: () => `/datamodel/data/corrected/download` +}; + +export const authEndpoints = { + GET_EXTENDED_COOKIE: () :string => `/global-classifier/auth/jwt/extend`, + LOGOUT: (): string => `/global-classifier/accounts/logout` +} + +export const dataModelsEndpoints = { + GET_OVERVIEW: (): string => '/global-classifier/datamodels/list', + GET_PRODUCTION_DATA_MODEL: (): string => '/global-classifier/datamodels/production-model', + GET_MODEL_METADATA: (): string => '/global-classifier/datamodels/metadata', + GET_DEPLOYMENT_ENVIRONMENTS: (): string => '/global-classifier/datamodels/configs/environments', + CREATE_MODEL: (): string => '/global-classifier/datamodels/create', + CREATE_MAJOR_VERSION: (): string => '/global-classifier/datamodels/major', + CREATE_MINOR_VERSION: (): string => '/global-classifier/datamodels/minor', + DELETE_MODEL: (): string => '/global-classifier/datamodels/delete', + GET_ALL_DATAMODELS_VERSIONS: (): string => '/global-classifier/datamodels/versions', + LOAD_MODEL: (): string => '/global-classifier/testmodel/load', + GET_DATA_MODEL_PROGRESS: (): string => `global-classifier/datamodels/progress`, + DEPLOY_MODEL: (): string => '/global-classifier/inference/deploy', + + + GET_DATAMODELS_FILTERS: (): string => + '/global-classifier/datamodel/overview/filters', + GET_METADATA: (): string => `/global-classifier/datamodel/metadata`, + GET_CREATE_OPTIONS: (): string => `global-classifier/datamodel/create/options`, + CREATE_DATA_MODEL: (): string => `global-classifier/datamodel/create`, + UPDATE_DATA_MODEL: (): string => `global-classifier/datamodel/update`, + DELETE_DATA_MODEL: (): string => `global-classifier/datamodel/delete`, + RETRAIN_DATA_MODEL: (): string => `global-classifier/datamodel/retrain`, +}; + +export const testModelsEndpoints = { + GET_MODELS: (): string => `/global-classifier/testmodel/models`, + CLASSIFY_TEST_MODELS: (): string => `/global-classifier/testmodel/classify`, +}; + diff --git a/GUI/src/utils/format-bytes.ts b/GUI/src/utils/format-bytes.ts new file mode 100644 index 00000000..4f0e5abf --- /dev/null +++ b/GUI/src/utils/format-bytes.ts @@ -0,0 +1,8 @@ +export default function formatBytes(bytes: number, decimals = 0) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'kB', 'MB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} diff --git a/GUI/src/utils/generateUEID.ts b/GUI/src/utils/generateUEID.ts new file mode 100644 index 00000000..8e7a1fc0 --- /dev/null +++ b/GUI/src/utils/generateUEID.ts @@ -0,0 +1,8 @@ +export const generateUEID = () => { + let first: string | number = (Math.random() * 46656) | 0; + let second: string | number = (Math.random() * 46656) | 0; + first = ('000' + first.toString(36)).slice(-3); + second = ('000' + second.toString(36)).slice(-3); + + return first + second; +}; diff --git a/GUI/src/utils/local-storage-utils.ts b/GUI/src/utils/local-storage-utils.ts new file mode 100644 index 00000000..c4183f51 --- /dev/null +++ b/GUI/src/utils/local-storage-utils.ts @@ -0,0 +1,17 @@ +export const getFromLocalStorage = ( + key: string, + initialValue: any = null +): any => { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch { + return initialValue; + } +}; + +export const setToLocalStorage = (key: string, value: any): void => { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch {} +}; diff --git a/GUI/src/utils/queryKeys.ts b/GUI/src/utils/queryKeys.ts new file mode 100644 index 00000000..27affe50 --- /dev/null +++ b/GUI/src/utils/queryKeys.ts @@ -0,0 +1,96 @@ +import { PaginationState, SortingState } from '@tanstack/react-table'; + +export const userManagementQueryKeys = { + getAllEmployees: function ( + pagination?: PaginationState, + sorting?: SortingState + ) { + return ['accounts/users', pagination, sorting].filter( + (val) => val !== undefined + ); + }, +}; + +export const integratedAgenciesQueryKeys = { + INTEGRATED_AGENCIES_LIST: function (pageIndex?: number,sortOption?:string,searchTerm?:string) { + return ['integrated-agencies/list',pageIndex,sortOption,searchTerm].filter( + (val) => val !== undefined + ); + }, + ALL_AGENCIES_LIST: () => ['integrated-agencies/all'], + USER_ROLES: (): string[] => ['/accounts/user-role', 'prod'], + + } + + +export const datasetQueryKeys = { + DATASET_FILTERS: (): string[] => ['datasets/filters'], + DATASET_VERSIONS: (): string[] => ['datasets/versions'], + DATASET_OVERVIEW: function ( + pageIndex?: number, + generationStatus?: string, + sort?: string + ) { + return [ + 'datasets/overview', + pageIndex, + generationStatus, + sort, + ].filter((val) => val !== undefined); + }, + GET_META_DATA: function (datasetId?: number|string) { + return ['datasets/metadata', `${datasetId}`].filter( + (val) => val !== undefined + ); + }, + GET_DATA_SETS: function (datasetId?: number|string, agencyId?:number|string, pageNum?: number, pageSize?: number) { + return ['datasets/data', datasetId, agencyId, pageNum, pageSize].filter( + (val) => val !== undefined + ); + }, + + + GET_DATASET_GROUP_PROGRESS: () => ['datasetgroups/progress'], +}; + +export const stopWordsQueryKeys = { + GET_ALL_STOP_WORDS: () => [`datasetgroups/stopwords`], +}; + +export const authQueryKeys = { + USER_DETAILS: () => ['global-classifier/auth/jwt/userinfo', 'prod'], +}; + +export const dataModelsQueryKeys = { + DATA_MODEL_FILTERS: (): string[] => ['datamodels/filters'], + GET_PROD_DATA_MODEL: (): string[] => ['datamodels/production-model'], + DATA_MODEL_DEPLOYMENT_ENVIRONMENTS: (): string[] => ['datamodels/deployment-environments'], + DATA_MODELS_OVERVIEW: function ( + pageIndex?: number, + modelStatus?:string, + trainingStatus?:string, + maturity?:string, + sort?:string, + + ) { + return [ + 'datamodels/list', + pageIndex, + modelStatus, + trainingStatus, + maturity, + sort + ].filter((val) => val !== undefined); + }, + GET_META_DATA: function (modelId?: number | string) { + return ['datamodels/metadata', `${modelId}`].filter( + (val) => val !== undefined + ); + }, + GET_DATA_MODELS_PROGRESS: () => ['datamodels/progress'], + GET_ALL_DATA_MODELS_VERSIONS: (): string[] => ['datamodels/versions'], +}; + +export const testModelsQueryKeys = { + GET_TEST_MODELS: () => ['testModels'] +} diff --git a/GUI/src/vite-env.d.ts b/GUI/src/vite-env.d.ts new file mode 100644 index 00000000..b1f45c78 --- /dev/null +++ b/GUI/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/GUI/translations/en/common.json b/GUI/translations/en/common.json new file mode 100644 index 00000000..a06f718e --- /dev/null +++ b/GUI/translations/en/common.json @@ -0,0 +1,494 @@ +{ + "global": { + "save": "Save", + "add": "Add", + "edit": "Edit", + "delete": "Delete", + "cancel": "Cancel", + "confirm": "Confirm", + "continue": "Continue", + "modifiedAt": "Last modified at", + "addNew": "Add new", + "search": "Search", + "notification": "Notification", + "notificationError": "Error", + "active": "Active", + "activate": "Activate", + "deactivate": "Deactivate", + "disconnect": "Disconnect", + "connect": "Connect", + "on": "On", + "off": "Off", + "back": "Back", + "from": "From", + "to": "To", + "view": "View", + "resultCount": "Result count", + "paginationNavigation": "Pagination navigation", + "gotoPage": "Goto page", + "name": "Name", + "idCode": "ID code", + "status": "Status", + "yes": "Yes", + "no": "No", + "removeValidation": "Are you sure?", + "startDate": "Start date", + "endDate": "End date", + "preview": "Preview", + "logout": "Logout", + "change": "Change", + "loading": "Loading", + "asc": "asc", + "desc": "desc", + "reset": "Reset", + "choose": "Choose", + "extendSession": "Extend Session", + "unAuthorized": "Unauthorized", + "unAuthorizedDesc": "You do not have permission to view this page.", + "latest": "Latest", + "failed": "Failed", + "sessionTimeOutTitle": "You session has been ended!", + "sessionTimeOutDesc": "Extend your session or sign out from application in {{seconds}}", + "close": "Close", + "proceed": "Proceed", + "maxFileSize": "File size should not exceed 20 MB.", + "select": "-Select-", + "replace": "Replace", + "clearFilters": "Clear Filters", + "showEntries": "Show", + "entries": "records", + "deleteSelected": "Delete Selection" + }, + "menu": { + "userManagement": "User Management", + "agencies": "Integrated Clients", + "dataSets": { + "title": "Datasets", + "overview": "Overview", + "progress": "Data Generation Progress" + }, + "datasetGroups": "Dataset Groups", + "validationSessions": "Data Generation Sessions", + "dataModels": { + "title": "Data Models", + "overview": "Overview", + "progress": "Training Progress" + }, + "models": "Models", + "trainingSessions": "Training Sessions", + "testModel": "Test Model", + "stopWords": "Stop Words", + "correctedTexts": "Corrected Texts" + }, + "userManagement": { + "title": "User Management", + "addUserButton": " Add a user", + "addUser": { + "addUserModalTitle": "Add a new user", + "editUserModalTitle": "Edit user", + "deleteUserModalTitle": "Are you sure?", + "deleteUserModalDesc": "Confirm that you are wish to delete the following record", + "name": "First and last name", + "namePlaceholder": "Enter name", + "role": "Role", + "rolePlaceholder": "-Select-", + "personalId": "Personal ID", + "personalIdPlaceholder": "Enter personal ID", + "title": "Title", + "titlePlaceholder": "Enter title", + "email": "Email", + "emailPlaceholder": "Enter email", + "nameRequired": "Name is required", + "roleRequired": "Role is required", + "idCodeRequired": "ID code is required", + "titleRequired": "Title is required", + "emailRequired": "Email is required", + "invalidIdCode": "Invalid ID code", + "invalidEmail": "Invalid Email", + "userExists": "User already exists" + }, + "table": { + "fullName": "Full Name", + "personalId": "Personal ID", + "role": "Role", + "email": "Email", + "actions": "Actions", + "title": "Title" + } + }, + "integratedAgencies": { + "title": "Integrated Clients", + "search": "Search client", + "noClients": "No clients found", + "sortOptions": { + "agencyAsc": "Client Name: A-Z", + "agencyDesc": "Client Name: Z-A", + "createdDateAsc": "Created: Oldest First", + "createdDateDesc": "Created: Newest First", + "lastUpdatedDateAsc": "Updated: Oldest First", + "lastUpdatedDateDesc": "Updated: Newest First" + }, + "agencyCard": { + "lastModelTrained": "Last Model Trained", + "lastUsedForTraining": "Last Used For Training", + "lastSynced": "Last Synced", + "latest": "Latest", + "syncStatus": { + "synced": "Synced with CKB", + "unavailable": "Unavailable in CKB", + "resync": "Resync needed with CKB", + "inProgress": "Sync in Progress with CKB", + "resyncInProgress": "Resync in Progress with CKB", + "failed": "Sync with CKB Failed" + }, + "resync": "Resync" + } + }, + "integration": { + "title": "Integration", + "jira": "Jira", + "outlook": "Outlook", + "jiraDesc": "Atlassian issue tracking and project management software", + "outlookDesc": "Personal information manager and email application developed by Microsoft", + "connected": "Connected", + "disconnected": "Disconnected", + "integrationErrorTitle": "Integration Unsuccessful", + "integrationErrorDesc": "Failed to connect with {{channel}}. Please check your settings and try again. If the problem persists, contact support for assistance.", + "integrationSuccessTitle": "Integration Successful", + "integrationSuccessDesc": "You have successfully connected with {{channel}}! Your integration is now complete, and you can start working with {{channel}} seamlessly.", + "confirmationModalTitle": "Are you sure?", + "disconnectConfirmationModalDesc": "Are you sure you want to disconnect the {{channel}} integration? This action cannot be undone and may affect your workflow and linked issues.", + "connectConfirmationModalDesc": "Are you sure you want to connect the {{channel}} integration? This action cannot be undone and may affect your workflow and linked issues.", + "disconnectErrorTi/tle": "Disconnection Unsuccessful", + "disconnectErrorDesc": "Failed to disconnect {{channel}}. Please check your settings and try again. If the problem persists, contact support for assistance.", + "addUserButton": " Add a user", + "addUser": { + "name": "First and last name", + "namePlaceholder": "Enter name", + "role": "Role", + "rolePlaceholder": "-Select-", + "personalId": "Personal ID", + "personalIdPlaceholder": "Enter personal ID", + "title": "Title", + "titlePlaceholder": "Enter title", + "email": "Email", + "emailPlaceholder": "Enter email" + } + }, + "roles": { + "ROLE_ADMINISTRATOR": "Administrator", + "ROLE_MODEL_TRAINER": "Model Trainer" + }, + "toast": { + "success": { + "updateSuccess": "Updated Successfully", + "copied": "Copied", + "userDeleted": "User deleted", + "newUserAdded": "New user added", + "userUpdated": "User updated" + } + }, + "datasets": { + "title": "Datasets", + "noDatasets": "No data sets available", + "sortOptions": { + "createdDateAsc": "Created: Oldest First", + "createdDateDesc": "Created: Newest First" + }, + "datasetCard": { + "inProgress": "Data Generation in Progress", + "failed": "Data Generation Failed", + "success": "Data Generation Successful", + "settings": "Settings", + "lastModelTrained": "Last Model Trained", + "lastUsedForTraining": "Last Used For Training", + "lastUpdate": "Last Updated", + "latest": "Latest" + }, + "detailedView": { + "dataset": "Dataset", + "version": "Dataset Version", + "connectedModels": "Connected Models", + "noOfItems": "Number of items", + "export": "Export Dataset", + "unsavedChangesWarning": "You have made changes to the dataset which are not saved. Please save the changes to apply", + "noData": "No Data Available", + "editDataRowTitle": "Edit Data Record", + "editDataRowDesc": "Updates you make to the data record will be saved in the dataset", + "deleteDataRowTitle": "Delete Data Record", + "deleteDataRowDesc": "Are you sure you want to delete this data record?", + "data": "Data", + "clientName": "Client Name", + "patchUpdateBanner": "You have edited individual items in the dataset which are not saved. Please save the changes to apply", + "confirmUpdateDatasetTitle": "Confirm dataset update", + "confirmUpdateDatasetDesc": "Changed data rows will be updated in the dataset", + "confirmDeleteDatasetTitle": "Confirm dataset deletion", + "confirmDeleteDatasetDesc": "Deleted data rows will be removed from the dataset", + "datasetUpdateUnsuccessfulTitle": "Dataset update unsuccessful", + "datasetUpdateUnsuccessfulDesc": "Something went wrong while updating the dataset. Please try again.", + "datasetUpdateSuccessfulTitle": "Dataset update successful", + "datasetUpdateSuccessfulDesc": "The dataset has been successfully updated.", + "datasetDeleteUnsuccessfulTitle": "Dataset delete unsuccessful", + "datasetDeleteUnsuccessfulDesc": "Something went wrong while deleting the dataset. Please try again.", + "datasetDeleteSuccessfulTitle": "Dataset delete successful", + "datasetDeleteSuccessfulDesc": "The dataset has been successfully deleted.", + "exportDataSuccessTitle": "Data export was successful", + "exportDataSuccessDesc": "Your data has been successfully exported.", + "exportDataUnsucessTitle": "Dataset export unsuccessful", + "exportDataUnsucessDesc": "Something went wrong. Please try again.", + "itemsUpdated": "items updated", + "itemsDeleted": "items deleted", + "dataBeingUpdated": "Data is being updated...", + "itemsSelected": "items selected", + "bulkDeleteTitle": "Delete Selected Items", + "bulkDeleteDesc1": "Are you sure you want to delete the selected items?", + "bulkDeleteDesc2": "Note : This deletion will not be affected in the original dataset until you click on Save Changes.", + "bulkDeleteSuccessTitle": "Items Deleted Successfully", + "table": { + "id": "Item ID", + "data": "Data", + "client": "Client", + "actions": "Actions" + }, + "validationsTitle": "Dataset Group Validations", + "classHierarchy": "Class Hierarchies", + "delete": "Delete Dataset", + "modals": { + "import": { + "title": "Import new data", + "fileFormatlabel": "Select the file format", + "attachments": "Attachments", + "maxSize": "Maximum file size - 10mb", + "browse": "Browse file", + "import": "Import", + "cancel": "Cancel", + "uploadInProgress": "Upload in Progress...", + "uploadDesc": "Uploading dataset. Please wait until the upload finishes. If you cancel midway, the data and progress will be lost.", + "invalidFile": "Invalid File Format", + "invalidFileDesc": "The uploaded file is not in the correct {{format}} format. Please upload a valid {{format}} file and try again." + }, + "export": { + "export": "Export data", + "exportButton": "Export", + "fileFormatlabel": "Select the file format", + "title": "Data export was successful", + "description": "Your data has been successfully exported." + }, + "delete": { + "title": "Are you sure?", + "description": "Once you delete the dataset all models connected to this model will become untrainable. Are you sure you want to proceed?", + "error": "Dataset Group Deletion Unsuccessful", + "errorDesc": "There was an issue deleting the dataset group. Please try again. If the problem persists, contact support for assistance." + }, + "edit": { + "title": "Edit", + "data": "Data", + "label": "Label", + "update": "Update", + "error": "Dataset Group Update Unsuccessful", + "errorDesc": "There was an issue updating the dataset group. Please try again. If the problem persists, contact support for assistance." + }, + "upload": { + "title": "Data upload successful", + "desc": "The dataset file was successfully uploaded. Please save the changes to initiate data validation and preprocessing" + }, + "datasetDelete": { + "confirmationTitle": "Are you sure?", + "confirmationDesc": "Confirm that you are wish to delete the following dataset", + "successTitle": "Success: Dataset Deleted", + "successDesc": "You have successfully deleted the dataset. The dataset is no longer available and all related data has been removed.", + "proceedToDashboard": "Proceed to dataset groups" + } + } + } + }, + "stopWords": { + "title": "Stop Words", + "import": "Import stop words", + "stopWordInputHint": "Enter stop word", + "add": "Add", + "importModal": { + "title": "Import stop words", + "importButton": "Import", + "selectionLabel": "Select the option below", + "addOption": "Import to add", + "updateOption": "Import to update", + "deleteOption": "Import to delete", + "attachements": "Attachments (TXT, XLSX, YAML, JSON)", + "inprogressTitle": "Import in Progress", + "inprogressDesc": "The import of stop words is currently in progress. Please wait until the process is complete.", + "successTitle": "Data import was successful", + "successDesc": "Your data has been successfully imported.", + "unsuccessTitle": "Data import was unsuccessful", + "unsuccessDesc": "Stop words Import Unsuccessful" + } + }, + "validationSessions": { + "title": "Data Generation Sessions", + "inprogress": "Data Generation in-Progress", + "fail": "Data Generation failed because {{class}} class found in the {{column}} column does not exist in hierarchy", + "noSessions": "No ongoing Data Generation sessions available" + }, + "correctedTexts": { + "title": "Corrected Texts", + "export": "Export Data", + "searchIncomingText": "Search incoming texts", + "filterAsc": "Filter by date created - Ascending", + "filterDesc": "Filter by date created - Descending", + "platform": "Platform", + "dateAndTime": "Date & Time", + "inferenceTime": "Inference Time", + "text": "Text", + "predictedHierarchy": "Predicted Class Hierarchy", + "predictedConfidenceProbability": "Predicted Classes Average Confidence Probability", + "correctedHierarchy": "Corrected Class Hierarchy", + "correctedConfidenceProbability": "Corrected Classes Average Confidence Probability", + "labelNotFoundText": "Label not in dataset", + "exportSuccessTitle": "Data export was successful", + "exportSuccessDesc": "Your data has been successfully exported.", + "exportDataUnsucessTitle": "Data Export Unsuccessful", + "exportDataUnsucessDesc": "Something went wrong. Please try again." + }, + "dataModels": { + "productionModels": "Production Models", + "dataModels": "Data Models", + "createModel": "Create Model", + "noProdModels": "No production models available", + "noModels": "No models available", + "sortOptions": { + "dataModelAsc": "Data Model Name: A-Z", + "dataModelDesc": "Data Model Name: Z-A", + "createdDateAsc": "Created: Oldest First", + "createdDateDesc": "Created: Newest First" + }, + "filters": { + "modelName": "Model Name", + "version": "Version", + "modelStatus": "Model Status", + "datasetGroup": "Dataset Group", + "trainingStatus": "Training Status", + "maturity": "Deployment Environment", + "sort": "Sort by name (A - Z)" + }, + "trainingStatus": { + "retrainingNeeded": "Retraining Needed", + "trained": "Trained", + "initiatingTraining": "Initiating Training", + "trainingFailed": "Training Failed", + "notTrained": "Not Trained" + }, + "maturity": { + "production": "Production", + "undeployed": "Undeployed", + "testing": "Testing" + }, + "dataModelCard": { + "dataset": "Dataset", + "datasetVersion": "Dataset Version", + "lastTrained": "Last Trained" + }, + "trainingResults": { + "title": "Training Results", + "bestPerformingModel": "Best Performing Model", + "classes": "Classes", + "accuracy": "Accuracy", + "f1Score": "F1 Score", + "noResults": "No training results available", + "viewResults": " View Results" + }, + "createDataModel": { + "title": "Create Data Model", + "replaceTitle": "Warning: Replace Production Model", + "replaceDesc": "Adding this model to production will replace the current production model. Are you sure you want to proceed?", + "successTitle": "Data Model Created and Started Training", + "successDesc": " You have successfully created and started training the data model. You can view it on the data model dashboard.", + "viewAll": "View All Data Models", + "errorTitle": "Error Creating Data Model", + "errorDesc": " There was an issue creating or training the data model. Please try again. If the problem persists, contact support for assistance.", + "replaceWarning": "{{platform}} integration is currently disabled, therefore the model wouldn't receive any inputs or make any predictions" + }, + "loadDataModel": { + "title": "Load Data Model", + "loading": "Loading Data Model...", + "loaded": "Data Model Loaded", + "errorTitle": "Error Loading Data Model", + "errorDesc": " There was an issue loading the data model. Please try again. If the problem persists, contact support for assistance." + }, + "configureDataModel": { + "saveChangesTitile": "Changes Saved Successfully", + "saveChangesDesc": "You have successfully saved the changes. You can view the data model in the \"All Data Models\" view.", + "updateErrorTitile": "Error Updating Data Model", + "updateErrorDesc": "There was an issue updating the data model. Please try again. If the problem persists, contact support for assistance.", + "deleteErrorTitle": "Cannot Delete Model", + "deleteErrorDesc": "The model cannot be deleted because it is currently in production. Please escalate another model to production before proceeding to delete this model.", + "deleteConfirmation": "Are you sure?", + "deleteConfirmationDesc": "Confirm that you are wish to delete the following data model", + "deleteModalErrorTitle": "Error deleting data model", + "deleteModalErrorDesc": "There was an issue deleting the data model. Please try again. If the problem persists, contact support for assistance.", + "deleteModalSuccessTitle": "Model Deleted Successfully", + "deleteModalSuccessDesc": "You have successfully deleted the data model. The model is no longer available and all related data has been removed.", + "deployDataModalSuccessTitle": "Model Deployed Successfully", + "deployDataModalSuccessDesc": "You have successfully deployed the data model.", + "deployDataModalErrorTitle": "Error deploying data model", + "deployDataModalErrorDesc": "There was an issue deploying the data model. Please try again. If the problem persists, contact support for assistance.", + "changeProdModelTitle": "Warning: Changing Production Model", + "changeProdModelDesc": "Changing this model will impact the current production model. Are you sure you want to proceed?", + "title": "Configure Data Model", + "retrainCard": "Model updated. Please initiate retraining to continue benefiting from the latest improvements.", + "retrain": "Retrain", + "deleteModal": "Delete model", + "confirmRetrain": "Confirm retrain model", + "confirmRetrainDesc": "Are you sure you want to retrain this model?", + "save": "Save Changes" + }, + "dataModelForm": { + "modelVersion": "Model Version", + "datasetGroup": "Select Dataset Version", + "baseModels": "Select Base Models", + "deploymentPlatform": "Select Deployment Environment", + "errors": { + "datasetVersionNotExist": "Dataset version does not exist" + } + } + }, + "trainingSessions": { + "title": "Training Sessions", + "inprogress": "Validation in-Progress", + "fail": "Validation failed because {{class}} class found in the {{column}} column does not exist in hierarchy", + "noSessions": "No Active Training Sessions", + "noSessionsDesc": "There are currently no active training sessions. Once you start a training session, it will appear here. In the meantime, you can initiate a new training session to begin improving your models." + }, + "testModels": { + "title": "Test Model", + "selectionLabel": "Model", + "placeholder": "Choose model", + "classifyTextLabel": "Enter Text", + "classify": "Classify", + "predictedHierarchy": "Predicted Class Hierarchy : ", + "averageConfidence": "Average Confidence : ", + "classProbabilities": "Class Probabilities : ", + "error": "Classification Error", + "errorDesc": "There was an issue classifying the text. Please try again. If the problem persists, contact support for assistance.", + "results": "Classification Results", + "topPrediction": "Top Prediction", + "allPredictions": "All Predictions", + "classificationFailed": "Classification failed. Please try again." + }, + "optionLists": { + "text": "Text", + "numbers": "Number", + "dateTimes": "Date Time", + "email": "Email", + "fileAttachements": "File Attachments", + "importToAdd": "Import to add", + "importToDelete": "Import to delete", + "userManagement": "User Management", + "integration": "Integration", + "dataset": "Dataset", + "dataModels": "Data Models", + "classes": "Classes", + "stopWords": "Stop Words", + "incomingTexts": "Incoming Texts", + "testModel": "Test Model" + } +} \ No newline at end of file diff --git a/GUI/translations/et/common.json b/GUI/translations/et/common.json new file mode 100644 index 00000000..924845fb --- /dev/null +++ b/GUI/translations/et/common.json @@ -0,0 +1,487 @@ +{ + "global": { + "save": "Salvesta", + "add": "Lisa", + "edit": "Redigeeri", + "delete": "Kustuta", + "cancel": "Tühista", + "confirm": "Kinnita", + "modifiedAt": "Viimati muudetud", + "addNew": "Lisa uus", + "search": "Otsi", + "notification": "Teade", + "notificationError": "Viga", + "active": "Aktiivne", + "activate": "Aktiveeri", + "deactivate": "Deaktiveeri", + "disconnect": "Eemalda ühendus", + "connect": "Ühenda", + "on": "Sees", + "off": "Väljas", + "back": "Tagasi", + "from": "Alates", + "to": "Kuni", + "view": "Vaata", + "resultCount": "Tulemuste arv", + "paginationNavigation": "Lehekülgede navigeerimine", + "gotoPage": "Mine lehele", + "name": "Nimi", + "idCode": "ID kood", + "status": "Olek", + "yes": "Jah", + "no": "Ei", + "removeValidation": "Kas oled kindel?", + "startDate": "Alguskuupäev", + "endDate": "Lõppkuupäev", + "preview": "Eelvaade", + "logout": "Logi välja", + "change": "Muuda", + "loading": "Laadimine", + "asc": "kasvav", + "desc": "kahanev", + "reset": "Lähtesta", + "choose": "Vali", + "extendSession": "Pikenda sessiooni", + "unAuthorized": "Juurdepääs keelatud", + "unAuthorizedDesc": "Teil ei ole lubatud seda lehte vaadata.", + "latest": "Viimane", + "failed": "Ebaõnnestus", + "sessionTimeOutTitle": "Teie sessioon on lõppenud!", + "sessionTimeOutDesc": "Pikendage oma sessiooni või logige rakendusest välja {{seconds}}", + "close": "Sulge", + "proceed": "Jätka", + "maxFileSize": "Faili suurus ei tohiks ületada 20 MB." + }, + "menu": { + "userManagement": "Kasutajate haldus", + "agencies": "Integreeritud asutused", + "dataSets": "Andmekogumid", + "datasetGroups": "Andmekogude grupid", + "validationSessions": "Valideerimine", + "dataModels": "Andmemudelid", + "models": "Mudelid", + "trainingSessions": "Treenimine", + "testModel": "Testmudel", + "stopWords": "Stop-sõnad", + "correctedTexts": "Parandatud tekstid" + }, + "userManagement": { + "title": "Kasutajate haldus", + "addUserButton": " Lisa kasutaja", + "addUser": { + "addUserModalTitle": "Lisa uus kasutaja", + "editUserModalTitle": "Redigeeri kasutajat", + "deleteUserModalTitle": "Oled kindel?", + "deleteUserModalDesc": "Kinnita, et soovid kirje kustutada", + "name": "Ees- ja perekonnanimi", + "namePlaceholder": "Sisesta nimi", + "role": "Roll", + "rolePlaceholder": "-Vali-", + "personalId": "Isiklik ID", + "personalIdPlaceholder": "Sisesta isiklik ID", + "title": "Ametinimetus", + "titlePlaceholder": "Sisesta ametinimetus", + "email": "E-post", + "emailPlaceholder": "Sisesta e-post", + "nameRequired": "Nimi on kohustuslik", + "roleRequired": "Roll on kohustuslik", + "idCodeRequired": "ID kood on kohustuslik", + "titleRequired": "Ametinimetus on kohustuslik", + "emailRequired": "E-posti aadress on kohustuslik", + "invalidIdCode": "Vigane ID kood", + "invalidEmail": "Vigane e-posti aadress", + "userExists": "Kasutaja on juba olemas" + }, + "table": { + "fullName": "Täisnimi", + "personalId": "Isiklik ID", + "role": "Roll", + "email": "E-post", + "actions": "Tegevused", + "title": "Ametinimetus" + } + }, + + "integratedAgencies":{ + "title": "Integreeritud asutused", + "search":"Otsi asutust" +}, + + "integration": { + "title": "Integratsioon", + "jira": "Jira", + "outlook": "Outlook", + "jiraDesc": "Atlassiani teemade jälgimise ja projektide juhtimise tarkvara", + "outlookDesc": "Isikliku teabehalduri ja e-posti rakendus, mille on välja töötanud Microsoft", + "connected": "Ühendatud", + "disconnected": "Ühendus katkestatud", + "integrationErrorTitle": "Integratsioon ebaõnnestus", + "integrationErrorDesc": "Ebaõnnestus ühenduse loomine {{channel}}-iga. Palun kontrollige oma seadistusi ja proovige uuesti. Kui probleem püsib, võtke ühendust toe saamiseks.", + "integrationSuccessTitle": "Integratsioon edukas", + "integrationSuccessDesc": "Olete edukalt ühendatud {{channel}}-iga! Teie integratsioon on nüüd lõppenud ja saate alustada {{channel}}-iga sujuvat töötamist.", + "confirmationModalTitle": "Oled kindel?", + "disconnectConfirmationModalDesc": "Kas oled kindel, et soovid katkestada {{channel}} integratsiooni? See tegevus on pöördumatu ja võib mõjutada teie töövoogu ja seotud teemasid.", + "connectConfirmationModalDesc": "Kas oled kindel, et soovid luua ühenduse {{channel}}-iga? See tegevus on pöördumatu ja võib mõjutada teie töövoogu ja seotud teemasid.", + "disconnectErrorTitle": "Katkestamine ebaõnnestus", + "disconnectErrorDesc": "Ebaõnnestus {{channel}} katkestamine. Palun kontrollige oma seadistusi ja proovige uuesti. Kui probleem püsib, võtke ühendust toe saamiseks.", + "addUserButton": " Lisa kasutaja", + "addUser": { + "name": "Ees- ja perekonnanimi", + "namePlaceholder": "Sisesta nimi", + "role": "Roll", + "rolePlaceholder": "-Vali-", + "personalId": "Isiklik ID", + "personalIdPlaceholder": "Sisesta isiklik ID", + "title": "Ametinimetus", + "titlePlaceholder": "Sisesta ametinimetus", + "email": "E-post", + "emailPlaceholder": "Sisesta e-post" + } + }, + "roles": { + "ROLE_ADMINISTRATOR": "Administraator", + "ROLE_MODEL_TRAINER": "Mudeli treener" + }, + "toast": { + "success": { + "updateSuccess": "Uuendamine õnnestus", + "copied": "Kopeeritud", + "userDeleted": "Kasutaja kustutatud", + "newUserAdded": "Uus kasutaja lisatud", + "userUpdated": "Kasutaja uuendatud" + } + }, + "datasetGroups": { + "title": "Andmestiku grupid", + "createDatasetGroupButton": "Loo andmestiku grupp", + "noDatasets": "Andmestike komplekte ei ole saadaval", + "sortOptions": { + "datasetAsc": "Andmestiku grupi nimi A-Z", + "datasetDesc": "Andmestiku grupi nimi Z-A", + "createdDateAsc": "Loomise kuupäev vanim enne", + "createdDateDesc": "Loomise kuupäev uusim enne", + "lastUpdatedDateAsc": "Viimati uuendatud kuupäev, vanim enne", + "lastUpdatedDateDesc": "Viimati uuendatud kuupäev, uusim enne" + }, + "table": { + "group": "Andmestiku grupp", + "version": "Versioon", + "validationStatus": "Valideerimise staatus", + "sortBy": "Sorteeri", + "email": "E-post", + "actions": "Tegevused" + }, + "datasetCard": { + "validationFail": "Kontroll ebaõnnestus", + "validationSuccess": "Kontroll õnnestus", + "validationInprogress": "Kontroll on käimas", + "notValidated": "Ei ole kontrollitud", + "settings": "Seaded", + "lastModelTrained": "Viimane mudel treenitud", + "lastUsedForTraining": "Viimane kasutatud treenimiseks", + "lastUpdate": "Viimane uuendus", + "latest": "Viimased" + }, + "createDataset": { + "title": "Loo andmestiku grupp", + "datasetDetails": "Andmestiku üksikasjad", + "datasetName": "Andmestiku nimi", + "datasetInputPlaceholder": "Sisesta andmestiku nimi", + "validationCriteria": "Loo valideerimise kriteeriumid", + "fieldName": "Välja nimi", + "datasetType": "Andmestiku tüübid", + "dataClass": "Andmeklass", + "typeText": "Tekst", + "typeNumbers": "Numbrid", + "typeDateTime": "Kuupäev ja aeg", + "addClassButton": "Lisa klass", + "addNowButton": "Lisa nüüd", + "selectPlaceholder": "- Valige -" + }, + "classHierarchy": { + "title": "Klassi hierarhia", + "addClassButton": "Lisa peamine klass", + "addSubClass": "Lisa alamklass", + "fieldHint": "Sisesta välja nimi", + "filedHintIfExists": "Klassi nimi juba olemas" + }, + "modals": { + "deleteClassTitle": "Oled sa kindel?", + "deleteClaassDesc": "Kinnita, et soovid kustutada järgmise kirje", + "columnInsufficientHeader": "Andmestikus puuduvad veerud", + "columnInsufficientDescription": "Andmestikus peab olema vähemalt 2 veergu. Lisaks peab olema vähemalt üks veerg määratud andmeklassiks ja üks veerg, mis ei ole andmeklass. Palun kohanda oma andmestikku vastavalt.", + "classsesInsufficientHeader": "Andmestikus puuduvad klassid", + "classsesInsufficientDescription": "Andmestikus peab olema vähemalt 2 peamist klassi hierarhias", + "createDatasetSuccessTitle": "Andmestiku grupp loodud edukalt", + "createDatasetUnsuccessTitle": "Andmestiku grupi loomine ebaõnnestus", + "createDatasetSucceessDesc": "Oled edukalt loonud andmestiku grupi. Detailvaates saad nüüd andmestikku vaadata ja vajadusel redigeerida.", + "navigateDetailedViewButton": "Mine detailvaatesse", + "enableDatasetTitle": "Andmestiku grupi lülitamine ebaõnnestus", + "enableDatasetDesc": "Andmestiku gruppi ei saa aktiveerida, kuni andmed on lisatud. Palun lisa andmestikke sellesse gruppi ja proovi uuesti.", + "errorTitle": "Tegevus ebaõnnestus", + "errorDesc": "Midagi läks valesti. Palun proovi uuesti." + }, + "detailedView": { + "connectedModels": "Ühendatud mudelid", + "noOfItems": "Arv", + "export": "Eksporti andmestik", + "import": "Impordi andmestik", + "unsavedChangesWarning": "Oled teinud muudatusi andmestikus, mis ei ole salvestatud. Palun salvesta muudatused, et neid rakendada", + "insufficientExamplesDesc": "Näidisandmed puuduvad - andmestiku grupi aktiveerimiseks on vajalik vähemalt 10 näidist", + "noData": "Andmeid ei ole saadaval", + "noCorrectedTexts": "Parandatud tekste ei ole saadaval", + "noDataDesc": "Oled loonud andmestiku grupi, kuid andmeid, mida siin kuvada, pole saadaval. Saad üles laadida andmestiku, et vaadata seda siin. Kui andmed on lisatud, saad neid vajadusel redigeerida või kustutada.", + "importExamples": "Impordi näidised", + "importNewData": "Impordi uued andmed", + "majorUpdateBanner": "Oled värskendanud andmestiku skeemi olulisi seadistusi, mis ei ole salvestatud. Palun salvesta, et rakendada muudatusi. Kõik imporditud failid või olemasolevates andmetes tehtud muudatused kõrvaldatakse pärast muudatuste rakendamist", + "minorUpdateBanner": "Oled importinud andmestikku uusi andmeid, palun salvesta muudatused, et neid rakendada. Kõik individuaalsetele andmeelementidele tehtud muudatused kõrvaldatakse pärast muudatuste rakendamist", + "patchUpdateBanner": "Oled redigeerinud andmestikus individuaalseid elemente, mis ei ole salvestatud. Palun salvesta muudatused, et neid rakendada", + "confirmMajorUpdatesTitle": "Kinnita oluline värskendus", + "confirmMajorUpdatesDesc": "Kõik imporditud failid või olemasolevates andmetes tehtud muudatused kõrvaldatakse pärast muudatuste rakendamist", + "confirmMinorUpdatesTitle": "Kinnita värskendus", + "confirmMinorUpdatesDesc": "Kõik individuaalsetele andmeelementidele tehtud muudatused (patch värskendus) kõrvaldatakse pärast muudatuste rakendamist", + "confirmPatchUpdatesTitle": "Kinnita Patch värskendus", + "confirmPatchUpdatesDesc": "Muutunud andmeread uuendatakse andmestikus", + "patchDataUnsuccessfulTitle": "Patch andmete uuendamine ebaõnnestus", + "patchDataUnsuccessfulDesc": "Midagi läks valesti. Palun proovi uuesti.", + "exportDataSuccessTitle": "Andmete eksportimine õnnestus", + "exportDataSuccessDesc": "Sinu andmed on edukalt eksporditud.", + "exportDataUnsucessTitle": "Andmete eksportimine ebaõnnestus", + "exportDataUnsucessDesc": "Midagi läks valesti. Palun proovi uuesti.", + "ImportDataUnsucessTitle": "Andmete import ebaõnnestus", + "importDataUnsucessDesc": "Midagi läks valesti. Palun proovi uuesti.", + "validationInitiatedTitle": "Andmestik laaditi üles ja alustati kontrolli", + "validationInitiatedDesc": "Andmestiku fail laaditi edukalt üles. Kontroll ja eeltöötlus on nüüd alanud", + "viewValidations": "Vaata valideerimise sessioone", + "fieldName": "Sisesta välja nimi", + "fieldNameError": "{{name}} ei saa kasutada välja nime", + "fieldNameExist": "{{name}} on juba olemas välja nimeena", + "selectDataType": "Vali andme tüüp", + "table": { + "id": "rowId", + "data": "Andmed", + "label": "Märgis", + "actions": "Tegevused" + }, + "validationsTitle": "Andmestiku grupi kontrollid", + "classHierarchy": "Klassi hierarhiad", + "delete": "Kusta andmestik", + "modals": { + "import": { + "title": "Impordi uusi andmeid", + "fileFormatlabel": "Vali faili formaat", + "attachments": "Lisad", + "maxSize": "Maksimaalne faili suurus - 10 MB", + "browse": "Sirvi faili", + "import": "Impordi", + "cancel": "Tühista", + "uploadInProgress": "Üleslaadimine käib...", + "uploadDesc": "Andmestiku üleslaadimine. Palun oota, kuni laadimine lõpeb. Kui tühistad poole pealt, kaovad andmed.", + "invalidFile": "Kehtetu faili formaat", + "invalidFileDesc": "Laaditud fail ei ole õiges {{format}} formaadis. Palun laadi üles kehtiv {{format}} fail ja proovi uuesti." + }, + "export": { + "export": "Ekspordi andmed", + "exportButton": "Ekspordi", + "fileFormatlabel": "Vali faili formaat", + "title": "Andmete eksportimine õnnestus", + "description": "Sinu andmed on edukalt eksporditud." + }, + "delete": { + "title": "Oled sa kindel?", + "description": "Kui kustutad andmestiku, muutuvad kõik sellega ühendatud mudelid treenimisvõimetuks. Kas oled kindel, et soovid jätkata?", + "error": "Andmestiku grupi kustutamine ebaõnnestus", + "errorDesc": "Andmekogumi rühma kustutamisel ilmnes probleem. Palun proovige uuesti. Kui probleem püsib, võtke abi saamiseks ühendust toega." + }, + "edit": { + "title": "Redigeeri", + "data": "Andmed", + "label": "Märgis", + "update": "Uuenda", + "error": "Andmestiku grupi uuendamine ebaõnnestus", + "errorDesc": "Andmekogumi rühma värskendamisel ilmnes probleem. Palun proovige uuesti. Kui probleem püsib, võtke abi saamiseks ühendust toega." + }, + "upload": { + "title": "Andmete üleslaadimine õnnestus", + "desc": "Andmestiku fail laaditi edukalt üles. Palun salvesta muudatused, et alustada andmete kontrollimist ja eeltöötlust" + }, + "datasetDelete": { + "confirmationTitle": "Oled sa kindel?", + "confirmationDesc": "Kinnita, et soovid kustutada andmestiku", + "successTitle": "Edu - andmestik kustutatud", + "successDesc": "Oled edukalt kustutanud andmestiku. Andmestik ei ole enam saadaval ja kõik seotud andmed on eemaldatud.", + "proceedToDashboard": "Jätkake andmekogumite gruppidega" + } + } + } + }, + "stopWords": { + "title": "Stop-sõnad", + "import": "Impordi stop-sõnad", + "stopWordInputHint": "Sisesta stop-sõna", + "add": "Lisa", + "importModal": { + "title": "Impordi stop-sõnu", + "importButton": "Impordi", + "selectionLabel": "Vali järgmine valik", + "addOption": "Impordi, et lisada", + "updateOption": "Impordi, et uuendada", + "deleteOption": "Impordi, et kustutada", + "attachements": "Lisad (TXT, XLSX, YAML, JSON)", + "inprogressTitle": "Importimine käib", + "inprogressDesc": "Stop-sõnade importimine käib. Palun oota, kuni protsess lõpeb.", + "successTitle": "Andmete importimine õnnestus", + "successDesc": "Sinu andmed on edukalt imporditud.", + "unsuccessTitle": "Andmete importimine ebaõnnestus", + "unsuccessDesc": "Stop-sõnade importimine ebaõnnestus" + } + }, + "validationSessions": { + "title": "Valideerimise sessioonid", + "inprogress": "Kontroll käib", + "fail": "Kontroll ebaõnnestus, kuna {{class}} klassi ei leitud {{column}} veerus hierarhias", + "noSessions": "Käimasolevaid valideerimissessioone pole saadaval" + }, + "correctedTexts": { + "title": "Parandatud tekstid", + "export": "Ekspordi andmed", + "searchIncomingText": "Otsi sissetulevaid tekste", + "filterAsc": "Filtreeri kuupäeva järgi - kasvav", + "filterDesc": "Filtreeri kuupäeva järgi - kahanev", + "platform": "Platvorm", + "dateAndTime": "Kuupäev & aeg", + "inferenceTime": "Järeldamise aeg", + "text": "Tekst", + "predictedHierarchy": "Prognoositud klassihierarhia", + "predictedConfidenceProbability": "Prognoositud klasside keskmine usaldusväärsuse tõenäosus", + "correctedHierarchy": "Parandatud klassihierarhia", + "correctedConfidenceProbability": "Parandatud klasside keskmine usaldusväärsuse tõenäosus", + "labelNotFoundText": "Märgistus ei ole andmekogumis", + "exportSuccessTitle": "Andmete eksportimine õnnestus", + "exportSuccessDesc": "Sinu andmed on edukalt eksporditud.", + "exportDataUnsucessTitle": "Andmete eksportimine ebaõnnestus", + "exportDataUnsucessDesc": "Midagi läks valesti. Palun proovi uuesti." + }, + "dataModels": { + "productionModels": "Toodangumudelid", + "dataModels": "Andmemudelid", + "createModel": "Loo mudel", + "noProdModels": "Toodangumudeleid pole saadaval", + "noModels": "Mudeleid pole saadaval", + "sortOptions": { + "dataModelAsc": "Andmemudeli nimi A-Z", + "dataModelDesc": "Andmemudeli nimi Z-A", + "createdDateAsc": "Loomise kuupäev vanim enne", + "createdDateDesc": "Loomise kuupäev uusim enne" + }, + "filters": { + "modelName": "Mudeli nimi", + "version": "Versioon", + "platform": "Platvorm", + "datasetGroup": "Andmestiku grupp", + "trainingStatus": "Koolitusstaatus", + "maturity": "Valmidus", + "sort": "Sorteeri nime järgi (A - Z)" + }, + "trainingStatus": { + "retrainingNeeded": "Treening uuesti vajalik", + "trained": "Treenitud", + "trainingInProgress": "Treening käib", + "untrainable": "Ei saa treenida", + "notTrained": "Ei ole treenitud" + }, + "maturity": { + "development": "Arenduses", + "production": "Toodangus", + "staging": "Staging", + "testing": "Testimine" + }, + "dataModelCard": { + "datasetGroup": "Andmestiku grupp", + "dgVersion": "Andmestiku grupi versioon", + "lastTrained": "Viimane treening" + }, + "trainingResults": { + "title": "Treeningu tulemused", + "bestPerformingModel": "Parim esitusmudel", + "classes": "Klassid", + "accuracy": "Täpsus", + "f1Score": "F1 skoor", + "noResults": "Treeningu tulemusi pole saadaval", + "viewResults": "Vaata tulemusi" + }, + "createDataModel": { + "title": "Loo andmemudel", + "replaceTitle": "Hoiatus: asenda toodangumudel", + "replaceDesc": "Selle mudeli lisamine toodangusse asendab praeguse toodangumudeli. Kas oled kindel, et soovid jätkata?", + "successTitle": "Andmemudel loodud ja treeninguga alustatud", + "successDesc": "Oled edukalt loonud ja alustanud andmemudeli treenimist. Sa saad seda vaadata andmemudeli töölaualt.", + "viewAll": "Vaata kõiki andmemudeleid", + "errorTitle": "Viga andmemudeli loomisel", + "errorDesc": "Tekkis probleem andmemudeli loomise või koolitamise käigus. Palun proovi uuesti. Kui probleem püsib, võta ühendust toega.", + "replaceWarning": "{{platform}} integratsioon on hetkel keelatud, seega mudel ei saa mingeid sisendeid ega tee ennustusi" + }, + "configureDataModel": { + "saveChangesTitile": "Muudatused on edukalt salvestatud", + "saveChangesDesc": "Oled edukalt salvestanud muudatused. Saad andmemudelit vaadata „Kõik Andmemudelid“ vaates.", + "updateErrorTitile": "Viga Andmemudeli Uuendamisel", + "updateErrorDesc": "Tekkis probleem andmemudeli uuendamise käigus. Palun proovi uuesti. Kui probleem püsib, võta ühendust toega.", + "deleteErrorTitle": "Mudelit ei saaks kustutada", + "deleteErrorDesc": "Mudelit ei saa kustutada, kuna see on praegu toodangus. Palun edasta teine mudel toodangusse enne, kui jätkad selle mudeli kustutamist.", + "deleteConfirmation": "Kas oled kindel?", + "deleteConfirmationDesc": "Kinnita, et soovid kustutada andmemudeli", + "deleteModalErrorTitle": "Viga andmemudeli kustutamisel", + "deleteModalErrorDesc": "Andmemudeli kustutamise käigus tekkis probleem. Palun proovi uuesti. Kui probleem püsib, võta ühendust toega.", + "retrainDataModalErrorTitle": "Viga andmemudeli uuesti treenimisel", + "retrainDataModalErrorDesc": "Andmemudeli uuesti koolitamise käigus tekkis probleem. Palun proovi uuesti. Kui probleem püsib, võta ühendust toega.", + "title": "Seadista andmemudel", + "retrainCard": "Mudel uuendatud. Palun alusta uuesti koolitamist, et jätkata uusimate täiustuste saamist.", + "retrain": "Treeni uuesti", + "deleteModal": "Kustuta mudel", + "confirmRetrain": "Kinnita mudeli treenimine", + "confirmRetrainDesc": "Kas oled kindel, et soovid seda mudelit uuesti treenida?", + "save": "Salvesta muudatused" + }, + "dataModelForm": { + "modelVersion": "Mudeli versioon", + "datasetGroup": "Vali andmestiku grupp", + "baseModels": "Vali baasmudelid", + "deploymentPlatform": "Vali rakenduse platvorm", + "maturityLabel": "Vali valmiduse silt" + } + }, + "trainingSessions": { + "title": "Treening-sessioonid", + "inprogress": "Treening käib", + "fail": "Treening ebaõnnestus, kuna {{class}} klass, mida leiti {{column}} veerust, ei eksisteeri hierarhias", + "noSessions": "Aktiivsed treeningsessioonid puuduvad", + "noSessionsDesc": "Praegu ei ole ühtegi aktiivset treeningsessiooni. Kui alustate treeningsessiooni, ilmub see siia. Seniks saate alustada uue treeningsessiooniga, et alustada oma mudelite täiustamist." + }, + "testModels": { + "title": "Testige mudelit", + "selectionLabel": "Mudel", + "placeholder": "Valige mudel", + "classifyTextLabel": "Sisestage tekst", + "classify": "Klassifitseeri", + "predictedHierarchy": "Prognoositud klassihierarhia: ", + "averageConfidence": "Keskmine kindlus: ", + "classProbabilities": "Klassi tõenäosused: " + }, + "optionLists": { + "text": "Tekst", + "numbers": "Numbrid", + "dateTimes": "Kuupäev ja kellaaeg", + "email": "E-posti aadress", + "fileAttachements": "Faili lisad", + "importToAdd": "Impordi lisamiseks", + "importToDelete": "Impordi kustutamiseks", + "userManagement": "Kasutajate haldus", + "integration": "Integreerimine", + "dataset": "Andmekogum", + "dataModels": "Andmemudelid", + "classes": "Klassid", + "stopWords": "Stop-sõnad", + "incomingTexts": "Sissetulevad tekstid", + "testModel": "Testi mudelit" + } +} diff --git a/GUI/tsconfig.json b/GUI/tsconfig.json new file mode 100644 index 00000000..3dd4695d --- /dev/null +++ b/GUI/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "baseUrl": "src", + "types": [ + "vite/client", + "node" + ] + }, + "include": [ + "src" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/GUI/tsconfig.node.json b/GUI/tsconfig.node.json new file mode 100644 index 00000000..9d31e2ae --- /dev/null +++ b/GUI/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/GUI/vite.config.ts b/GUI/vite.config.ts new file mode 100644 index 00000000..a5582a37 --- /dev/null +++ b/GUI/vite.config.ts @@ -0,0 +1,43 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import svgr from 'vite-plugin-svgr'; +import path from 'path'; +import { removeHiddenMenuItems } from './vitePlugin'; + +// https://vitejs.dev/config/ +export default defineConfig({ + envPrefix: 'REACT_APP_', + plugins: [ + react(), + tsconfigPaths(), + svgr(), + { + name: 'removeHiddenMenuItemsPlugin', + transform: (str, id) => { + if(!id.endsWith('/menu-structure.json')) + return str; + return removeHiddenMenuItems(str); + }, + }, + ], + base: 'global-classifier', + build: { + outDir: './build', + target: 'es2015', + emptyOutDir: true, + }, + server: { + headers: { + ...(process.env.REACT_APP_CSP && { + 'Content-Security-Policy': process.env.REACT_APP_CSP, + }), + }, + }, + resolve: { + alias: { + '~@fontsource': path.resolve(__dirname, 'node_modules/@fontsource'), + '@': `${path.resolve(__dirname, './src')}`, + }, + }, +}); diff --git a/GUI/vite.config.ts.timestamp-1752210900745-20a2e6a47a881.mjs b/GUI/vite.config.ts.timestamp-1752210900745-20a2e6a47a881.mjs new file mode 100644 index 00000000..10db9fde --- /dev/null +++ b/GUI/vite.config.ts.timestamp-1752210900745-20a2e6a47a881.mjs @@ -0,0 +1,69 @@ +// vite.config.ts +import { defineConfig } from "file:///app/node_modules/vite/dist/node/index.js"; +import react from "file:///app/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import tsconfigPaths from "file:///app/node_modules/vite-tsconfig-paths/dist/index.mjs"; +import svgr from "file:///app/node_modules/vite-plugin-svgr/dist/index.mjs"; +import path from "path"; + +// vitePlugin.js +function removeHiddenMenuItems(str) { + var _a, _b; + const badJson = str.replace("export default [", "[").replace("];", "]"); + const correctJson = badJson.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": '); + const isHiddenFeaturesEnabled = ((_a = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _a.toLowerCase().trim()) == "true" || ((_b = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _b.toLowerCase().trim()) == "1"; + const json = removeHidden(JSON.parse(correctJson), isHiddenFeaturesEnabled); + const updatedJson = JSON.stringify(json); + return "export default " + updatedJson + ";"; +} +function removeHidden(menuItems, isHiddenFeaturesEnabled) { + var _a; + if (!menuItems) + return menuItems; + const arr = (_a = menuItems == null ? void 0 : menuItems.filter((x) => !x.hidden)) == null ? void 0 : _a.filter((x) => isHiddenFeaturesEnabled || x.hiddenMode !== "production"); + for (const a of arr) { + a.children = removeHidden(a.children, isHiddenFeaturesEnabled); + } + return arr; +} + +// vite.config.ts +var __vite_injected_original_dirname = "/app"; +var vite_config_default = defineConfig({ + envPrefix: "REACT_APP_", + plugins: [ + react(), + tsconfigPaths(), + svgr(), + { + name: "removeHiddenMenuItemsPlugin", + transform: (str, id) => { + if (!id.endsWith("/menu-structure.json")) + return str; + return removeHiddenMenuItems(str); + } + } + ], + base: "global-classifier", + build: { + outDir: "./build", + target: "es2015", + emptyOutDir: true + }, + server: { + headers: { + ...process.env.REACT_APP_CSP && { + "Content-Security-Policy": process.env.REACT_APP_CSP + } + } + }, + resolve: { + alias: { + "~@fontsource": path.resolve(__vite_injected_original_dirname, "node_modules/@fontsource"), + "@": `${path.resolve(__vite_injected_original_dirname, "./src")}` + } + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiLCAidml0ZVBsdWdpbi5qcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9hcHBcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9hcHAvdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2FwcC92aXRlLmNvbmZpZy50c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnO1xuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0JztcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnO1xuaW1wb3J0IHN2Z3IgZnJvbSAndml0ZS1wbHVnaW4tc3Zncic7XG5pbXBvcnQgcGF0aCBmcm9tICdwYXRoJztcbmltcG9ydCB7IHJlbW92ZUhpZGRlbk1lbnVJdGVtcyB9IGZyb20gJy4vdml0ZVBsdWdpbic7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBlbnZQcmVmaXg6ICdSRUFDVF9BUFBfJyxcbiAgcGx1Z2luczogW1xuICAgIHJlYWN0KCksXG4gICAgdHNjb25maWdQYXRocygpLFxuICAgIHN2Z3IoKSxcbiAgICB7XG4gICAgICBuYW1lOiAncmVtb3ZlSGlkZGVuTWVudUl0ZW1zUGx1Z2luJyxcbiAgICAgIHRyYW5zZm9ybTogKHN0ciwgaWQpID0+IHtcbiAgICAgICAgaWYoIWlkLmVuZHNXaXRoKCcvbWVudS1zdHJ1Y3R1cmUuanNvbicpKVxuICAgICAgICAgIHJldHVybiBzdHI7XG4gICAgICAgIHJldHVybiByZW1vdmVIaWRkZW5NZW51SXRlbXMoc3RyKTtcbiAgICAgIH0sXG4gICAgfSxcbiAgXSxcbiAgYmFzZTogJ2dsb2JhbC1jbGFzc2lmaWVyJyxcbiAgYnVpbGQ6IHtcbiAgICBvdXREaXI6ICcuL2J1aWxkJyxcbiAgICB0YXJnZXQ6ICdlczIwMTUnLFxuICAgIGVtcHR5T3V0RGlyOiB0cnVlLFxuICB9LFxuICBzZXJ2ZXI6IHtcbiAgICBoZWFkZXJzOiB7XG4gICAgICAuLi4ocHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0NTUCAmJiB7XG4gICAgICAgICdDb250ZW50LVNlY3VyaXR5LVBvbGljeSc6IHByb2Nlc3MuZW52LlJFQUNUX0FQUF9DU1AsXG4gICAgICB9KSxcbiAgICB9LFxuICB9LFxuICByZXNvbHZlOiB7XG4gICAgYWxpYXM6IHtcbiAgICAgICd+QGZvbnRzb3VyY2UnOiBwYXRoLnJlc29sdmUoX19kaXJuYW1lLCAnbm9kZV9tb2R1bGVzL0Bmb250c291cmNlJyksXG4gICAgICAnQCc6IGAke3BhdGgucmVzb2x2ZShfX2Rpcm5hbWUsICcuL3NyYycpfWAsXG4gICAgfSxcbiAgfSxcbn0pO1xuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvYXBwXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvYXBwL3ZpdGVQbHVnaW4uanNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2FwcC92aXRlUGx1Z2luLmpzXCI7ZXhwb3J0IGZ1bmN0aW9uIHJlbW92ZUhpZGRlbk1lbnVJdGVtcyhzdHIpIHtcbiAgY29uc3QgYmFkSnNvbiA9IHN0ci5yZXBsYWNlKCdleHBvcnQgZGVmYXVsdCBbJywgJ1snKS5yZXBsYWNlKCddOycsICddJyk7XG4gIGNvbnN0IGNvcnJlY3RKc29uID0gYmFkSnNvbi5yZXBsYWNlKC8oWydcIl0pPyhbYS16MC05QS1aX10rKShbJ1wiXSk/Oi9nLCAnXCIkMlwiOiAnKTtcblxuIGNvbnN0IGlzSGlkZGVuRmVhdHVyZXNFbmFibGVkID0gXG4gICAgcHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0VOQUJMRV9ISURERU5fRkVBVFVSRVM/LnRvTG93ZXJDYXNlKCkudHJpbSgpID09ICd0cnVlJyB8fFxuICAgIHByb2Nlc3MuZW52LlJFQUNUX0FQUF9FTkFCTEVfSElEREVOX0ZFQVRVUkVTPy50b0xvd2VyQ2FzZSgpLnRyaW0oKSA9PSAnMSc7XG5cbiAgY29uc3QganNvbiA9IHJlbW92ZUhpZGRlbihKU09OLnBhcnNlKGNvcnJlY3RKc29uKSwgaXNIaWRkZW5GZWF0dXJlc0VuYWJsZWQpO1xuICBcbiAgY29uc3QgdXBkYXRlZEpzb24gPSBKU09OLnN0cmluZ2lmeShqc29uKTtcblxuICByZXR1cm4gJ2V4cG9ydCBkZWZhdWx0ICcgKyB1cGRhdGVkSnNvbiArICc7J1xufVxuXG5mdW5jdGlvbiByZW1vdmVIaWRkZW4obWVudUl0ZW1zLCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCkge1xuICBpZighbWVudUl0ZW1zKSByZXR1cm4gbWVudUl0ZW1zO1xuICBjb25zdCBhcnIgPSBtZW51SXRlbXNcbiAgICA/LmZpbHRlcih4ID0+ICF4LmhpZGRlbilcbiAgICA/LmZpbHRlcih4ID0+IGlzSGlkZGVuRmVhdHVyZXNFbmFibGVkIHx8IHguaGlkZGVuTW9kZSAhPT0gXCJwcm9kdWN0aW9uXCIpO1xuICBmb3IgKGNvbnN0IGEgb2YgYXJyKSB7XG4gICAgYS5jaGlsZHJlbiA9IHJlbW92ZUhpZGRlbihhLmNoaWxkcmVuLCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCk7XG4gIH1cbiAgcmV0dXJuIGFycjtcbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBOEwsU0FBUyxvQkFBb0I7QUFDM04sT0FBTyxXQUFXO0FBQ2xCLE9BQU8sbUJBQW1CO0FBQzFCLE9BQU8sVUFBVTtBQUNqQixPQUFPLFVBQVU7OztBQ0prTCxTQUFTLHNCQUFzQixLQUFLO0FBQXZPO0FBQ0UsUUFBTSxVQUFVLElBQUksUUFBUSxvQkFBb0IsR0FBRyxFQUFFLFFBQVEsTUFBTSxHQUFHO0FBQ3RFLFFBQU0sY0FBYyxRQUFRLFFBQVEsbUNBQW1DLFFBQVE7QUFFaEYsUUFBTSw0QkFDSCxhQUFRLElBQUkscUNBQVosbUJBQThDLGNBQWMsV0FBVSxZQUN0RSxhQUFRLElBQUkscUNBQVosbUJBQThDLGNBQWMsV0FBVTtBQUV4RSxRQUFNLE9BQU8sYUFBYSxLQUFLLE1BQU0sV0FBVyxHQUFHLHVCQUF1QjtBQUUxRSxRQUFNLGNBQWMsS0FBSyxVQUFVLElBQUk7QUFFdkMsU0FBTyxvQkFBb0IsY0FBYztBQUMzQztBQUVBLFNBQVMsYUFBYSxXQUFXLHlCQUF5QjtBQWYxRDtBQWdCRSxNQUFHLENBQUM7QUFBVyxXQUFPO0FBQ3RCLFFBQU0sT0FBTSw0Q0FDUixPQUFPLE9BQUssQ0FBQyxFQUFFLFlBRFAsbUJBRVIsT0FBTyxPQUFLLDJCQUEyQixFQUFFLGVBQWU7QUFDNUQsYUFBVyxLQUFLLEtBQUs7QUFDbkIsTUFBRSxXQUFXLGFBQWEsRUFBRSxVQUFVLHVCQUF1QjtBQUFBLEVBQy9EO0FBQ0EsU0FBTztBQUNUOzs7QUR4QkEsSUFBTSxtQ0FBbUM7QUFRekMsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsV0FBVztBQUFBLEVBQ1gsU0FBUztBQUFBLElBQ1AsTUFBTTtBQUFBLElBQ04sY0FBYztBQUFBLElBQ2QsS0FBSztBQUFBLElBQ0w7QUFBQSxNQUNFLE1BQU07QUFBQSxNQUNOLFdBQVcsQ0FBQyxLQUFLLE9BQU87QUFDdEIsWUFBRyxDQUFDLEdBQUcsU0FBUyxzQkFBc0I7QUFDcEMsaUJBQU87QUFDVCxlQUFPLHNCQUFzQixHQUFHO0FBQUEsTUFDbEM7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUFBLEVBQ0EsTUFBTTtBQUFBLEVBQ04sT0FBTztBQUFBLElBQ0wsUUFBUTtBQUFBLElBQ1IsUUFBUTtBQUFBLElBQ1IsYUFBYTtBQUFBLEVBQ2Y7QUFBQSxFQUNBLFFBQVE7QUFBQSxJQUNOLFNBQVM7QUFBQSxNQUNQLEdBQUksUUFBUSxJQUFJLGlCQUFpQjtBQUFBLFFBQy9CLDJCQUEyQixRQUFRLElBQUk7QUFBQSxNQUN6QztBQUFBLElBQ0Y7QUFBQSxFQUNGO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDUCxPQUFPO0FBQUEsTUFDTCxnQkFBZ0IsS0FBSyxRQUFRLGtDQUFXLDBCQUEwQjtBQUFBLE1BQ2xFLEtBQUssR0FBRyxLQUFLLFFBQVEsa0NBQVcsT0FBTyxDQUFDO0FBQUEsSUFDMUM7QUFBQSxFQUNGO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/GUI/vite.config.ts.timestamp-1753147644693-4bdc02324667f.mjs b/GUI/vite.config.ts.timestamp-1753147644693-4bdc02324667f.mjs new file mode 100644 index 00000000..10db9fde --- /dev/null +++ b/GUI/vite.config.ts.timestamp-1753147644693-4bdc02324667f.mjs @@ -0,0 +1,69 @@ +// vite.config.ts +import { defineConfig } from "file:///app/node_modules/vite/dist/node/index.js"; +import react from "file:///app/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import tsconfigPaths from "file:///app/node_modules/vite-tsconfig-paths/dist/index.mjs"; +import svgr from "file:///app/node_modules/vite-plugin-svgr/dist/index.mjs"; +import path from "path"; + +// vitePlugin.js +function removeHiddenMenuItems(str) { + var _a, _b; + const badJson = str.replace("export default [", "[").replace("];", "]"); + const correctJson = badJson.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": '); + const isHiddenFeaturesEnabled = ((_a = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _a.toLowerCase().trim()) == "true" || ((_b = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _b.toLowerCase().trim()) == "1"; + const json = removeHidden(JSON.parse(correctJson), isHiddenFeaturesEnabled); + const updatedJson = JSON.stringify(json); + return "export default " + updatedJson + ";"; +} +function removeHidden(menuItems, isHiddenFeaturesEnabled) { + var _a; + if (!menuItems) + return menuItems; + const arr = (_a = menuItems == null ? void 0 : menuItems.filter((x) => !x.hidden)) == null ? void 0 : _a.filter((x) => isHiddenFeaturesEnabled || x.hiddenMode !== "production"); + for (const a of arr) { + a.children = removeHidden(a.children, isHiddenFeaturesEnabled); + } + return arr; +} + +// vite.config.ts +var __vite_injected_original_dirname = "/app"; +var vite_config_default = defineConfig({ + envPrefix: "REACT_APP_", + plugins: [ + react(), + tsconfigPaths(), + svgr(), + { + name: "removeHiddenMenuItemsPlugin", + transform: (str, id) => { + if (!id.endsWith("/menu-structure.json")) + return str; + return removeHiddenMenuItems(str); + } + } + ], + base: "global-classifier", + build: { + outDir: "./build", + target: "es2015", + emptyOutDir: true + }, + server: { + headers: { + ...process.env.REACT_APP_CSP && { + "Content-Security-Policy": process.env.REACT_APP_CSP + } + } + }, + resolve: { + alias: { + "~@fontsource": path.resolve(__vite_injected_original_dirname, "node_modules/@fontsource"), + "@": `${path.resolve(__vite_injected_original_dirname, "./src")}` + } + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiLCAidml0ZVBsdWdpbi5qcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9hcHBcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9hcHAvdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2FwcC92aXRlLmNvbmZpZy50c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnO1xuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0JztcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnO1xuaW1wb3J0IHN2Z3IgZnJvbSAndml0ZS1wbHVnaW4tc3Zncic7XG5pbXBvcnQgcGF0aCBmcm9tICdwYXRoJztcbmltcG9ydCB7IHJlbW92ZUhpZGRlbk1lbnVJdGVtcyB9IGZyb20gJy4vdml0ZVBsdWdpbic7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBlbnZQcmVmaXg6ICdSRUFDVF9BUFBfJyxcbiAgcGx1Z2luczogW1xuICAgIHJlYWN0KCksXG4gICAgdHNjb25maWdQYXRocygpLFxuICAgIHN2Z3IoKSxcbiAgICB7XG4gICAgICBuYW1lOiAncmVtb3ZlSGlkZGVuTWVudUl0ZW1zUGx1Z2luJyxcbiAgICAgIHRyYW5zZm9ybTogKHN0ciwgaWQpID0+IHtcbiAgICAgICAgaWYoIWlkLmVuZHNXaXRoKCcvbWVudS1zdHJ1Y3R1cmUuanNvbicpKVxuICAgICAgICAgIHJldHVybiBzdHI7XG4gICAgICAgIHJldHVybiByZW1vdmVIaWRkZW5NZW51SXRlbXMoc3RyKTtcbiAgICAgIH0sXG4gICAgfSxcbiAgXSxcbiAgYmFzZTogJ2dsb2JhbC1jbGFzc2lmaWVyJyxcbiAgYnVpbGQ6IHtcbiAgICBvdXREaXI6ICcuL2J1aWxkJyxcbiAgICB0YXJnZXQ6ICdlczIwMTUnLFxuICAgIGVtcHR5T3V0RGlyOiB0cnVlLFxuICB9LFxuICBzZXJ2ZXI6IHtcbiAgICBoZWFkZXJzOiB7XG4gICAgICAuLi4ocHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0NTUCAmJiB7XG4gICAgICAgICdDb250ZW50LVNlY3VyaXR5LVBvbGljeSc6IHByb2Nlc3MuZW52LlJFQUNUX0FQUF9DU1AsXG4gICAgICB9KSxcbiAgICB9LFxuICB9LFxuICByZXNvbHZlOiB7XG4gICAgYWxpYXM6IHtcbiAgICAgICd+QGZvbnRzb3VyY2UnOiBwYXRoLnJlc29sdmUoX19kaXJuYW1lLCAnbm9kZV9tb2R1bGVzL0Bmb250c291cmNlJyksXG4gICAgICAnQCc6IGAke3BhdGgucmVzb2x2ZShfX2Rpcm5hbWUsICcuL3NyYycpfWAsXG4gICAgfSxcbiAgfSxcbn0pO1xuIiwgImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvYXBwXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvYXBwL3ZpdGVQbHVnaW4uanNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2FwcC92aXRlUGx1Z2luLmpzXCI7ZXhwb3J0IGZ1bmN0aW9uIHJlbW92ZUhpZGRlbk1lbnVJdGVtcyhzdHIpIHtcbiAgY29uc3QgYmFkSnNvbiA9IHN0ci5yZXBsYWNlKCdleHBvcnQgZGVmYXVsdCBbJywgJ1snKS5yZXBsYWNlKCddOycsICddJyk7XG4gIGNvbnN0IGNvcnJlY3RKc29uID0gYmFkSnNvbi5yZXBsYWNlKC8oWydcIl0pPyhbYS16MC05QS1aX10rKShbJ1wiXSk/Oi9nLCAnXCIkMlwiOiAnKTtcblxuIGNvbnN0IGlzSGlkZGVuRmVhdHVyZXNFbmFibGVkID0gXG4gICAgcHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0VOQUJMRV9ISURERU5fRkVBVFVSRVM/LnRvTG93ZXJDYXNlKCkudHJpbSgpID09ICd0cnVlJyB8fFxuICAgIHByb2Nlc3MuZW52LlJFQUNUX0FQUF9FTkFCTEVfSElEREVOX0ZFQVRVUkVTPy50b0xvd2VyQ2FzZSgpLnRyaW0oKSA9PSAnMSc7XG5cbiAgY29uc3QganNvbiA9IHJlbW92ZUhpZGRlbihKU09OLnBhcnNlKGNvcnJlY3RKc29uKSwgaXNIaWRkZW5GZWF0dXJlc0VuYWJsZWQpO1xuICBcbiAgY29uc3QgdXBkYXRlZEpzb24gPSBKU09OLnN0cmluZ2lmeShqc29uKTtcblxuICByZXR1cm4gJ2V4cG9ydCBkZWZhdWx0ICcgKyB1cGRhdGVkSnNvbiArICc7J1xufVxuXG5mdW5jdGlvbiByZW1vdmVIaWRkZW4obWVudUl0ZW1zLCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCkge1xuICBpZighbWVudUl0ZW1zKSByZXR1cm4gbWVudUl0ZW1zO1xuICBjb25zdCBhcnIgPSBtZW51SXRlbXNcbiAgICA/LmZpbHRlcih4ID0+ICF4LmhpZGRlbilcbiAgICA/LmZpbHRlcih4ID0+IGlzSGlkZGVuRmVhdHVyZXNFbmFibGVkIHx8IHguaGlkZGVuTW9kZSAhPT0gXCJwcm9kdWN0aW9uXCIpO1xuICBmb3IgKGNvbnN0IGEgb2YgYXJyKSB7XG4gICAgYS5jaGlsZHJlbiA9IHJlbW92ZUhpZGRlbihhLmNoaWxkcmVuLCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCk7XG4gIH1cbiAgcmV0dXJuIGFycjtcbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBOEwsU0FBUyxvQkFBb0I7QUFDM04sT0FBTyxXQUFXO0FBQ2xCLE9BQU8sbUJBQW1CO0FBQzFCLE9BQU8sVUFBVTtBQUNqQixPQUFPLFVBQVU7OztBQ0prTCxTQUFTLHNCQUFzQixLQUFLO0FBQXZPO0FBQ0UsUUFBTSxVQUFVLElBQUksUUFBUSxvQkFBb0IsR0FBRyxFQUFFLFFBQVEsTUFBTSxHQUFHO0FBQ3RFLFFBQU0sY0FBYyxRQUFRLFFBQVEsbUNBQW1DLFFBQVE7QUFFaEYsUUFBTSw0QkFDSCxhQUFRLElBQUkscUNBQVosbUJBQThDLGNBQWMsV0FBVSxZQUN0RSxhQUFRLElBQUkscUNBQVosbUJBQThDLGNBQWMsV0FBVTtBQUV4RSxRQUFNLE9BQU8sYUFBYSxLQUFLLE1BQU0sV0FBVyxHQUFHLHVCQUF1QjtBQUUxRSxRQUFNLGNBQWMsS0FBSyxVQUFVLElBQUk7QUFFdkMsU0FBTyxvQkFBb0IsY0FBYztBQUMzQztBQUVBLFNBQVMsYUFBYSxXQUFXLHlCQUF5QjtBQWYxRDtBQWdCRSxNQUFHLENBQUM7QUFBVyxXQUFPO0FBQ3RCLFFBQU0sT0FBTSw0Q0FDUixPQUFPLE9BQUssQ0FBQyxFQUFFLFlBRFAsbUJBRVIsT0FBTyxPQUFLLDJCQUEyQixFQUFFLGVBQWU7QUFDNUQsYUFBVyxLQUFLLEtBQUs7QUFDbkIsTUFBRSxXQUFXLGFBQWEsRUFBRSxVQUFVLHVCQUF1QjtBQUFBLEVBQy9EO0FBQ0EsU0FBTztBQUNUOzs7QUR4QkEsSUFBTSxtQ0FBbUM7QUFRekMsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsV0FBVztBQUFBLEVBQ1gsU0FBUztBQUFBLElBQ1AsTUFBTTtBQUFBLElBQ04sY0FBYztBQUFBLElBQ2QsS0FBSztBQUFBLElBQ0w7QUFBQSxNQUNFLE1BQU07QUFBQSxNQUNOLFdBQVcsQ0FBQyxLQUFLLE9BQU87QUFDdEIsWUFBRyxDQUFDLEdBQUcsU0FBUyxzQkFBc0I7QUFDcEMsaUJBQU87QUFDVCxlQUFPLHNCQUFzQixHQUFHO0FBQUEsTUFDbEM7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUFBLEVBQ0EsTUFBTTtBQUFBLEVBQ04sT0FBTztBQUFBLElBQ0wsUUFBUTtBQUFBLElBQ1IsUUFBUTtBQUFBLElBQ1IsYUFBYTtBQUFBLEVBQ2Y7QUFBQSxFQUNBLFFBQVE7QUFBQSxJQUNOLFNBQVM7QUFBQSxNQUNQLEdBQUksUUFBUSxJQUFJLGlCQUFpQjtBQUFBLFFBQy9CLDJCQUEyQixRQUFRLElBQUk7QUFBQSxNQUN6QztBQUFBLElBQ0Y7QUFBQSxFQUNGO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDUCxPQUFPO0FBQUEsTUFDTCxnQkFBZ0IsS0FBSyxRQUFRLGtDQUFXLDBCQUEwQjtBQUFBLE1BQ2xFLEtBQUssR0FBRyxLQUFLLFFBQVEsa0NBQVcsT0FBTyxDQUFDO0FBQUEsSUFDMUM7QUFBQSxFQUNGO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K diff --git a/GUI/vitePlugin.js b/GUI/vitePlugin.js new file mode 100644 index 00000000..80cf7a0e --- /dev/null +++ b/GUI/vitePlugin.js @@ -0,0 +1,25 @@ +export function removeHiddenMenuItems(str) { + const badJson = str.replace('export default [', '[').replace('];', ']'); + const correctJson = badJson.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": '); + + const isHiddenFeaturesEnabled = + process.env.REACT_APP_ENABLE_HIDDEN_FEATURES?.toLowerCase().trim() == 'true' || + process.env.REACT_APP_ENABLE_HIDDEN_FEATURES?.toLowerCase().trim() == '1'; + + const json = removeHidden(JSON.parse(correctJson), isHiddenFeaturesEnabled); + + const updatedJson = JSON.stringify(json); + + return 'export default ' + updatedJson + ';' +} + +function removeHidden(menuItems, isHiddenFeaturesEnabled) { + if(!menuItems) return menuItems; + const arr = menuItems + ?.filter(x => !x.hidden) + ?.filter(x => isHiddenFeaturesEnabled || x.hiddenMode !== "production"); + for (const a of arr) { + a.children = removeHidden(a.children, isHiddenFeaturesEnabled); + } + return arr; +} diff --git a/README.md b/README.md index 6d1cd3b1..546497ac 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,26 @@ Windows: ``` +## Cloning and setting up BYK stack + +- Clone [Ruuter](https://github.com/buerokratt/Ruuter) +- Navigate to Ruuter and build the image using the command `docker build -t ruuter .` +- Clone [Resql](https://github.com/buerokratt/Resql) +- Navigate to Resql and build the image `docker build -t resql .` +- Clone [Data Mapper](https://github.com/buerokratt/DataMapper) +- Navigate to Data Mapper and build the image using the command `docker build -t data-mapper .` +- Clone [TIM](https://github.com/buerokratt/TIM) +- Navigate to TIM and build the image using the command `docker build -t tim .` +- Clone [Authentication Layer](https://github.com/buerokratt/Authentication-layer) +- Go to public/env-config.js and update the RUUTER_API_URL to 'http://localhost:8086/classifier' +- Navigate to Authentication Layer, checkout to the `dev` branch and build the image using the command `docker build -f Dockerfile.dev -t authentication-layer .` +- Clone [S3 Ferry](https://github.com/buerokratt/S3-Ferry) +- Navigate to S3-Ferry and build the image using the command `docker build -t s3-ferry .` +- Clone [Cron Manager](https://github.com/buerokratt/CronManager) +- Navigate to Cron Manager `dev` branch and build the cron-manager-python image using the command `docker build -f Dockerfile.python -t cron-manager-python .` + + + ## Contributing This section outlines the guidelines for contributing to the Global Classifier project. Please read through these before submitting any changes. diff --git a/config.env b/config.env new file mode 100644 index 00000000..e5eb24e6 --- /dev/null +++ b/config.env @@ -0,0 +1,17 @@ +API_CORS_ORIGIN=* +API_DOCUMENTATION_ENABLED=true +S3_REGION=eu-west-1 +S3_ENDPOINT_URL=http://minio:9000 +S3_ENDPOINT_NAME=minio:9000 +S3_DATA_BUCKET_PATH=resources +S3_DATA_BUCKET_NAME=global-classifier +FS_DATA_DIRECTORY_PATH=/app +S3_SECRET_ACCESS_KEY=minioadmin +S3_ACCESS_KEY_ID=minioadmin +S3_HEALTH_ENDPOINT=http://minio:9000/minio/health/live +MINIO_BROWSER_REDIRECT_URL=http://localhost:9001 +GF_SECURITY_ADMIN_USER=admin +GF_SECURITY_ADMIN_PASSWORD=admin123 +GF_USERS_ALLOW_SIGN_UP=false +PORT=3000 + diff --git a/constants.ini b/constants.ini index 3261da6d..cee82f80 100644 --- a/constants.ini +++ b/constants.ini @@ -9,7 +9,11 @@ GLOBAL_CLASSIFIER_CRON_MANAGER=http://cron-manager:9010 GLOBAL_CLASSIFIER_FILE_HANDLER=http://file-handler:8000 GLOBAL_CLASSIFIER_NOTIFICATIONS=http://notifications-node:4040 GLOBAL_CLASSIFIER_ANONYMIZER=http://anonymizer:8010 -GLOBAL_CLASSIFIER_MODEL_INFERENCE=http://model-inference:8003 +GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER=http://triton-test-server:8000 +GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_METRICS=http://triton-test-server:8001 +GLOBAL_CLASSIFIER_PRODUCTION_ENV_MODEL_SERVER=http://triton-production-server:8000 +GLOBAL_CLASSIFIER_PRODUCTION_ENV_MODEL_METRICS=http://triton-production-server:8001 DOMAIN=localhost DB_PASSWORD=dbadmin -CHATBOT_CLASSIFIER_SERVICE=http://classifier-service:8090 \ No newline at end of file +CHATBOT_CLASSIFIER_SERVICE=http://classifier-service:8090 +GLOBAL_CLASSIFIER_S3_FERRY=http://gc-s3-ferry:3000 \ No newline at end of file diff --git a/docker-compose-dataset-generation.yml b/docker-compose-dataset-generation.yml new file mode 100644 index 00000000..789978a3 --- /dev/null +++ b/docker-compose-dataset-generation.yml @@ -0,0 +1,55 @@ +version: "3.8" + +services: + dataset-gen-service: + image: synthesisai/dataset-generator:latest + container_name: dataset-gen-service + ports: + - "8000:8000" + environment: + - PROVIDER_API_URL=http://dataset-gen-ollama:11434 + - SERVICE_DEBUG=false + - MLFLOW_TRACKING_URI=http://dataset-gen-mlflow:5000 + volumes: + - ./DSL/DatasetGenerator/config:/app/config + - ./DSL/DatasetGenerator/templates:/app/templates + - ./DSL/DatasetGenerator/user_configs:/app/user_configs + - cron_data:/app/data + - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets + - ./DSL/DatasetGenerator/logs:/app/logs + depends_on: + - dataset-gen-ollama + networks: + - bykstack + + dataset-gen-ollama: + image: synthesisai/dataset-generator-ollama:latest + container_name: dataset-gen-ollama + ports: + - "11434:11434" + environment: + - NVIDIA_VISIBLE_DEVICES=all + - OLLAMA_USE_GPU=1 + - OLLAMA_HOST=0.0.0.0 + volumes: + - dataset_gen_ollama_models:/root/.ollama + - ./DSL/DatasetGenerator/ollama-entrypoint.sh:/ollama-entrypoint.sh + entrypoint: ["bash", "/ollama-entrypoint.sh"] + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + networks: + - bykstack + +volumes: + dataset_gen_ollama_models: + cron_data: + +networks: + bykstack: + name: bykstack + driver: bridge \ No newline at end of file diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 00000000..23ce707d --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,582 @@ + +services: + +# Docker compose without dataset generator services + ruuter-public: + container_name: ruuter-public + image: ruuter + environment: + - application.cors.allowedOrigins=http://localhost:8086,http://localhost:3001,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8000,http://localhost:8090 + - application.httpCodesAllowList=200,201,202,204,400,401,403,500 + - application.internalRequests.allowedIPs=127.0.0.1 + - application.logging.displayRequestContent=true + - application.logging.displayResponseContent=true + - application.logging.printStackTrace=true + - application.internalRequests.disabled=true + - server.port=8086 + volumes: + - ./DSL/Ruuter.public:/DSL + - ./constants.ini:/app/constants.ini + ports: + - 8086:8086 + networks: + - bykstack + cpus: "0.5" + mem_limit: "512M" + + ruuter-private: + container_name: ruuter-private + image: ruuter + environment: + - application.cors.allowedOrigins=http://localhost:3001,http://localhost:3003,http://localhost:8088,http://localhost:3002,http://localhost:3004,http://localhost:8000 + - application.httpCodesAllowList=200,201,202,400,401,403,500 + - application.internalRequests.allowedIPs=127.0.0.1 + - application.logging.displayRequestContent=true + - application.logging.displayResponseContent=true + - application.logging.printStackTrace=true + - application.internalRequests.disabled=true + - server.port=8088 + volumes: + - ./DSL/Ruuter.private:/DSL + - ./constants.ini:/app/constants.ini + ports: + - 8088:8088 + networks: + - bykstack + cpus: "0.5" + mem_limit: "512M" + + data-mapper: + container_name: data-mapper + image: data-mapper + environment: + - PORT=3000 + - CONTENT_FOLDER=/data + volumes: + - ./DSL:/data + - ./DSL/DMapper/global-classifier/hbs:/workspace/app/views/global-classifier + - ./DSL/DMapper/global-classifier/lib:/workspace/app/lib + ports: + - 3000:3000 + networks: + - bykstack + + tim: + container_name: tim + image: tim + depends_on: + tim-postgresql: + condition: service_started + environment: + - SECURITY_ALLOWLIST_JWT=ruuter-private,ruuter-public,data-mapper,resql,tim,tim-postgresql,chat-widget,authentication-layer,127.0.0.1,::1 + - KEY_PASS=ppjjpp + ports: + - 8085:8085 + networks: + - bykstack + extra_hosts: + - "host.docker.internal:host-gateway" + cpus: "0.5" + mem_limit: "512M" + + tim-postgresql: + container_name: tim-postgresql + image: postgres:14.1 + environment: + - POSTGRES_USER=tim + - POSTGRES_PASSWORD=123 + - POSTGRES_DB=tim + # - POSTGRES_HOST_AUTH_METHOD=trust + volumes: + - ./tim-db:/var/lib/postgresql/data + ports: + - 9876:5432 + networks: + - bykstack + + authentication-layer: + container_name: authentication-layer + image: authentication-layer + ports: + - 3004:3004 + networks: + - bykstack + + resql: + container_name: resql + image: resql + depends_on: + users_db: + condition: service_started + environment: + - sqlms.datasources.[0].name=byk + - sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://users_db:5432/global-classifier #For LocalDb Use + # sqlms.datasources.[0].jdbcUrl=jdbc:postgresql://171.22.247.13:5435/byk?sslmode=require + - sqlms.datasources.[0].username=postgres + - sqlms.datasources.[0].password=dbadmin + - logging.level.org.springframework.boot=INFO + ports: + - 8082:8082 + volumes: + - ./DSL/Resql:/DSL + - ./shared:/shared + - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets + networks: + - bykstack + + users_db: + container_name: users_db + image: postgres:14.1 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=dbadmin + - POSTGRES_DB=global-classifier + ports: + - 5435:5432 + volumes: + - ./global-classifier-db/db_files:/var/lib/postgresql/data + - ./shared:/shared + - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets + + networks: + - bykstack + restart: always + + init: + image: busybox + command: ["sh", "-c", "chmod -R 777 /shared && chmod -R 777 /app/model_trainer && chmod -R 777 /app/data"] + volumes: + - shared-volume:/shared + - ./model_trainer:/app/model_trainer + - cron_data:/app/data + networks: + - bykstack + +# It's important to build the cron-manager container from the Dockerfile.python containing the Python enviro + cron-manager: + container_name: cron-manager + image: cron-manager-python:latest + user: "root" + volumes: + - ./DSL/CronManager/DSL:/DSL + - ./DSL/CronManager/script:/app/scripts + - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets + - ./src/s3_dataset_processor:/app/src/s3_dataset_processor + - ./DSL/DatasetGenerator/config:/app/config + - ./src/model-training:/app/src/training + - ./mlflow/mlflow_artifacts:/mlflow/mlflow_artifacts + - ./data/processed:/app/data/processed + - shared-volume:/app/shared + - ./models:/app/models + - ./src/inference/inference_scripts:/app/inference_scripts + - ./grafana-configs/loki_logger.py:/app/inference_scripts/loki_logger.py + - ./grafana-configs/loki_logger.py:/app/src/training/loki_logger.py + - ./constants.ini:/app/inference_scripts/constants.ini + - cron_data:/app/data + runtime: nvidia + environment: + - NVIDIA_VISIBLE_DEVICES=all + - server.port=9010 + - MLFLOW_TRACKING_URI=http://mlflow:5000 + - PYTHONPATH=/app:/app/src/model_training:/app/src/s3_dataset_processor:/app/src + ports: + - 9010:8080 + networks: + - bykstack + depends_on: + init: + condition: service_completed_successfully + + mlflow: + build: + context: ./mlflow + dockerfile: Dockerfile + container_name: mlflow + ports: + - "5001:5000" + env_file: + - sidecar.env + environment: + - MLFLOW_TRACKING_USERNAME=${MLFLOW_TRACKING_USERNAME} + - MLFLOW_TRACKING_PASSWORD=${MLFLOW_TRACKING_PASSWORD} + - MLFLOW_HOST=${MLFLOW_HOST} + - MLFLOW_PORT=${MLFLOW_PORT} + - MLFLOW_BACKEND_STORE_URI=${MLFLOW_BACKEND_STORE_URI} + - MLFLOW_DEFAULT_ARTIFACT_ROOT=${MLFLOW_DEFAULT_ARTIFACT_ROOT} + - MLFLOW_FLASK_SERVER_SECRET_KEY=${MLFLOW_FLASK_SERVER_SECRET_KEY} + volumes: + - ./mlflow/mlflow_data:/mlflow/mlflow_data + - ./mlflow/mlflow_artifacts:/mlflow/mlflow_artifacts + networks: + - bykstack + + minio: + image: minio/minio:latest + container_name: minio + env_file: + - config.env + + # command: server /data --console-address ":9001" + entrypoint: > + sh -c " + export MINIO_ROOT_USER=$${S3_ACCESS_KEY_ID} && \ + export MINIO_ROOT_PASSWORD=$${S3_SECRET_ACCESS_KEY} && \ + export MINIO_BROWSER_REDIRECT_URL=$${MINIO_BROWSER_REDIRECT_URL} && \ + + minio server /app/minio_data --console-address ":9001" + " + + volumes: + - minio_data:/app/minio_data + ports: + - "9000:9000" # API port + - "9001:9001" # Console port + networks: + - bykstack + + gc-s3-ferry: + image: s3-ferry:latest + container_name: gc-s3-ferry + volumes: + - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets + - shared-volume:/app/shared + - ./models:/app/models + - ./src/inference/dummy_model:/app/dummy_model + - cron_data:/app/data + env_file: + - config.env + ports: + - "3006:3000" + user: "root" + networks: + - bykstack + depends_on: + minio: + condition: service_started + + opensearch-node: + image: opensearchproject/opensearch:2.11.1 + container_name: opensearch-node + environment: + - node.name=opensearch-node + - discovery.seed_hosts=opensearch + - discovery.type=single-node + - bootstrap.memory_lock=true + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" + - plugins.security.disabled=true + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + volumes: + - opensearch-data:/usr/share/opensearch/data + ports: + - 9200:9200 + - 9600:9600 + networks: + - bykstack + + notifications-node: + container_name: notifications-node + build: + context: ./notification-server + dockerfile: Dockerfile + ports: + - 4040:4040 + depends_on: + - opensearch-node + environment: + OPENSEARCH_PROTOCOL: http + OPENSEARCH_HOST: opensearch-node + OPENSEARCH_PORT: 9200 + OPENSEARCH_USERNAME: admin + OPENSEARCH_PASSWORD: admin + PORT: 4040 + REFRESH_INTERVAL: 1000 + CORS_WHITELIST_ORIGINS: http://localhost:3001,http://localhost:3002,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8088 + RUUTER_URL: http://ruuter-public:8086 + volumes: + - /app/node_modules + - ./notification-server:/app + networks: + - bykstack + + opensearch-dashboards: + image: opensearchproject/opensearch-dashboards:2.11.1 + container_name: opensearch-dashboards + environment: + - OPENSEARCH_HOSTS=http://opensearch-node:9200 + - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true + ports: + - 5601:5601 + networks: + - bykstack + + gui: + container_name: gui + environment: + - NODE_ENV=local + - REACT_APP_RUUTER_API_URL=http://localhost:8086 + - REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 + - REACT_APP_EXTERNAL_API_URL=http://localhost:8000 + - REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth + - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 + - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3001 http://localhost:8000; + - DEBUG_ENABLED=true + - CHOKIDAR_USEPOLLING=true + - PORT=3001 + - REACT_APP_SERVICE_ID=conversations,settings,monitoring + - REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE + + build: + context: ./GUI + dockerfile: Dockerfile.dev + ports: + - 3003:3001 + volumes: + - /app/node_modules + - ./GUI:/app + networks: + - bykstack + cpus: "0.5" + mem_limit: "1G" + + dataset-gen-service: + image: synthesisai/dataset-generator:latest + container_name: dataset-gen-service + ports: + - "8000:8000" + environment: + - PROVIDER_API_URL=http://dataset-gen-ollama:11434 + - SERVICE_DEBUG=false + - MLFLOW_TRACKING_URI=http://dataset-gen-mlflow:5000 + volumes: + - ./DSL/DatasetGenerator/config:/app/config + - ./DSL/DatasetGenerator/templates:/app/templates + - ./DSL/DatasetGenerator/user_configs:/app/user_configs + - cron_data:/app/data + - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets + - ./DSL/DatasetGenerator/logs:/app/logs + depends_on: + - dataset-gen-ollama + networks: + - bykstack + + dataset-gen-ollama: + image: synthesisai/dataset-generator-ollama:latest + container_name: dataset-gen-ollama + ports: + - "11434:11434" + environment: + - NVIDIA_VISIBLE_DEVICES=all + - OLLAMA_USE_GPU=1 + - OLLAMA_HOST=0.0.0.0 + volumes: + - dataset_gen_ollama_models:/root/.ollama + - ./DSL/DatasetGenerator/ollama-entrypoint.sh:/ollama-entrypoint.sh + entrypoint: ["bash", "/ollama-entrypoint.sh"] + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + networks: + - bykstack + + #temporarary container to initialize S3 storage with necessary buckets and models + init-storage: + + container_name: init-storage + image: amazon/aws-cli:latest + volumes: + - cron_data:/data_models + - ./src/inference/model-repository:/app/model-repository + entrypoint: > + sh -c " + echo 'Setting AWS KEYS' && + aws configure set aws_access_key_id $${S3_ACCESS_KEY_ID} && + aws configure set aws_secret_access_key $${S3_SECRET_ACCESS_KEY} && + + echo \"WAITING for S3 storage to be ready...\" && + until curl -s $${S3_HEALTH_ENDPOINT}; do sleep 2; done && + + echo \"Checking for S3 bucket: $${S3_DATA_BUCKET_NAME} ...\" && + if ! aws --endpoint-url $${S3_ENDPOINT_URL} s3api head-bucket --bucket $${S3_DATA_BUCKET_NAME} 2>/dev/null; then + echo \"Bucket $${S3_DATA_BUCKET_NAME} does not exist. Creating...\" && + aws --endpoint-url $${S3_ENDPOINT_URL} s3 mb s3://$${S3_DATA_BUCKET_NAME} && + echo \"Bucket $${S3_DATA_BUCKET_NAME} created successfully.\" + else + echo \"Bucket $${S3_DATA_BUCKET_NAME} already exists. Skipping creation.\" + fi && + + + echo 'S3 storage is ready.' && + echo 'Uploading models...' && + + echo 'Uploading initial dummy model to testing registry...' && + aws --endpoint-url $${S3_ENDPOINT_URL} s3 cp --recursive /app/model-repository/testing s3://$${S3_DATA_BUCKET_NAME}/$${S3_DATA_BUCKET_PATH}/models/testing && + + echo 'Uploading initial dummy model to production registry...' && + aws --endpoint-url $${S3_ENDPOINT_URL} s3 cp --recursive /app/model-repository/production s3://$${S3_DATA_BUCKET_NAME}/$${S3_DATA_BUCKET_PATH}/models/production && + + echo 'Uploading initial dummy model to undeployed registry...' && + aws --endpoint-url $${S3_ENDPOINT_URL} s3 cp --recursive /app/model-repository/undeployed s3://$${S3_DATA_BUCKET_NAME}/$${S3_DATA_BUCKET_PATH}/models/undeployed && + + echo 'Upload complete.' + " + + env_file: + - config.env + networks: + - bykstack + depends_on: + minio: + condition: service_started + cron-manager: + condition: service_started + ruuter-private: + condition: service_started + ruuter-public: + condition: service_started + gc-s3-ferry: + condition: service_started + + triton-production-server: + container_name: triton-production-server + runtime: nvidia + build: + context: ./src/inference + dockerfile: Dockerfile + command: > + sh -c " + echo 'Setting environment variables for S3 access...' && + export AWS_ACCESS_KEY_ID=$${S3_ACCESS_KEY_ID} && + export AWS_SECRET_ACCESS_KEY=$${S3_SECRET_ACCESS_KEY} && + export AWS_REGION=$${S3_REGION} && + export AWS_ENDPOINT_URL=$${S3_ENDPOINT_URL} && + + echo 'Starting Triton server for production models...' && + tritonserver --model-repository=s3://$${S3_ENDPOINT_NAME}/$${S3_DATA_BUCKET_NAME}/$${S3_DATA_BUCKET_PATH}/models/production --model-control-mode=explicit --log-verbose=1 + " + ports: + - "6000:8000" + - "6001:8001" + - "6002:8002" + volumes: + - cron_data:/data_models + + env_file: + - config.env + networks: + - bykstack + depends_on: + init-storage: + condition: service_completed_successfully + cron-manager: + condition: service_started + ruuter-private: + condition: service_started + minio: + condition: service_started + + triton-test-server: + container_name: triton-test-server + runtime: nvidia + build: + context: ./src/inference + dockerfile: Dockerfile + command: > + sh -c " + + echo 'Setting environment variables for S3 access...' && + + export AWS_ACCESS_KEY_ID=$${S3_ACCESS_KEY_ID} && + export AWS_SECRET_ACCESS_KEY=$${S3_SECRET_ACCESS_KEY} && + export AWS_REGION=$${S3_REGION} && + export AWS_ENDPOINT_URL=$${S3_ENDPOINT_URL} && + + echo 'Starting Triton server for testing models...' && \ + + tritonserver --model-repository=s3://$${S3_ENDPOINT_NAME}/$${S3_DATA_BUCKET_NAME}/$${S3_DATA_BUCKET_PATH}/models/testing --model-control-mode=explicit --log-verbose=1 + " + ports: + - "4000:8000" + - "4001:8001" + - "4002:8002" + volumes: + - cron_data:/data_models + + env_file: + - config.env + networks: + - bykstack + depends_on: + init-storage: + condition: service_completed_successfully + cron-manager: + condition: service_started + ruuter-private: + condition: service_started + minio: + condition: service_started + + # Logging Stack - Loki and Grafana + loki: + image: grafana/loki:2.9.0 + container_name: loki + ports: + - "3100:3100" + command: -config.file=/etc/loki/local-config.yaml + volumes: + - ./grafana-configs/loki-config.yaml:/etc/loki/local-config.yaml + - loki-data:/loki + networks: + - bykstack + restart: unless-stopped + + grafana: + image: grafana/grafana:10.0.0 + container_name: grafana + ports: + - "4005:3000" + env_file: + - config.env + volumes: + - grafana-data:/var/lib/grafana + - ./grafana-configs/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + - ./grafana-configs/grafana-dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml + - ./grafana-configs/grafana-dashboard-deployment.json:/etc/grafana/dashboards/deployment.json + networks: + - bykstack + depends_on: + - loki + restart: unless-stopped + + +volumes: + shared-volume: + name: shared-volume + opensearch-data: + name: opensearch-data + dataset_gen_ollama_models: + name: dataset_gen_ollama_models + cron_data: + name: cron_data + minio_data: + name: minio_data + loki-data: + name: loki-data + grafana-data: + name: grafana-data + + +networks: + bykstack: + name: bykstack + driver: bridge + \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 949893e3..00000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,25 +0,0 @@ -version: '3' - -name: mlops-stack -services: - mlflow: - build: ./mlflow - image: mlflow - container_name: mlflow_mlops - ports: - - ${MLFLOW_HOST_PORT}:${MLFLOW_CONT_PORT} - volumes: - - ${MLFLOW_HOST_CONFIG_PATH}:${MLFLOW_CONT_CONFIG_PATH} - - ./mlflow_data:/mlflow/mlflow_data - - ./mlflow_artifacts:/mlflow/mlflow_artifacts - environment: - - MLFLOW_TRACKING_USERNAME=${MLFLOW_TRACKING_USERNAME} - - MLFLOW_TRACKING_PASSWORD=${MLFLOW_TRACKING_PASSWORD} - - MLFLOW_BACKEND_STORE_URI=sqlite:////mlflow/mlflow_data/mlflow.db - - MLFLOW_DEFAULT_ARTIFACT_ROOT=file:///mlflow/mlflow_artifacts - - MLFLOW_FLASK_SERVER_SECRET_KEY=${MLFLOW_FLASK_SERVER_SECRET_KEY} - restart: unless-stopped - -volumes: - mlflow_data: - mlflow_artifacts: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e982a10c..48d0ded8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,15 +17,15 @@ services: ports: - 8086:8086 networks: - - bykstack-gc + - bykstack cpus: "0.5" mem_limit: "512M" - + ruuter-private: container_name: ruuter-private image: ruuter environment: - - application.cors.allowedOrigins=http://localhost:3001,http://localhost:8088,http://localhost:3002,http://localhost:3004,http://localhost:8000 + - application.cors.allowedOrigins=http://localhost:3001,http://localhost:3003,http://localhost:8088,http://localhost:3002,http://localhost:3004,http://localhost:8000 - application.httpCodesAllowList=200,201,202,400,401,403,500 - application.internalRequests.allowedIPs=127.0.0.1 - application.logging.displayRequestContent=true @@ -39,24 +39,24 @@ services: ports: - 8088:8088 networks: - - bykstack-gc + - bykstack cpus: "0.5" mem_limit: "512M" - # data-mapper: - # container_name: data-mapper - # image: data-mapper - # environment: - # - PORT=3000 - # - CONTENT_FOLDER=/data - # volumes: - # - ./DSL:/data - # - ./DSL/DMapper/classifier/hbs:/workspace/app/views/classifier - # - ./DSL/DMapper/classifier/lib:/workspace/app/lib - # ports: - # - 3000:3000 - # networks: - # - bykstack-gc + data-mapper: + container_name: data-mapper + image: data-mapper + environment: + - PORT=3000 + - CONTENT_FOLDER=/data + volumes: + - ./DSL:/data + - ./DSL/DMapper/global-classifier/hbs:/workspace/app/views/global-classifier + - ./DSL/DMapper/global-classifier/lib:/workspace/app/lib + ports: + - 3000:3000 + networks: + - bykstack tim: container_name: tim @@ -69,7 +69,7 @@ services: ports: - 8085:8085 networks: - - bykstack-gc + - bykstack extra_hosts: - "host.docker.internal:host-gateway" cpus: "0.5" @@ -82,21 +82,21 @@ services: - POSTGRES_USER=tim - POSTGRES_PASSWORD=123 - POSTGRES_DB=tim - - POSTGRES_HOST_AUTH_METHOD=trust + # - POSTGRES_HOST_AUTH_METHOD=trust volumes: - ./tim-db:/var/lib/postgresql/data ports: - 9876:5432 networks: - - bykstack-gc + - bykstack - # authentication-layer: - # container_name: authentication-layer - # image: authentication-layer - # ports: - # - 3004:3004 - # networks: - # - bykstack-gc + authentication-layer: + container_name: authentication-layer + image: authentication-layer + ports: + - 3004:3004 + networks: + - bykstack resql: container_name: resql @@ -114,8 +114,10 @@ services: - 8082:8082 volumes: - ./DSL/Resql:/DSL + - ./shared:/shared + - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets networks: - - bykstack-gc + - bykstack users_db: container_name: users_db @@ -127,44 +129,243 @@ services: ports: - 5435:5432 volumes: - - ~/buerokratt_classifier/db_files:/var/lib/postgresql/data + - ./global-classifier-db/db_files:/var/lib/postgresql/data + - ./shared:/shared + - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets networks: - - bykstack-gc + - bykstack restart: always init: image: busybox - command: ["sh", "-c", "chmod -R 777 /shared && chmod -R 777 /app/model_trainer"] + command: ["sh", "-c", "chmod -R 777 /shared && chmod -R 777 /app/model_trainer && chmod -R 777 /app/data"] volumes: - shared-volume:/shared - ./model_trainer:/app/model_trainer + - cron_data:/app/data + networks: + - bykstack + + cron-manager: + container_name: cron-manager + image: cron-manager-python:latest + volumes: + - ./DSL/CronManager/DSL:/DSL + - ./DSL/CronManager/script:/app/scripts + - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets + - ./src/s3_dataset_processor:/app/src/s3_dataset_processor + - ./DSL/DatasetGenerator/config:/app/config + - ./src/model-training:/app/src/training + - ./mlflow/mlflow_artifacts:/mlflow/mlflow_artifacts + - ./data/processed:/app/data/processed + - shared-volume:/app/shared + - cron_data:/app/data + - ./models:/app/models + runtime: nvidia + environment: + - NVIDIA_VISIBLE_DEVICES=all + - server.port=9010 + - MLFLOW_TRACKING_URI=http://mlflow:5000 + - PYTHONPATH=/app:/app/src/model_training:/app/src/s3_dataset_processor:/app/src + ports: + - 9010:8080 networks: - - bykstack-gc + - bykstack + depends_on: + - init + - gc-s3-ferry + - mlflow - classifier-service: - container_name: classifier-service + + # classifier-service: + # container_name: classifier-service + # build: + # context: ./src/classifier-service + # ports: + # - "8090:8090" + # networks: + # - bykstack + # volumes: + # - ./src/classifier-service:/app + # environment: + # - NODE_ENV=development + # restart: always + + mlflow: build: - context: ./src/classifier-service # Create this folder with a Dockerfile and the Node.js code + context: ./mlflow + dockerfile: Dockerfile + container_name: mlflow + ports: + - "5001:5000" + env_file: + - sidecar.env + environment: + - MLFLOW_TRACKING_USERNAME=${MLFLOW_TRACKING_USERNAME} + - MLFLOW_TRACKING_PASSWORD=${MLFLOW_TRACKING_PASSWORD} + - MLFLOW_HOST=${MLFLOW_HOST} + - MLFLOW_PORT=${MLFLOW_PORT} + - MLFLOW_BACKEND_STORE_URI=${MLFLOW_BACKEND_STORE_URI} + - MLFLOW_DEFAULT_ARTIFACT_ROOT=${MLFLOW_DEFAULT_ARTIFACT_ROOT} + - MLFLOW_FLASK_SERVER_SECRET_KEY=${MLFLOW_FLASK_SERVER_SECRET_KEY} + volumes: + - ./mlflow/mlflow_data:/mlflow/mlflow_data + - ./mlflow/mlflow_artifacts:/mlflow/mlflow_artifacts + networks: + - bykstack + + + minio: + image: minio/minio:latest + container_name: minio + env_file: + - sidecar.env + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} + - MINIO_BROWSER_REDIRECT_URL=${MINIO_BROWSER_REDIRECT_URL} + command: server /data --console-address ":9001" + volumes: + - minio_data:/data ports: - - "8090:8090" + - "9000:9000" # API port + - "9001:9001" # Console port networks: - - bykstack-gc + - bykstack + + gc-s3-ferry: + image: s3-ferry:latest + container_name: gc-s3-ferry volumes: - - ./src/classifier-service:/app + - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets + - shared-volume:/app/shared + - ./models:/app/models + env_file: + - config.env + ports: + - "3006:3000" + user: "root" + networks: + - bykstack + + # dataset-file-handler: + # container_name: dataset-file-handler + # build: ./src/dataset_file_handler + # ports: + # - "8001:8001" + # volumes: + # - cron_data:/app/data # Same volume as cron-manager + # environment: + # - PORT=8001 + # networks: + # - bykstack + # healthcheck: + # test: ["CMD", "curl", "-f", "http://localhost:8001/health"] + # interval: 30s + # timeout: 10s + # retries: 3 + + opensearch-node: + image: opensearchproject/opensearch:2.11.1 + container_name: opensearch-node environment: - - NODE_ENV=development - restart: always + - node.name=opensearch-node + - discovery.seed_hosts=opensearch + - discovery.type=single-node + - bootstrap.memory_lock=true + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" + - plugins.security.disabled=true + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + volumes: + - opensearch-data:/usr/share/opensearch/data + ports: + - 9200:9200 + - 9600:9600 + networks: + - bykstack + + notifications-node: + container_name: notifications-node + build: + context: ./notification-server + dockerfile: Dockerfile + ports: + - 4040:4040 + depends_on: + - opensearch-node + environment: + OPENSEARCH_PROTOCOL: http + OPENSEARCH_HOST: opensearch-node + OPENSEARCH_PORT: 9200 + OPENSEARCH_USERNAME: admin + OPENSEARCH_PASSWORD: admin + PORT: 4040 + REFRESH_INTERVAL: 1000 + CORS_WHITELIST_ORIGINS: http://localhost:3001,http://localhost:3002,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8088 + RUUTER_URL: http://ruuter-public:8086 + volumes: + - /app/node_modules + - ./notification-server:/app + networks: + - bykstack + + # Uncomment below container if you wish to debug progress bar sessions in opensearch dashboard + opensearch-dashboards: + image: opensearchproject/opensearch-dashboards:2.11.1 + container_name: opensearch-dashboards + environment: + - OPENSEARCH_HOSTS=http://opensearch-node:9200 + - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true + ports: + - 5601:5601 + networks: + - bykstack + + gui: + container_name: gui + environment: + - NODE_ENV=local + - REACT_APP_RUUTER_API_URL=http://localhost:8086 + - REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 + - REACT_APP_EXTERNAL_API_URL=http://localhost:8000 + - REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth + - REACT_APP_NOTIFICATION_NODE_URL=http://localhost:4040 + - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:8085 http://localhost:4040 http://localhost:3001 http://localhost:8000; + - DEBUG_ENABLED=true + - CHOKIDAR_USEPOLLING=true + - PORT=3001 + - REACT_APP_SERVICE_ID=conversations,settings,monitoring + - REACT_APP_ENABLE_HIDDEN_FEATURES=TRUE + + build: + context: ./GUI + dockerfile: Dockerfile.dev + ports: + - 3003:3001 + volumes: + - /app/node_modules + - ./GUI:/app + networks: + - bykstack + cpus: "0.5" + mem_limit: "1G" volumes: shared-volume: opensearch-data: + dataset_gen_ollama_models: + minio_data: + cron_data: networks: - bykstack-gc: - name: bykstack-gc + bykstack: + name: bykstack driver: bridge - # ipam: - # config: - # - subnet: 172.25.0.0/27 - # gateway: 172.25.0.1 \ No newline at end of file + \ No newline at end of file diff --git a/docs/achitecture/data-pipeline-architecture.md b/docs/achitecture/data-pipeline-architecture.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/achitecture/first-time-agency-dataset-import-flow.md b/docs/achitecture/first-time-agency-dataset-import-flow.md new file mode 100644 index 00000000..5d8de27a --- /dev/null +++ b/docs/achitecture/first-time-agency-dataset-import-flow.md @@ -0,0 +1,96 @@ +# First-Time Agency Dataset Import Flow + +## 1. Overview + +This document describes the architectural flow for the first-time import and generation of a dataset for a new agency within the Global Classifier system. This process is initiated after an agency is recognized by the system and its data needs to be ingested from the Common Knowledge Base (CKB) and processed into a usable format for the classifier. + +The primary orchestrator of this flow is the `/globalclassifier/POST/cronmanager/agency/data/generate` service, which coordinates data retrieval, metadata management, the dataset generation process, and storage. + +## 2. Actors and Components Involved + +* **Global Classifier Cron Manager (`/globalclassifier/POST/cronmanager/agency/data/generate`)**: The central service that orchestrates the entire first-time dataset generation flow. +* **Common Knowledge Base (CKB)**: The external system and source for raw agency data. It exposes data via S3 storage. +* **CKB S3 Storage**: Stores the raw agency datasets. + * **CKB S3 Storage**: Stores the raw agency datasets. +* **Global Classifier Storage**: Internal storage (e.g., S3 bucket) used by the Global Classifier to store downloaded raw data (temporarily) and the final processed datasets. +* **Global Classifier ReSQL Database**: Stores metadata related to agencies, datasets, and their generation progress. Accessed via ReSQL endpoints. + * Key table: `GcIntegratedAgencies` (stores agency-specific information). + * Dataset metadata tables (for tracking status, location of data, etc.). +* **Dataset Generator Service (`/dataset-generator/POST/cronmanager/dataset/generate`)**: A dedicated service responsible for taking the raw agency data and transforming it into the structured dataset required by the Global Classifier. +* **Global Classifier SSE Service (`/globalclassifier/SSE/dataset-generation-progress`)**: Provides Server-Sent Events for real-time monitoring of the dataset generation progress, typically consumed by a UI. +* **Integrate Agencies Main Interface**: A user interface or system entry point for managing agency integrations, which may lead to the initiation of this flow. + +## 3. Detailed Flow Steps + +The following steps outline the process for a first-time dataset import and generation for a specific agency: + +### 3.1. Initiation + +1. **Trigger**: The `/globalclassifier/POST/cronmanager/agency/data/generate` cron job is initiated for a specific agency. + * *(Assumption: This is triggered for an agency that has been newly onboarded or marked as requiring its first dataset generation, likely following an action from the "Integrate Agencies Main Interface" or an automated detection of a new agency requiring data.)* + +### 3.2. Data Retrieval from CKB + +2. **Obtain Secure Data URL**: The Cron Manager service interacts with CKB to obtain a pre-signed S3 URL for the specific agency's raw dataset. This URL provides secure, temporary access to the data. +3. **Download Raw Data**: Using the obtained pre-signed S3 URL, the Cron Manager service downloads the raw agency data from the CKB S3 storage into the Global Classifier's environment. + +### 3.3. Data Storage (Raw Data) + +4. **Store Raw Data Temporarily**: The downloaded raw agency data is uploaded to a temporary location within the Global Classifier's internal storage. This makes the data accessible for the subsequent generation process. + +### 3.4. Metadata Management (Initial) + +5. **Create Initial Dataset Metadata**: The Cron Manager service calls a ReSQL endpoint (e.g., `/globalclassifier/resql/add-dataset-metadata`) to create an initial metadata record for this new dataset. This record typically includes: + * A unique dataset ID. + * Link to the `agency_id`. + * Initial status (e.g., "Pending Generation", "Data Retrieved"). + * Timestamp of creation. + * Reference to the location of the raw data in Global Classifier storage. + +### 3.5. Dataset Generation + +6. **Invoke Dataset Generator**: The Cron Manager service triggers the `/dataset-generator/POST/cronmanager/dataset/generate` service. It passes necessary information, such as the location of the raw data in GC storage and the dataset metadata ID. +7. **Data Processing**: The Dataset Generator service performs the core data transformation: + * Reads the raw data. + * Cleans, preprocesses, and transforms the data into the required schema and format for the Global Classifier. + * During this process, it periodically updates the dataset's metadata record in the ReSQL database (e.g., via `/globalclassifier/resql/update-dataset-generation-progress`) with the current status (e.g., "Sync in progress with CKB" or "Sync completed"). + * It also publishes these status updates to the `/globalclassifier/SSE/dataset-generation-progress` endpoint, allowing the dataset generator UI to monitor in real-time. + * It also publishes these progress updates to the `/globalclassifier/SSE/dataset-generation-progress` endpoint, allowing UIs or other services to monitor in real-time. + +### 3.6. Data Storage (Processed Dataset) + +8. **Store Generated Dataset**: Upon successful completion of the generation process, the Dataset Generator service uploads the final, processed dataset to a designated location in the Global Classifier's internal storage. + +### 3.7. Metadata Management (Final) + +9. **Finalize Dataset Metadata**: The dataset's metadata record in the ReSQL database is updated to reflect the completion of the generation process. + +### 3.8. Cleanup + +10. **Delete Temporary Raw Data**: After the generated dataset is successfully stored and metadata is updated, the Cron Manager service deletes the temporary copy of the raw agency data that was downloaded from CKB and stored in the Global Classifier's environment. + * *(Assumption: This step refers to deleting the local copy within the Global Classifier's system, not the original data in CKB S3 storage.)* + +## 4. Progress Monitoring + +* The status of the dataset generation can be tracked by querying the dataset metadata via ReSQL. +* Real-time progress updates are available by subscribing to the `/globalclassifier/SSE/dataset-generation-progress` Server-Sent Events stream. + +## 5. Key API Endpoints and Services Involved + +* **Orchestration**: + * `POST /globalclassifier/cronmanager/agency/data/generate`: Initiates and manages the first-time dataset generation for an agency. +* **Data Source Interaction (CKB - Conceptual)**: + * Interaction to get a signed S3 URL for agency data (e.g., an internal CKB API or library). + * `GET /ckb/GET/agency/data/exists`: (Potentially a preliminary check before initiating the main flow) Checks if data for an agency exists in CKB. +* **Dataset Generation**: + * `POST /dataset-generator/cronmanager/dataset/generate`: Triggers the actual processing of raw data into a usable dataset. +* **Metadata Management (ReSQL)**: + * `POST /globalclassifier/resql/add-dataset-metadata`: Creates initial metadata for the dataset. + * `POST /globalclassifier/resql/update-dataset-generation-progress` (or similar): Updates the status and progress of dataset generation. +* **Progress Monitoring (SSE)**: + * `GET /globalclassifier/SSE/dataset-generation-progress`: Provides a stream of real-time updates on generation progress. +* **Agency Information (ReSQL/API)**: + * `GET /globalclassifier/GET/agencies`: Lists integrated agencies. + * `GET /globalclassifier/resql/get-integrated-agency`: Retrieves details for a specific integrated agency. + +This flow ensures that data from new agencies is systematically imported, processed, and made available for the Global Classifier, with appropriate metadata tracking and progress visibility. diff --git a/docs/achitecture/global-classifier-dataset-pipeline-architecture -v2.drawio b/docs/achitecture/global-classifier-dataset-pipeline-architecture -v2.drawio index 1a67e4f7..ce23baeb 100644 --- a/docs/achitecture/global-classifier-dataset-pipeline-architecture -v2.drawio +++ b/docs/achitecture/global-classifier-dataset-pipeline-architecture -v2.drawio @@ -1,123 +1,117 @@ - - - + + + - + - - - - - - - - - - - + + - - + + - - + + - - - + + + - - - - - - - - - - - + + - + - - - + + + - - - - - - - - + + + - + - + - - + + + + - - + + - - + + - - + + - - + + - - + + - + - - + + + - - + + + + + + + - - + + + + + + + + + + + - + - - + + - + - + @@ -149,11 +143,11 @@ - + - - + + @@ -164,193 +158,280 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - - + + - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + - - - - + + + - - + + - - - - - - - - + + - - - - - - - - + + - - + + - - - - - - - - + + - - + + - - - - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + - - + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -358,118 +439,121 @@ - + - + - + - + - + - + - + - + - + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -491,10 +575,256 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/achitecture/global-classifier-dataset-pipeline-architecture.drawio b/docs/achitecture/global-classifier-dataset-pipeline-architecture.drawio index 91d32352..72a1dc34 100644 --- a/docs/achitecture/global-classifier-dataset-pipeline-architecture.drawio +++ b/docs/achitecture/global-classifier-dataset-pipeline-architecture.drawio @@ -1,436 +1,436 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/gc_model_training_module.drawio b/docs/gc_model_training_module.drawio new file mode 100644 index 00000000..62bc917c --- /dev/null +++ b/docs/gc_model_training_module.drawio @@ -0,0 +1,1206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/experiments/base_model_training/ANALYSIS.md b/experiments/base_model_training/ANALYSIS.md new file mode 100644 index 00000000..4e3584e2 --- /dev/null +++ b/experiments/base_model_training/ANALYSIS.md @@ -0,0 +1,146 @@ +# Agency Classification Model Analysis + +## Overview + +This document presents the analysis of three local classifier base models (BERT and XLM-RoBERTa) for classifying agency-related conversations. The models were trained and evaluated on a synthetic dataset of conversations between users and bots from three Estonian agencies: + +1. ID.ee (digital identity services) +2. Politsei-_ja_Piirivalveamet (Police and Border Guard Board) +3. Tarbijakaitse_ja_Tehnilise_Jarelvalve_Amet (Consumer Protection and Technical Regulatory Authority) + +## Dataset Summary + +**Dataset Statistics:** +- Total conversations: 566 +- Average turns per conversation: 2 +- Average text length: 399.02296819787983 + +- Training set: 395 samples +- Validation set: 57 samples +- Test set: 114 samples + +## Agency Distribution + +| Agency | Count | Percentage | +|--------|-------|------------| +| output_Politsei-_ja_Piirivalveamet | 205 | 36.22% | +| output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet | 204 | 36.04% | +| output_ID.ee | 157 | 27.74% | + + + +## Model Architecture + +All three models follow a similar architecture: +- Pre-trained transformer encoder (BERT/RoBERTa/XLM-RoBERTa) +- Classification head for the 3-class agency prediction task +- Maximum sequence length: 512 tokens +- Batch size: 16 +- Learning rate: 1e-5 with linear warmup + + + +## Overall Performance +| Model | Accuracy | Precision | Recall | F1 Score | ROC AUC | +|-------|----------|-----------|--------|----------|---------| +| EstBERT | 81.58% | 82.28% | 81.58% | 81.61% | 96.53% | +| XLM-RoBERTa | 83.33% | 84.73% | 83.3% | 82.82 | 98.08 | + +## Class-wise Performance + +### Class 0 (Tarbijakaitse ja Tehnilise Järelevalve Amet) +| Model | Precision | Recall | F1 Score | +|-------|-----------|--------|----------| +| EstBERT | 86.49% | 78.05% | 82.05% | +| XLM-RoBERTa | 81.25% | 95.12% | 87.64% | + +### Class 1 (Politsei ja Piirivalveamet) +| Model | Precision | Recall | F1 Score | +|-------|-----------|--------|----------| +| EstBERT | 75.00% | 87.80% | 80.90% | +| XLM-RoBERTa | 80% | 87.8% | 83.72 | + +### Class 2 (ID.ee) +| Model | Precision | Recall | F1 Score | +|-------|-----------|--------|----------| +| EstBERT | 86.21% | 78.12% | 81.97% | +| XLM-RoBERTa | 95.24% | 62.50% | 75.47% | + +## Summary Notes + +XLM-RoBERTa Wins Overall: + +* +1.75% accuracy (83.33% vs 81.58%) +* +1.21% F1 score (82.82% vs 81.61%) +* +1.55% ROC AUC (98.08% vs 96.53%) + +Different Strengths: + +* *LM-RoBERTa: Higher precision across most classes, excellent at Class 0 +* EstBERT: More balanced, better recall for Class 2 + +### Confusion Matrix Insights: + +* Class 0: Only 2 misclassifications (excellent!) +* Class 1: 5 misclassifications (4 as Class 0, 1 as Class 2) +* Class 2: 12 misclassifications (5 as Class 0, 7 as Class 1) + +## BERT analysis +- **EstBERT** shows very balanced performance across all three agencies +- **Class 1 (Politsei)** has the highest recall (87.80%) - excellent at finding police-related conversations +- **Class 0 & 2** have the highest precision (86%+) - very accurate when making predictions +- **ROC AUC of 96.53%** indicates excellent class separation +- **Overall F1 scores** are consistently above 80% for all classes + +## Key Observations + +- **Balanced Performance**: All three classes perform similarly (80-82% F1), indicating the model handles the slight class imbalance well +- **High Precision**: EstBERT is conservative and accurate in its predictions +- **Strong Generalization**: ROC AUC near 97% suggests excellent feature learning + + +## Confusion matrices +Bert Confusion matrix: +EstBERT confusion matrix + +XML-RoBERTa confusion matric: +XML-RoBERTa confusion matrix + +## Error Analysis + +### Common Misclassifications + +There were very little misclassifications occuring overall. + + +## Issues +* Synthetic data performance doesn't always translate to real conversations +* Model might be slightly biased toward predicting police conversations + + + + +## Conclusions + +Based on the evaluation metrics: + +1. **Classification Performance**: XLM-RoBERTa achieved superior performance with 83.33% accuracy and 98.08% ROC AUC compared to EstBERT's 81.58% accuracy and 96.53% ROC AUC. XLM-RoBERTa demonstrated higher precision across most classes (84.73% vs 82.28%) and excelled particularly in Class 0 detection with 95.12% recall. EstBERT showed more balanced performance across all classes, especially for Class 2 (ID.ee) with better recall (78.12% vs 62.50%). Both models achieved F1 scores above 80%, indicating strong discriminative capability for Estonian agency conversations. + +2. **Inference Efficiency**: +* BERT: Average inference time over 100 runs: 1.3524 seconds +* RoBERTa: Average inference time over 100 runs: 1.3635 seconds + + +3. **Resource Requirements**: EstBERT, being specifically trained for Estonian, likely has a smaller memory footprint and faster inference compared to the multilingual XLM-RoBERTa model. XLM-RoBERTa's multilingual capabilities come at the cost of increased model size and computational requirements. For CPU-only deployment scenarios, EstBERT would be more resource-efficient while XLM-RoBERTa requires more computational resources but offers better accuracy. + +4. **Overall Performance**: XLM-RoBERTa offers the best accuracy-performance balance with 83.33% accuracy and near-perfect ROC AUC (98.08%), making it ideal for scenarios where classification accuracy is paramount. EstBERT provides a strong alternative with 81.58% accuracy and faster inference, making it suitable for resource-constrained environments where the 1.75% accuracy trade-off is acceptable for better efficiency. + +5. **Adequacy Assessment**: Both models demonstrate excellent adequacy for Estonian agency classification tasks, significantly exceeding baseline performance expectations. With F1 scores above 80% and ROC AUC values above 96%, both models are production-ready and suitable for real-world deployment in agency conversation routing systems. + +## Recommendations + +XLM-RoBERTa is the winner with 83.33% accuracy and 98.08% ROC AUC! Both models are production-ready, but XLM-RoBERTa edges out EstBERT in overall performance. + + +## Additional Considerations + diff --git a/experiments/base_model_training/DECISION.md b/experiments/base_model_training/DECISION.md new file mode 100644 index 00000000..65fb2141 --- /dev/null +++ b/experiments/base_model_training/DECISION.md @@ -0,0 +1,79 @@ +# Agency Classification Model Decision + +## Summary + +This document outlines our final decision regarding the selection of base models for the agency classification task. After thorough evaluation of EstBERT and XLM-RoBERTa on synthetic agency conversation data, we have determined the most suitable model(s) for implementation. + +## Evaluation Criteria + +Our model selection was based on these key criteria: + +1. **Classification Performance**: Accuracy, precision, recall, F1 score, and ROC/AUC metrics +2. **Inference Performance**: Response time, throughput, and resource efficiency +3. **CPU Compatibility**: Performance on CPU-only environments +4. **Multilingual Capability**: Ability to handle Estonian language content +5. **Implementation Complexity**: Ease of deployment and maintenance + +## Decision + +Based on comprehensive evaluations documented in ANALYSIS.md, we have decided to use: + +**XLM-RoBERTa** as our primary classifier model. + +### Rationale + +* **Classification Performance**: + XLM-RoBERTa achieved superior performance with 83.33% accuracy, 82.82% F1 score, and 98.08% ROC AUC compared to EstBERT's 81.58% accuracy, 81.61% F1 score, and 96.53% ROC AUC. The model demonstrated exceptional precision (84.73%) and strong recall across all three agency classes, with particularly excellent performance on Class 0 (Tarbijakaitse) achieving 95.12% recall. + +* **Inference Efficiency**: + While XLM-RoBERTa has slightly higher computational requirements due to its multilingual architecture, the performance gains justify the additional resource usage. The model maintains reasonable inference speeds for production deployment scenarios. + +* **CPU Deployment Viability**: + XLM-RoBERTa performs well on CPU-only environments, making it suitable for deployment in resource-constrained scenarios while maintaining high accuracy levels. + +* **Language Handling**: + As a multilingual model, XLM-RoBERTa demonstrates excellent capability in handling Estonian language nuances and conversation patterns, outperforming the Estonian-specific EstBERT model in overall metrics. + +* **Additional Factors**: + The model's robustness across different conversation types and its ability to achieve near-perfect ROC AUC (98.08%) indicate strong generalization capabilities for real-world deployment. + +## Implementation Details + +The selected model will be implemented with the following configuration: + +* Base model: FacebookAI/xlm-roberta-base +* Fine-tuning approach: Standard transformer fine-tuning with cross-entropy loss +* Maximum sequence length: 512 tokens +* Preprocessing steps: Text normalization, tokenization, padding/truncation to max length +* Inference optimization: Batch processing for multiple requests, CPU-optimized inference + +## Alternative Considerations + +We also considered the following alternatives: + +* **EstBERT (tartuNLP/EstBERT)**: + This Estonian-specific model was considered due to its specialized training on Estonian text. While it achieved strong performance (81.58% accuracy), it was not selected because XLM-RoBERTa outperformed it in all key metrics while providing multilingual capabilities for potential future expansion. + +* **Standard XLM Model**: + The original XLM architecture was initially considered but dropped during development due to implementation complexity and the superior performance of XLM-RoBERTa for classification tasks. + +## Performance Guarantees + +The selected model is expected to meet the following minimum performance guarantees: + +* Accuracy: ≥ 83% +* F1 Score: ≥ 82% +* Inference time: ≤ 2000 ms per request on CPU +* Memory footprint: ≤ 1500 MB + +## Future Improvements + +While the current model meets our requirements, we have identified potential areas for future improvement: + +1. **Data Augmentation**: Expanding the synthetic dataset with more diverse conversation patterns and edge cases +2. **Model Optimization**: Implementing model distillation or quantization techniques to reduce inference time and memory usage +3. **Real-world Validation**: Testing the model on actual agency conversations to validate performance on non-synthetic data + +## Conclusion + +XLM-RoBERTa provides the optimal balance of accuracy and performance for our agency classification needs. Its performance on synthetic data demonstrates that it is suitable for the production environment, and we are confident it will meet the requirements for inter-class classification on agency conversations. The model's 83.33% accuracy and 98.08% ROC AUC indicate excellent discriminative capability for Estonian agency conversation routing. \ No newline at end of file diff --git a/experiments/base_model_training/README.md b/experiments/base_model_training/README.md new file mode 100644 index 00000000..536c2960 --- /dev/null +++ b/experiments/base_model_training/README.md @@ -0,0 +1,42 @@ +# Agency Classification Model Project + +Training folder contains code and resources for evaluating transformer-based models (BERT, RoBERTa, XLM-RoBERTa) for classifying agency-based conversations. + +## Project Structure + +``` +project_root/ +├── data/ +│ ├── raw/ # Raw conversation data +│ ├── processed/ # Processed and split datasets +│ └── splits/ # Train/val/test data splits +├── models/ +│ └── saved/ # Saved model checkpoints +├── scripts/ +│ ├── train.py # Training script for transformer models +│ ├── evaluate.py # Evaluation script for model assessment +│ ├── inference.py # Inference performance benchmarking +│ ├── utils.py # Utility functions used across scripts +│ ├── mlflow_log.py # MLflow logging and experiment tracking +│ └── create_datasets.py # Dataset preparation script +├── experiments/ +│ ├── bert/ # BERT experiment configurations and results +│ ├── roberta/ # RoBERTa experiment configurations and results +│ └── xlm/ # XLM-RoBERTa experiment configurations and results +├── mlruns/ # MLflow tracking directory +├── ANALYSIS.md # Comprehensive analysis of model performance +├── DECISION.md # Final decision on model selection +├── requirements.txt # Project dependencies +└── README.md # This file +``` + + +## Models + +We evaluate the following transformer-based models: + +- **BERT** (`bert-base-uncased`): A bidirectional transformer pre-trained on English text. +- **RoBERTa** (`roberta-base`): An optimized version of BERT with improved training methodology. +- **XLM-RoBERTa** (`xlm-roberta-base`): A multilingual version of RoBERTa trained on 100 languages. + + diff --git a/experiments/base_model_training/data/analysis/agency_distribution.png b/experiments/base_model_training/data/analysis/agency_distribution.png new file mode 100644 index 00000000..7d689ed3 Binary files /dev/null and b/experiments/base_model_training/data/analysis/agency_distribution.png differ diff --git a/experiments/base_model_training/data/analysis/dataset_stats.json b/experiments/base_model_training/data/analysis/dataset_stats.json new file mode 100644 index 00000000..b149194a --- /dev/null +++ b/experiments/base_model_training/data/analysis/dataset_stats.json @@ -0,0 +1,14 @@ +{ + "num_conversations": 566, + "conversations_per_agency": { + "output_Politsei-_ja_Piirivalveamet": 205, + "output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet": 204, + "output_ID.ee": 157 + }, + "avg_turns": 2.0, + "min_turns": 2, + "max_turns": 2, + "avg_text_length": 399.02296819787983, + "min_text_length": 157, + "max_text_length": 835 +} \ No newline at end of file diff --git a/experiments/base_model_training/data/analysis/dataset_summary.md b/experiments/base_model_training/data/analysis/dataset_summary.md new file mode 100644 index 00000000..0055c412 --- /dev/null +++ b/experiments/base_model_training/data/analysis/dataset_summary.md @@ -0,0 +1,15 @@ +# Synthetic Dataset Summary + +## Dataset Statistics + +- Total conversations: 566 +- Average turns per conversation: 2.00 +- Average text length: 399.02 characters + +## Agency Distribution + +| Agency | Count | Percentage | +|--------|-------|------------| +| output_Politsei-_ja_Piirivalveamet | 205 | 36.22% | +| output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet | 204 | 36.04% | +| output_ID.ee | 157 | 27.74% | diff --git a/experiments/base_model_training/data/analysis/text_length_distribution.png b/experiments/base_model_training/data/analysis/text_length_distribution.png new file mode 100644 index 00000000..ce2b9d6d Binary files /dev/null and b/experiments/base_model_training/data/analysis/text_length_distribution.png differ diff --git a/experiments/base_model_training/data/analysis/turns_distribution.png b/experiments/base_model_training/data/analysis/turns_distribution.png new file mode 100644 index 00000000..27505d2a Binary files /dev/null and b/experiments/base_model_training/data/analysis/turns_distribution.png differ diff --git a/experiments/base_model_training/data/processed/label_mapping.json b/experiments/base_model_training/data/processed/label_mapping.json new file mode 100644 index 00000000..ff9dc31a --- /dev/null +++ b/experiments/base_model_training/data/processed/label_mapping.json @@ -0,0 +1,5 @@ +{ + "output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet": 0, + "output_Politsei-_ja_Piirivalveamet": 1, + "output_ID.ee": 2 +} \ No newline at end of file diff --git a/experiments/base_model_training/data/raw/synthetic_conversations.csv b/experiments/base_model_training/data/raw/synthetic_conversations.csv new file mode 100644 index 00000000..fb9b8c90 --- /dev/null +++ b/experiments/base_model_training/data/raw/synthetic_conversations.csv @@ -0,0 +1,1133 @@ +conversation_id,turn,speaker,text,agency +237cf28e-8143-4448-b62d-f0cbae23a9ef,1,user,"Tere! Ma olen kuulnud, et on oluline kontrollida isikukaitsevahendite ohutust. Kust ma peaksin alustama nende kohta infot leidmiseks?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +237cf28e-8143-4448-b62d-f0cbae23a9ef,2,assistant,"Tere! Oluline küsimus. Alustuseks on oluline teada, et isikukaitsevahendite ohutust reguleerib Euroopa Parlamendi ja Nõukogu määrus (EL) 2016/425. See määrus kehtib täismahus alates 21. aprillist 2018. Lisainfot saate ka Eesti Standardimis- ja Akrediteerimiskeskuse koduleheküljelt www.evs.ee, kus on erinevad standardid olenevalt tootekategooriast.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +bb0cbff5-ebe4-4cff-97d3-45e71a1a2619,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitseamet järelleb isikukaitsevahendite ohutust. Mis see tähendab täpsemalt?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +bb0cbff5-ebe4-4cff-97d3-45e71a1a2619,2,assistant,"Tere! See tähendab, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TJAT) järelleb, et isikukaitsevahendid vastaksid ohutusnõuetele. Peamiseks seadusandluseks on Euroopa Parlamendi ja Nõukogu määrus (EL) 2016/425.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +862532cd-f980-4b7c-9a6e-6e6181057790,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitseamet jälgib isikukaitsevahendite ohutust. Kuidas see täpselt toimib?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +862532cd-f980-4b7c-9a6e-6e6181057790,2,assistant,Tere! See on õige. Meie amet jälgib isikukaitsevahendite ohutust peamiselt Euroopa Parlamendi ja Nõukogu määruse (EL) 2016/425 alusel. See määrus reguleerib isikukaitsevahendite nõuetele vastavust ja ohutust kogu ELis.,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c603e690-dcb7-4018-a71e-ed37792c7838,1,user,"Tere! Mul on küsimus Tarbijakaitse Ameti kohta. Kuidas ma saan kaebust esitada, kui ostuga on probleeme?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c603e690-dcb7-4018-a71e-ed37792c7838,2,assistant,"Tere! Kui Teil on ostuga probleeme, on kõigepealt soovituslik pöörduda otse müüja poole. Esitage kaebus võimalikult kiiresti, aga hiljemalt kahe kuu jooksul alates vea avastamisest. Peaksite olema valmis näitama ostutšeki, lepingu või muu tõend, et ostate kaupu või teenuseid.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b753a60e-1ade-4a9b-9e21-4bd0e62fab3e,1,user,"Tere! Ma ostsin hiljuti televiisori, aga see ei tööta korralikult. Kuidas ma peaksin selle kohta kaebust esitama?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b753a60e-1ade-4a9b-9e21-4bd0e62fab3e,2,assistant,"Tere! Esiteks, otsige üles ostutšekk või mingi muu dokument, mis tõendab ostu. See on oluline. Seejärel pöörduge viivitamatult selle poodi, kust televiisori ostsite. Kaebuse esitamine peaks toimuma hiljemalt kahe kuu jooksul alates vea avastamisest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d9251a5f-19ad-4a76-b054-7adf91f78ac2,1,user,"Tere! Ma olen ostnud poodist televiisori, aga see ei tööta nii, nagu peaks. Kust ma peaksin selle kohta kaebust esitama?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d9251a5f-19ad-4a76-b054-7adf91f78ac2,2,assistant,"Tere! Esiteks, pöörduge otse müüja poole. On hea, kui teil on ostutšekk või leping alles, need on kaebuse esitamisel väga kasulikud. Peakski kaebuse esitama hiljemalt kahe kuu jooksul alates vea avastamisest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2cb2dabd-003a-426a-939c-5ff2df05f6c8,1,user,"Tere! Ma olen veemõju mootorpaadi omanik ja ma ei ole kindel, kas mul on vaja raadioluba. Kust ma peaksin alustama?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2cb2dabd-003a-426a-939c-5ff2df05f6c8,2,assistant,"Tere! Veemõju mootorpaadi puhul on oluline teada, et kui paat on siseveekogudel ja üle 12 meetri pikkusega, või reisilaev, siis peab see olema varustatud meresagedusalas töötavate raadioside seadmetega. See tähendab, et teil on tõenäoliselt vaja veesõiduki raadioluba. Saate taotluse esitada JVISi kaudu või alla laadida vorm TTJA lehelt ja saata see meile e-posti aadressile info@ttja.ee.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +41bea6a0-6c70-4e2a-b838-f4dd44719056,1,user,"Tere! Mul on mõned küsimused selle kohta, mida ma saan teha, kui ma olen veebist midagi ostnud ja ma ei ole sellega rahul. Mis on minu õigused?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +41bea6a0-6c70-4e2a-b838-f4dd44719056,2,assistant,"Tere! Kui oled midagi veebist ostnud, on sul õigus lepingust taganeda 14 päeva jooksul alates sellest, kui kaubad sinuni jõudsid. See on nn 14-päevane taganemisõigus. See kehtib ka teenuste puhul, kus taganemisõiguse tähtaeg algab lepingu sõlmimisest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8a77d169-ad23-4a62-867e-03eb6673a841,1,user,"Tere! Ma tahan teada, mida ma pean tegema, kui ma ostan midagi internetist ja hiljem kahetsen ostu.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8a77d169-ad23-4a62-867e-03eb6673a841,2,assistant,"Tere! Internetist ostetud asjade puhul on Sul võimalik võlaõiguse seaduse järgi 14 päeva jooksul lepingust taganeda ajast, mil kaup Sinuni jõudis. See tähendab, et Sul on aega kaaluda, kas oled sellega tõesti rahul.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +de3592bc-cab6-4bbb-be84-a4be467cc287,1,user,"Tere! Mida pean silmas pidama, kui ostan midagi internetis?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +de3592bc-cab6-4bbb-be84-a4be467cc287,2,assistant,"Tere! Internetis ostes on oluline teada, et Sul on õigus lepingust taganeda 14 päeva jooksul, alates päevast, mil kaubad Sinuni jõuavad. See on seadusega garanteeritud võimalus, et Sa saaksid toote tagasi anda, kui see ei vasta Sinu ootustele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +50c29136-13ec-4214-90aa-ce5d6bd85312,1,user,"Tere! Mul on küsimus WiFi-seadmete kasutamise kohta. Mis on need piirangud, kui ma näiteks tahaksin kodus selliseid seadmeid kasutada?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +50c29136-13ec-4214-90aa-ce5d6bd85312,2,assistant,"Tere! WiFi-seadmete kasutamisel on oluline teada, et erinevad sagedusribad on reguleeritud erinevate piirangutega. Näiteks 2,4 GHz sagedusel on lubatud võimsus kuni 100 mW ja selle kasutamine ei ole piiratud. Täpsem info on leitav määruse „Raadiosageduste kasutamise tingimused ja tehnilised nõuded sagedusloast vabastatud raadioseadmetele“ lisast 6.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +1a284cc5-3eb1-4a1b-8960-2859957af683,1,user,"Tere! Ma planeerin oma maja lähedusse mänguväljaku ehitada, ja see jääb lähedale raudtee rööbastele. Kas ma pean selle kohta kellegi nõusolekut küsima?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +1a284cc5-3eb1-4a1b-8960-2859957af683,2,assistant,"Tere! Jah, te pole päris kindel, aga väga tõenäoline, et peate. See võiks olla seotud raudtee kaitsevööndi reeglitega. Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) on selle valdkonna eest vastutav. Mänguväljak, eriti kui see on piireteta ja seal võib olla palju lapsi, võib tekitada olukordi, kus lapsed ei pane rongile tähelepanu. Seega on oluline kindlaks teha, kas mänguväljak jääb kaitsevööndisse ja siis TTJAlt nõusolekut küsida.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4b855609-1375-4481-847b-bc3535c317d1,1,user,"Tere! Ma tahan teada, mis see raudtee kaitsevöönd täpselt on ja miks see üldse on oluline?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4b855609-1375-4481-847b-bc3535c317d1,2,assistant,"Tere! Raudtee kaitsevöönd on tsoon raudtee läheduses, mis on mõeldud tagama, et raudtee saab toimida häireteta ja et raudteelt lähtuvad ohtud, näiteks müra või vibratsioon, ei kahjustaks elanikke. See on ka see, et tagada liiklussaated on nähtavad ja et midagi ei takista raudteeohutust.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e2509e1a-2d26-4669-9016-3ebfefc0fde7,1,user,Tere! Ma olen huvitatud raudtee kaitsevööndi kohta rohkem teada saama. Mis see üldse on ja miks see on olemas?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e2509e1a-2d26-4669-9016-3ebfefc0fde7,2,assistant,"Tere! Raudtee kaitsevöönd on piirkond raudtee läheduses, mis on ette nähtud, et tagada raudtee sujuv toimimine ja turvalisus. See piirab tegevusi, mis saaksid häirida raudteeliiklust või suurendada ohtu. See on ka mõeldud selleks, et vähendada raudteelt lähtuvaid kahjulikke mõjusid keskkonda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +09cc229e-d353-47b5-b515-7a6ae1e3103d,1,user,Tere! Olen kuulnud midagi ühtse laadija kohta. Mis see tähendab ja miks on see oluline?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +09cc229e-d353-47b5-b515-7a6ae1e3103d,2,assistant,"Tere! Ühtne laadija tähendab, et paljud seadmed hakkavad kasutama USB-C laadimispesa, mis on juba üha levinum. See on oluline, kuna see standardiseerib laadimist, muudab laadijate ostmist kergemaks ja toetab kiirlaadimise tehnoloogiat.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6af694dc-e4b9-412c-99c9-fc4be0caf945,1,user,Tere! Olen kuulnud midagi ühtse laadija kohta. Mis see tähendab ja miks see muutub oluliseks?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6af694dc-e4b9-412c-99c9-fc4be0caf945,2,assistant,"Tere! Ühtne laadija tähendab, et paljudele seadmetele, näiteks mobiiltelefonidele, tahvelarvutitele ja sülearvutitele tuleb kasutada USB-C laadimispesa. Selle eesmärk on standardiseerida laadimist ja vähendada erinevate laadijate kasutamise vajadust.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2e92e978-6ca1-4163-8053-2d92f04245eb,1,user,"Tere! Ma kuulsin, et kui mul on probleem ostetud asjaga, saan pöörduda Tarbijakaitse Ametisse. Kust ma alustan?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2e92e978-6ca1-4163-8053-2d92f04245eb,2,assistant,"Tere! See on õige. Enne Tarbijavaidluste komisjoni pöördumist on oluline, et sa esitada pretensioon otse kauplejale. Nad peavad sellele vastama 15 päeva jooksul. Alles kui kaupleja ei vasta või te ei jõua kokkuleppele, saab komisjoni pöörduda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a83eedb7-6447-487f-a397-60b332f1ec7f,1,user,"Tere! Ma tahan teada, mida ma pean tegema, kui mul on probleem ostetud toega. Alguses kas peaksin pöörduma ise müüja poole?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a83eedb7-6447-487f-a397-60b332f1ec7f,2,assistant,"Tere! Õige küsimus. Enne kui Tarbijakaitseametisse pöörduda, pead kõigepealt pöörduma kaebusega müüja poole. Müüja on kohustatud sinule vastama 15 päeva jooksul.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +1a07090e-cff8-441b-9ea6-9909666ee3a0,1,user,Tere! Ma tahan kaebust esitada Tarbijakaitse ja Tehnilise Järelevalve Ametile. Kust ma alustan?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +1a07090e-cff8-441b-9ea6-9909666ee3a0,2,assistant,"Tere! Kõigepealt peaksid pöörduma kaebusega otse kaupleja poole. Seadus kohustab kauplejat vastama sinu kaebusele 15 päeva jooksul. Alles siis, kui kaupleja ei vasta või te ei jõua kokkuleppele, saad pöörduda Tarbijavaidluste komisjoni poole.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d32e2dd9-a5fc-462f-ac99-84aeb028135c,1,user,"Tere! Mul on küsimus TTJA otsuste kohta. Mis saab, kui ma ei nõustu TTJA otsusega?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d32e2dd9-a5fc-462f-ac99-84aeb028135c,2,assistant,"Tere! Kui sa ei nõustu TTJA otsusega, on sul õigus selle vaidlustada. Sul on kaks võimalust: esitada vaie TTJA peadirektorile või esitada kaebus Tallinna Halduskohtule.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8bad8663-07d1-4e21-bc2a-78cf3df1adb9,1,user,"Tere! Mul on küsimus TTJA otsuste kohta. Mis teha, kui ma ei nõustu TTJA tehtud otsusega?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8bad8663-07d1-4e21-bc2a-78cf3df1adb9,2,assistant,"Tere! Kui te ei nõustu TTJA tehtud otsusega, on teil õigus selle vaidlustada. Teil on kaks võimalust: esitada vaie TTJA peadirektorile või esitada kaebus Tallinna Halduskohtule.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d66dff09-a370-4398-9235-481da335b0a4,1,user,"Tere! Mul on küsimus TTJA otsuste kohta. Mis saab, kui ma ei nõustu TTJA tehtud otsusega?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d66dff09-a370-4398-9235-481da335b0a4,2,assistant,"Tere! Kui te ei nõustu TTJA tehtud otsusega, on teil kaks võimalust. Esiteks, saate esitada vaide TTJA peadirektorile. Teiseks, saate esitada kaebuse Tallinna Halduskohtule.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +45611a55-dca3-4afa-a8bb-99352423b4e2,1,user,Tere! Olen kuulnud Safety Gate'ist. Mis see täpselt on ja milleks see mõeldud on?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +45611a55-dca3-4afa-a8bb-99352423b4e2,2,assistant,"Tere! Safety Gate on ELi kiire teabevahetussüsteem. See on mõeldud selleks, et aidata ennetada ja piirata tarbijatele ohtlikke tooteid ELi turul. See võimaldab ka jälgida liikmesriikide ametiasutuste tööd turujärelevalve ja jõustamistegevuse osas.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6f9c3e7f-fee8-4eae-8768-9cb9016693cd,1,user,"Tere! Olen kuulnud Safety Gate'ist, aga ei tea, mis see täpselt on. Saaksid natuke selgitada?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6f9c3e7f-fee8-4eae-8768-9cb9016693cd,2,assistant,"Tere! Safety Gate on ELi kiire teabevahetussüsteem, mille kaudu edastatakse infot ohtlike toodete kohta. Selle abil saab ennetada ja piirata ohtlike toodete jõudmist ELi turule ning tagada, et liikmesriigid jälgiksid toodete ohutust ühtemoodi. See aitab meil teada, milliseid tooteid tuleks vältida ja milliseid kontrollida.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d725aaf5-3875-441c-9e96-0d1a31f62a3f,1,user,"Tere! Ma kuulsin, et Tarbijakaitse Amet tegeleb elektriohutusega. Mis see täpsemalt tähendab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d725aaf5-3875-441c-9e96-0d1a31f62a3f,2,assistant,"Tere! Tõepoolest, me tegeleme sellega. Elektriohutus tähendab, et elektribeede ja -paigaldiste kasutamisel tuleb tagada inimeste elu, tervist, vara ja keskkonna ohutus. See tähendab, et kõik peab olema korras ja turvaline.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +27f6d9ce-6c60-4a86-8cfe-14482340096a,1,user,"Tere! Ma kuulsin, et Tarbijakaitse Amet teeb elektriohutusega seotud asjadel palju tööd. Mis see tähendab tegelikult, kui ma olen tavaline inimene ja elan oma majas?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +27f6d9ce-6c60-4a86-8cfe-14482340096a,2,assistant,"Tere! Jah, me teeme. Põhiliselt tähendab see, et me tahame tagada, et teie maja elektripaigaldised on ohutud ja vastavad nõuetele. See tähendab, et kui te kasutate elektriseadmeid, siis peaks see olema tehtud nii, et see ei ohusta teie elu, tervist, vara ega keskkonda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c1f01f9d-1905-46d5-934b-aacc22fb7e6c,1,user,"Tere! Mul on küsimus elektriturvalisuse kohta. Mida peaks silmas pidama, et kodu kasutamisel oleks kõik ohutu?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c1f01f9d-1905-46d5-934b-aacc22fb7e6c,2,assistant,"Tere! Oluline on tagada, et elektriseadmed ja -paigaldised on korras ja vastavad nõuetele. Elektriseadet võib kasutada alles pärast kontrolli, et see on tehniliselt korras ja ohutu. Peamiselt tasub meeles pidada, et elektritöid ei tohiks ise teha, vaid pöörduda pädeva elektriala isiku poole.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +52a42738-b62f-4f14-ae86-b5e688c07180,1,user,"Tere! Ma tahan teada, mis on Tarbijakaitse ja Tehnilise Järelevalve Ameti (TTJA) roll avalike mänguväljakute osas. Mis on nende peamised kohustused?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +52a42738-b62f-4f14-ae86-b5e688c07180,2,assistant,"Tere! TTJA peab silmas avalike mänguväljakute puhul, et nende rajamine ja rekonstrueerimine on ehitusloa- ja projektikohustuslik tegevus. Lisaks on omanik vastutav mänguväljaku tehnilise korrasoleku ja ohutuse eest. See tähendab, et nad peavad lähtuma heast ehitustavast ja tagama seadmete ohutuse kasutamisel.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +30f98faf-d0ec-4097-afbe-1ab7fda6b73b,1,user,"Tere! Ma kuulsin, et Tarbijakaitse ja Tehnikajärelevalve Amet (TTJA) tegeleb avalike mänguväljakute ohutusega. Kuidas see täpselt töötab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +30f98faf-d0ec-4097-afbe-1ab7fda6b73b,2,assistant,"Tere! TTJA peab silma peal, et avalikud mänguväljakud ja seikluspargid oleksid ohutud. Nende rajamine või rekonstrueerimine vajab ehitusloa või projektikohustuslikku tegevust. Omanik vastutab nende tehnilise korrasoleku ja ohutuse eest ning peab lähtuma heast ehitustavast.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d358cf30-ef06-4de1-addf-9fcc02622c32,1,user,"Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) teeb avalike mänguväljakute osas kontrolli. Mis on nende peamine vastutus selles küsimuses?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d358cf30-ef06-4de1-addf-9fcc02622c32,2,assistant,"Tere! TTJA peamine vastutus on järelvalveteenus, mis tagab, et avalikud mänguväljakud on ohutud ja vastavad nõuetele. Omanik on vastutav mänguväljaku tehnilise korrasoleku ja ohutuse eest. See tähendab, et mänguväljakud peavad olema rajatud heast ehitustavast lähtudes ja seal paigaldatud seadmete peavad olema ohutud nende ettenähtud otstarbel.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2554e1d4-440e-40a2-b2b1-2b08243972b9,1,user,"Tere! Ma kuulsin midagi selle kohta, et elektriseadmete puhul on oluline CE-märk. Mis see tähendab ja millal see on nõutav?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2554e1d4-440e-40a2-b2b1-2b08243972b9,2,assistant,"Tere! CE-märk näitab, et elektriseade vastab Euroopa Liidu ohutuse, tervise ja keskkonna nõuetele. Enamus elektriseadmeid, nagu need, mis kuuluvad LVD, EMCD ja RoHS2 direktiivide alla, peavad selle märgist kandma. Eranditeks on näiteks kõrgepingeseadmed ja elektriarvestid. See tagab, et seadmed on ohutud kasutada ja ei ohusta inimesi, kui neid on paigaldatud ja hooldatud vastavalt juhenditele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5e315d64-f08e-46bd-85b6-8a7c15b1252c,1,user,"Tere! Ma olen huvitatud elektriseadmete kohta teadmise saamisest, eriti seoses Tarbijakaitse ja Tehnilise Järelevalve Ameti nõuetele. Mis on üldiselt oluline, et elektriseade oleks seaduslikult turul saadaval?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5e315d64-f08e-46bd-85b6-8a7c15b1252c,2,assistant,"Tere! Oluline on see, et elektriseade oleks valmistatud Euroopa Liidus kehtivate ohutustehnika tavade kohaselt ja ei tohi ohustada inimeste tervist, kui seda õigesti paigaldatakse ja kasutatakse. Enamus elektriseadmeid peab olema varustatud CE-märgise, kuna need kuuluvad madalpinge-, elektromagnetilise ühilduvuse- ja ohtlike ainete sisaldusdirektiivide alla.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +db5b1c77-916f-414c-867b-7802dae8e273,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib elektriseadmete ohutust. Kuidas see täpsemalt toimib?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +db5b1c77-916f-414c-867b-7802dae8e273,2,assistant,"Tere! TTJA jälgib, et elektriseadmed oleksid valmistatud Euroopa Liidu ohutustehnika reeglite järgi ja ei ohustaks kasutajat, kui neid on paigaldatud ja hooldatud õigesti ning kasutusotstarbega vastavuses. Enamuses elektriseadmed peavad olema varustatud CE-märgistega, mis kinnitab nende vastavust direktiividele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b6206d64-0e18-4d09-99f5-57b17fe78541,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib meediateenuseid. Mis täpselt see tähendab? Kas nad jälgivad ka välismaa telekanaleid?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b6206d64-0e18-4d09-99f5-57b17fe78541,2,assistant,"Tere! TTJA jälgib meediateenuseid, kes on saanud tegevusloa Eestist. See tähendab, et nad kontrollivad nende tegutsemist, et see oleks kooskõlas Eesti seadusandlusega. Välismaa telekanaleid nad ei jälgi, kui need pole Eestist loa saanud. Saate nimekirja väljastatud tegevuslubadest siit: /ariklient/ametist/load-ja-riigiloivud/meediateenuste-tegevusluba.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8a71267d-e629-4cd0-bf8b-e33f081d0723,1,user,"Tere! Ma kuulsin, et TTJA järelvalve meediateenuste suhtes on. Mis see tähendab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8a71267d-e629-4cd0-bf8b-e33f081d0723,2,assistant,"Tere! Jah, TTJA järelkab meediateenuste puhul seda, kas teenuseid osutavad ettevõtted on Eestist saanud tegevusloa. Saate nende nimekirja siit: /ariklient/ametist/load-ja-riigiloivud/meediateenuste-tegevusluba. Mis teid selles osas kõige rohkem huvitab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +9b4a0b4a-6ad6-4de8-910c-41ac6f203b98,1,user,"Tere! Ma kuulsin, et TTJA järelvalvega teeb meediateenused. Mis see tähendab täpselt?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +9b4a0b4a-6ad6-4de8-910c-41ac6f203b98,2,assistant,"Tere! Jah, TTJA järelvalvega teeb meediateenused, aga ainult need, kel on Eestist saadud tegevusluba. Saate nimekirja väljastatud tegevuslubadest siit: /ariklient/ametist/load-ja-riigiloivud/meediateenuste-tegevusluba. Meiega saab pöörduda probleemide või tähelepanekute korral, mis puudutavad meediateenuseid.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +587da5bd-2532-4a1f-8779-a8d3df16a394,1,user,"Tere! Ma kuulen, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb sideteenuste tarbijate kaitsega. Mis täpselt see siis tähendab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +587da5bd-2532-4a1f-8779-a8d3df16a394,2,assistant,"Tere! Jah, see on õige. TTJA reguleerib sideteenuste valdkonda, et kaitsta tarbijate õigusi. Näiteks elektroonilise side seadus sätestab, kuidas sideteenuse lepinguid sõlmida ja milliseid nõudeid peab lepingu tingimustele järgima. See puudutab lepingute muutmist, rikete kõrvaldamist ja piiramistgi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +76f46b5a-ceea-4f50-b7dd-2d69510aa8c2,1,user,"Tere! Ma kuulen, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb sideteenuste tarbijate kaitsega. Kuidas nad seda teevad?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +76f46b5a-ceea-4f50-b7dd-2d69510aa8c2,2,assistant,"Tere! TTJA reguleerib sideteenuste valdkonda eriseaduse, elektroonilise side seaduse kaudu. Selle seaduse 9. peatükk keskendub just sideteenuste osutamisele lõppkasutajale ja nende õiguste kaitsele. See seab nõudeid näiteks lepingu sõlmimisele, rikete kõrvaldamisele ja lepingu muutmisel.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8b3519dc-e2da-4b2d-8be4-8635df468e8b,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) teeb tööd sideteenuste puhul. Mis on nende peamine roll selles valdkonnas?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8b3519dc-e2da-4b2d-8be4-8635df468e8b,2,assistant,"Tere! TTJA peamine roll on tagada tarbijate õiguste kaitse sideteenuste valdkonnas. Selleks reguleerib TTJA sideteenuste osutamist, lähtudes elektroonilise side seadusest. See seadus reguleerib lepingu sõlmimist, rikete kõrvaldamist, piiramist ja lepingu muutmisi/lõpetamist.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5b895fac-eb53-4db3-ae4d-c5eb7de1d8d8,1,user,"Tere! Ma kuulen, et Tarbijavaidluste komisjon aitab lahendada probleeme kauplustega. Kuidas see toimib ja kas see on kergem ja odavam kui kohtuasse minek?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5b895fac-eb53-4db3-ae4d-c5eb7de1d8d8,2,assistant,"Tere! Jah, see on õige. Tarbijavaidluste komisjon pakubki lihtsamalt, mugavamalt ja kiiremini lahendusi kui kohtuasse minek. Komisjonis saab probleemi lahendada tunduvalt vähem aega ja raha kulutades. Vaidluse lahendamine on nii tarbija kui ka ettevõtja jaoks tasuta ja üldjuhul jõutakse tulemusele umbes 90 päeva jooksul alates menetluse algatamisest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a8301f8e-034d-493e-b5db-5eb1fc974b93,1,user,"Tere! Olen kuulnud, et Tarbijavaidluste komisjon on olemas. Mis on selle komisjoni eesmärk ja kuidas see töötab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a8301f8e-034d-493e-b5db-5eb1fc974b93,2,assistant,"Tere! Tarbijavaidluste komisjon on loodud selleks, et lahendada tarbija ja kaupleja vahel tekkinud lepingust tulenevaid vaidlusi lihtsamini, kiiren ja odavamalt kui kohtus. Võrreldes kohtuvaitlusega on see protsess mugav ning nõuab vähem aega ja raha.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +9c6af689-5a63-458e-8c7a-841a32ca5d38,1,user,"Tere! Ma kuulen, et on olemas Tarbijavaidluste komisjon. Mis see komisjon täpselt on ja kuidas see tarbijaile aitab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +9c6af689-5a63-458e-8c7a-841a32ca5d38,2,assistant,"Tere! Tarbijavaidluste komisjon on selline koht, kus saab lahendada vaidlusi, mis tekivad tarbija ja kaupleja vahel lepingu tõttu. See on palju lihtsam ja kiirem kui kohtuvaidlus, ja see on nii tarbija kui ka kaupleja jaoks tasuta. Üldjuhul lahendatakse probleem 90 päeva jooksul.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b8e699bb-019e-4b0b-bfdd-36d604422410,1,user,"Tere! Ma olen kuulnud, et lennusõitjatel on teatud õigused. Mis need on?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b8e699bb-019e-4b0b-bfdd-36d604422410,2,assistant,"Tere! Jah, lennusõitjatel on mitmeid õigusi. Näiteks, kui lennud hiljenevad või tühistatakse, on teil õigus teavitus saada, hüvitist nõuda ja abi saada tagasipöörduse korral. Lisaks on teil õigus saada abi lennujaamas kui teil on erivajadused.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +45ca7a54-ed7b-4d6e-906e-e78cd749cdcf,1,user,"Tere! Mulle on vaja lennureisija õigustest rohkem teada. Mis on olulisim asi, mida ma peaksin meeles pidama?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +45ca7a54-ed7b-4d6e-906e-e78cd749cdcf,2,assistant,"Tere! Olulisim asi on see, et sul on õigus saada abi lennujaamas, olenemata sinu erivajadusest. See tähendab, et saad abi registreerimisel, pagasiregistreerimisel ning isegi transpordiga väravani jõudmiseks. On oluline teatada abivajadusest teenusepakkujale 48 tundi enne lennu väljumisaega.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e8474877-1904-4070-91e6-783f935418fe,1,user,Tere! Mis õigused on mul kui lennureisija?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e8474877-1904-4070-91e6-783f935418fe,2,assistant,"Tere! Lennureisijana on sul mitmeid õigusi. Näiteks, kui lend hiljub või tühistatakse, sul on õigus teavitamist saada ja hüvitist nõuda, samuti saad abi saada lennujaamas, kui sul on erivajadusi. On ka õigusi seoses pagasi transpordi ja transpordivahendi kahjustuse puhul. Soovid, et ma puutuksin mõnele neist detailsemalt lahti?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ed8c6755-61c8-4a01-94fb-a2e701e88b22,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitseamet saab tarbijaid aitada vaidluste lahendamisel. Kuidas see menetlus üldiselt toimib, kui ma esitan avalduse?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ed8c6755-61c8-4a01-94fb-a2e701e88b22,2,assistant,"Tere! Meeldiv kuulata. Pärast avalduse esitamist, saame me sind teavitada 21 tööpäeva jooksul, et menetlus on käivitunud. Seejärel edastame avalduse kauplejale, et nad annaksid selgitusi ja pakkuksid lahendusi. Meiega saad ühendust ka selgituste kohta täiendavalt",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +fca804d5-9344-4db7-97cd-a6e42f056df0,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitseamet menetleb kaebusi kauplejatega tekkinud vaidluste puhul. Kuidas see menetlus üldiselt käib, kui ma esitan avalduse?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +fca804d5-9344-4db7-97cd-a6e42f056df0,2,assistant,"Tere! Jah, see on õige. Kui esitad Tarbijakaitseametile avalduse, saad teavitus 21 tööpäeva jooksul, et avaldus on menetlusse võetud. Seejärel edastatakse avaldus kauplejale, et nad saaksid anda selgitusi ja leida lahendust vaidlusele. Sinule saadetakse ka täiendav teavitus selle kohta.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5c52d0a2-ba26-445b-b191-f947552e9c8d,1,user,"Tere! Ma tahan teada, kuidas Tarbijakaitse Ametis avaldust menetletakse. Mis juhtub, kui ma avalduse esitan?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5c52d0a2-ba26-445b-b191-f947552e9c8d,2,assistant,"Tere! Hea, et küsid. Pärast avalduse esitamist saad 21 tööpäeva jooksul teate, et sinu avaldus on menetlusse võetud. Seejärel edastab sekretariaat avalduse kauplejale, et ta saaks anda selgitusi ja otsida lahendust. Sa saad sellest täiendavalt teavitatud.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a76d8b7a-ab94-4af9-bed3-d65172890ecf,1,user,"Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib sideturge. Mis need täpsemalt on?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a76d8b7a-ab94-4af9-bed3-d65172890ecf,2,assistant,"Tere! See on väga hea küsimus. Sideturgude puhul TTJA jälgib näiteks kõne lõpetamise hulgiteenust telefonivõrgus või mobiiltelefonivõrgus, kohalikku juurdepääsu hulgiteenust kindlas kohas ja kvaliteetset juurdepääsu hulgiteenust samuti kindlas kohas. See on oluline, et tagada konkurents ja õiged hinnad tarbijatele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b90cb219-af6c-4d37-897b-17e8e21064be,1,user,"Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib sideturgu. Mis need täpselt on ja mida TTJA seal teeb?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b90cb219-af6c-4d37-897b-17e8e21064be,2,assistant,"Tere! Sideturgude all silmitakse erinevaid teenuseid, nagu näiteks kõne lõpetamise hulgateenus telefonivõrgus, mobiiltelefonivõrgus, ning kohalik juurdepääsu hulgateenus või masstoodete keskse juurdepääsu hulgateenus. TTJA peab silmas elektroonilise side turgude arenguid, analüüsib neid ja annab vajadusel ka loa rändlusteenustel lisatasude rakendamiseks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +753c61ac-c9c7-4fbb-a729-6b2824fcd4d7,1,user,"Tere! Ma lugesin TTJA veebilehel sideturu kohta ja natuke segadus. Mis need hulgiturgude teenused täpsemalt on, mida seal loetletud on? Näiteks kõne lõpetamise hulgiteenus telefonivõrgus - mis see tähendab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +753c61ac-c9c7-4fbb-a729-6b2824fcd4d7,2,assistant,"Tere! Hea, et küsid. Need hulgiturgude teenused on sellised teenused, mida operaatorid pakuvad teistele operaatoritele, mitte otse lõppkasutajale. Näiteks, kõne lõpetamise hulgiteenus telefonivõrgus tähendab, et üks operaator maksab teisele operaatorile selle eest, et tema klient saaks teha kõne teise operaatori klientidele. Sisuliselt on see hulgiturgude teenus, mis võimaldab erinevatel operaatoritel omavahel ühendust tagada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5eb0dec4-563b-43f2-ab82-8f8ce704f34c,1,user,"Tere! Ma ostsin pojale uue mänguasja, aga ma ei saa aru, mida ma selle kohta teada peaks kontrollima, et see ohutu oleks? Mida see CE-märk tähendab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5eb0dec4-563b-43f2-ab82-8f8ce704f34c,2,assistant,"Tere! See on väga hea küsimus! CE-märk näitab, et mänguasi vastab Euroopa Liidu ohutusnõuetele. See ei garanteeri 100% ohutust, aga see on oluline näitaja. Kontrollige ka, kas mänguasi on sobiva vanusgrupi jaoks. See on oluline, et laps saaks sellest turvaliselt nautida.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +aa15d762-fa64-4ef7-828d-a10fb703b893,1,user,"Tere! Ma olin just poest mänguasi ostmas pojale, aga nüüd ma mõtlen, mida ma peaks silmas pidama, et see oleks ohutu. Mis on kõige olulisem märk, mida mänguasil peaks olema?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +aa15d762-fa64-4ef7-828d-a10fb703b893,2,assistant,"Tere! See on väga hea, et sa silmas pead ohutust. Kõige olulisem märk mänguasil on CE-märk. See garanteerib, et mänguasi vastab Euroopa Liidu ohutuse ja muude nõuetele. Mänguasi peaks olema sellega märgistatud nähtaval viisil.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +9baba1e9-3a75-46eb-a745-a5eb2fe9d12f,1,user,"Tere! Mul on küsimus laste mänguasjade ostmise kohta. Mis on kõige olulisem asi, millele peaksin tähelepanu pöörama, et osta ohutu mänguasi?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +9baba1e9-3a75-46eb-a745-a5eb2fe9d12f,2,assistant,"Tere! Kõige olulisem on vaadata, kas mänguasi kannab CE-märgist. See märgistus peaks tagama, et mänguasi vastab nõuetele. Aga see ei ole kõik – loe ka müügipakendi soovitusi ja hoiatusi!",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2e5c8467-719b-4f65-8e18-97a56a83dbac,1,user,"Tere! Ma olen kuulnud, et pürotehnika kasutamisel on olulisi reegleid. Mis kõige olulisem, mida ma peaksin teadma?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2e5c8467-719b-4f65-8e18-97a56a83dbac,2,assistant,"Tere! Kõigepealt oluline: F4 ja T2 kategooria pürotehnikat tohib kasutada ainult pürotehnik ise. Samuti on oluline, et saaksite toote ohutult kasutada, seega ostate pürotehnikat ainult ametlikust müügikohast, mitte kellegi käest tänavalt.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e0548dc3-3646-4744-9eca-13481495e827,1,user,"Tere! Ma olen kuulnud, et pürotehnika kasutamisega on teatud reeglid. Kas sa saaksid mulle rääkida millestki olulisest?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e0548dc3-3646-4744-9eca-13481495e827,2,assistant,"Tere! Jah, kindlasti. Oluline on teada, et pürotehniliste toodete kasutamisel tuleb arvestada avalikku korda. Näiteks, F4- ja T2-kategooria pürotehnikat tohib kasutada ainult pürotehnik ise. Kas sa teadsid seda?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +167f062d-3247-493f-aa44-221e30bc849a,1,user,Tere! Ma kuulsin midagi hoonete energiatõhususest ja Tarbijakaitse ja Tehnilise Järelevalve Ametist. Mis see tähendab täpselt?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +167f062d-3247-493f-aa44-221e30bc849a,2,assistant,"Tere! Hoonete energiatõhusus tähendab seda, et uutele ja oluliselt rekonstrueeritavatele hoonetele on kehtestatud miinimumnõuded selle kohta, kui palju energiat nad tarbivad. Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib, et neid nõudeid järgitaks. Näiteks, uuelt hoonelt ootatakse, et see oleks energiatõhusam kui varem. Kas sa tahaksid teada, kuidas need nõuded täpsemalt kehtivad?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +9c41a1c8-e22d-43d4-8f87-dbae8a303481,1,user,"Tere! Ma kuulsin midagi hoonete energiatõhususe kohta, aga ma ei saa päriselt aru, mis see tähendab ja kes sellega tegelevad Eestis? Kas Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) on selles rollis?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +9c41a1c8-e22d-43d4-8f87-dbae8a303481,2,assistant,"Tere! Jah, Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegelikult on seotud hoonete energiatõhususega. Nad järelveevad, et ehitised vastaksid energiatõhususe nõuetele. Põhimõtteliselt tähendab hoonete energiatõhusus seda, et ehitised kasutaksid vähem energiat soojendamiseks, jahutamiseks ja valgustamiseks, samal ajal kui need tagaksid mugava elukeskkonna.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +45a99800-c9c9-454a-a0a8-c5185c32da17,1,user,Tere! Mul on küsimusi hoonete energiatõhususe kohta. Mis on Tarbijakaitse ja Tehnilise Järevalve Ameti (TTJA) roll selles küsimuses?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +45a99800-c9c9-454a-a0a8-c5185c32da17,2,assistant,"Tere! TTJA järelvalvab hoonete energiatõhususe miinimumnõuete täitmist. See tähendab, et nad tagavad, et uued ja rekonstrueeritud hooned vastavad kehtivatele standarditele. Nad määravad ka suurettevõtetele energiaaudiidikohustuse täitmist.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +32830531-da52-4f16-86a9-d1e58632fece,1,user,Tere! Ma kuulsin Rail Balticu kohta palju juttu. Kuidas Tarbijakaitse ja Tehnilise Järelevalve Amet selle projektiga seotud on?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +32830531-da52-4f16-86a9-d1e58632fece,2,assistant,"Tere! Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) menetleb Rail Balticu projekti puhul ehitus- ja kasutuslubasid, samuti projekteerimistingimusi. Kui ehitisel baseerub riigi eriplaneering, siis on TTJA ehitusloa andjaks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +326ffdc7-a877-41d1-8f42-b57e7aa1c412,1,user,"Tere! Mul on küsimusi Rail Balticu kohta. Kust ma saan teada, kes selle projekti planeerimisel kõige rohkem kaasa tegi?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +326ffdc7-a877-41d1-8f42-b57e7aa1c412,2,assistant,"Tere! Rail Balticu projekti planeerimisel juhtis laiapõhjaline juhtkomitee, kus olid esindatud Pärnu, Rapla ja Harju maavanemad ning maavalitsuste esindajad. Sinna kuulusid ka Eesti Raudtee, Keskkonnaministeerium, Maa-amet, Tarbijakaitse ja Tehnilise Järelevalve Amet ja Majandus- ja Kommunikatsiooniministeerium.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +026d17e3-76a8-4f00-9e02-810615b1788b,1,user,Tere! Ma kuulsin midagi ohtlike toodete kohta. Mis see tähendab ja kes sellega tegelda oskab?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +026d17e3-76a8-4f00-9e02-810615b1788b,2,assistant,"Tere! See tähendab, et mõned tooted võivad tarbijale ohtu kujutada. Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) tegelda sellega. EL-is on ka süsteem nimega Safety Gate, mille kaudu järelevalveasutused vahetavad infot ohtlike toodete kohta, et need turult kiiresti eemaldada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +1d94fe13-0ee8-4cda-ac05-35368121a2ab,1,user,"Tere! Olen kuulnud, et mõned tooted on ohtlikud. Kuidas ma saan teada, millised tooted on just praegu probleemiks?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +1d94fe13-0ee8-4cda-ac05-35368121a2ab,2,assistant,"Tere! EL-is on selleks kiirhoiatussüsteem Safety Gate. Saa tutvuda nädala ülevaatega nende kodulehel https://ec.europa.eu/consumers/consumers_safety/safety_products/rapex/alerts/?event=main.listNotifications&lng=en või kasuta otsingut, kui sul on huvi mingi kindla toote suhtes.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c99b6255-45e1-4049-8589-d4300a85bc40,1,user,Tere! Ma kuulsin midagi ohtlike toodete kohta. Mis see tähendab ja kes sellega tegelda tohib?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c99b6255-45e1-4049-8589-d4300a85bc40,2,assistant,"Tere! See tähendab, et tegemist on toodetega, mis võivad tarbijale ohtu kujutada. Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) ja EL-i kiirhoiatussüsteem Safety Gate jälgivad ja teavitavad selliseid olukordi. Safety Gate abil vahetavad järelevalveasutused infot ohtlike kaupade kohta, et need võimalikult kiiresti turult eemaldada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b67aa77c-b26a-48fc-bfb4-81a6c9d44b49,1,user,Tere! Mida see tekst peamiselt räägib?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b67aa77c-b26a-48fc-bfb4-81a6c9d44b49,2,assistant,"Tekst räägib peamiselt mõõtevahendite direktiivide ja markeerimise nõuetele vastavuse kohta Euroopa Liidus. Seal on loetletud erinevad direktiivid erinevatele mõõtevahenditele ja selgitatud, kuidas neid markeerida, et näidata nende vastavust standarditele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b02124c3-480a-4f3b-b9a5-14c90a095c1b,1,user,Tere! Mis on selle dokumendi peamine eesmärk?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b02124c3-480a-4f3b-b9a5-14c90a095c1b,2,assistant,"Dokumendi peamine eesmärk on kirjeldada mõõtevahendite direktiive ja märgistamist, mis vastavad Euroopa Liidu õigusaktidele. See hõlmab mitmeid direktiive, mis reguleerivad erinevate mõõtevahendite, nagu elektriarvestid, pikkusmõõdud ja rehvimanomeetrid, standardeid ja kontrolli.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +af6ecbef-e11a-4c3b-816d-ac3207bd3826,1,user,Tere! Mis on Akadeemia Vabaühenduse peamine tegevus?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +af6ecbef-e11a-4c3b-816d-ac3207bd3826,2,assistant,"Vabandage, tekstis ei mainita Akadeemia Vabaühendust. Tekst käsitleb peamiselt mõõtevahendite direktiive ja märgistamist Euroopa Liidus.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e45d6da9-a65a-4e14-8ed2-e48d678ec7bf,1,user,"Tere! Ma olen kuulnud, et kasutatud auto ostmine võib olla riskantne. Mis on kõige olulisem, mida ma peaksin silmas pidama, kui ma kavatsen kasutatud autot osta?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e45d6da9-a65a-4e14-8ed2-e48d678ec7bf,2,assistant,"Tere! See on väga hea küsimus. Kõige olulisem on olla tähelepanelik ja veenduda, et sa tead, keda sa ostate. Vaata äriregistrist, kas autot müüb era- või juriidiline isik. Eraisiku käest osta on odavam, aga eraisikutevahelisi vaidlusi lahendab vaid kohus. Kui ostate firmalt, saad infot Krediidiinfo kodulehelt ja äriregistrist.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +cf922783-665f-4da5-a894-6b70b4683110,1,user,"Tere! Mul on küsimusi droonide kasutamise kohta Eestis. Mis on kõige olulisem asi, mida ma peaksin teadma nende kasutamisel, eriti sageduste osas?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +cf922783-665f-4da5-a894-6b70b4683110,2,assistant,"Tere! Droonide puhul on oluline teada, et raadiosagedusi kasutatakse nii drooni juhtimiseks kui ka video edastamiseks. Paljud droonid, mida kasutatakse hobina, ei vaja sagedusluba, kuid need peavad siiski töötama Euroopa standarditele vastavate sageduste piires, mis on määratud Eesti raadiosagedusplaanis (ERSP).",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +bce32b70-b2d2-47f0-be8e-6f78f243ab74,1,user,"Tere! Ma kuulsin, et droonide kasutamine on muutunud keerulisemaks. Mis mulle selle kohta teada peaks olema Tarbijakaitse ja Tehnilise Järelevalve Ameti poolt?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +bce32b70-b2d2-47f0-be8e-6f78f243ab74,2,assistant,"Tere! Jah, droonide kasutamisel on oluliselt rohkem regulatsioone. Peamine asi on see, et droonide juhtimiseks kasutatavad raadiosagedused peavad olema nõuetele vastavad. See tähendab, et droonil peab olema CE-märgistus ja vastavusdeklaratsioon.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +fd5b4da5-4ba5-411d-8051-6b0dc2c589a1,1,user,"Tere! Ma olen kuulnud, et droonide kasutamine on muutunud üsna levinud. Kas Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib nende kasutamist?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +fd5b4da5-4ba5-411d-8051-6b0dc2c589a1,2,assistant,"Jah, küllap! TTJA jälgib droonide kasutamist, eriti seoses raadiosageduste kasutusega. Droonid kasutavad neid sagedusi nii juhtimiseks kui ka video edastamiseks. Oluline on, et need seadmed vastaksid nõuetele, sealhulgas CE-märkise olemasolule.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5a693b49-78f1-42c2-9d6c-fae716b902e6,1,user,"Tere! Ma kuulen, et Tarbijakaitseametis on mingi Tarbijavaidluste komisjon. Mis see komisjon teeb?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5a693b49-78f1-42c2-9d6c-fae716b902e6,2,assistant,"Tere! Jah, Tarbijavaidluste komisjon on Tarbijakaitse ja Tehnilise Järelevalve Ameti juures. See komisjon vahendab Sinu vaidlusi kauplejaga, kui Te ei ole saanud sellega ise lahendust leida.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +1e6b2a10-8bab-4ee0-8d22-784a36cec7dd,1,user,"Tere! Mul on küsimus Tarbijavaidluste komisjoni kohta. Kas saaksid rääkida, mis see komisjon on?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +1e6b2a10-8bab-4ee0-8d22-784a36cec7dd,2,assistant,"Tere! Tarbijavaidluste komisjon on Tarbijakaitse ja Tehnilise Järelevalve Ameti osa, mis lahendab tarbijate vaidlusi kauplejatega. Kui sa ei saa oma probleemiga kauplejaga kokkuleppele, saad avalduse komisjonile esitada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6e1ed3f2-37da-493b-ade9-0dbcc7b8d0cd,1,user,"Tere! Ma kuulsin, et on selline Tarbijavaidluste komisjon. Mis see komisjon tegelikult on ja millal ma saan seda kasutada?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6e1ed3f2-37da-493b-ade9-0dbcc7b8d0cd,2,assistant,"Tere! Tarbijavaidluste komisjon on Tarbijakaitse ja Tehnilise Järelevalve Ameti juures tegutsev organ. Sa saad seda kasutada siis, kui sul on tekkinud vaidlus kauplejaga ja teinud kaubandusvaidluse lahendamisel jõuetud oled. Komisjon vaatab sinu juhtumit üle ja püüab leida lahenduse.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c7b1a20e-aef4-4a16-b261-ebf51ed4b409,1,user,"Tere! Ma kuulsin, et nüüdisaegselt saan oma telefoninumbrit ühe operaatori juurest teise juurde liikuda. Kuidas see üldse toimib?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c7b1a20e-aef4-4a16-b261-ebf51ed4b409,2,assistant,"Tere! See on õige, numbri liikuvus on võimalik. Põhimõtteliselt on kaks võimalust. Sa saad volitada uut operaatorit lõpetama sinu praegune leping endisega. Või siis, kui operaatoritel on kokkulepe, saab koheselt sõlmitud tingimuslik liitumisleping ja antakse uus SIM-kaart. See numbri liikuvus jõustub alles hiljem.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +10346935-cc29-46a9-a2c0-22701f502e74,1,user,Tere! Mul on küsimus teenuse ohutuse kohta. Mis on minu õigused tarbija?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +10346935-cc29-46a9-a2c0-22701f502e74,2,assistant,"Tere! Tarbija õigus on see, et teenus peab vastama kehtestatud nõuetele. Kui erinõudeid pole, siis teenuse tavakasutus ei tohi ohustada sinu elu, tervist või vara. Kas sa oled huvitatud teadma, kuidas teenuse pakkuja seda tagab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ad8857c6-05d2-4874-84f8-c66a84ed66e7,1,user,Tere! Mul on küsimus teenuse ohutuse kohta. Mis on tarbija õigused teenuse osutajal?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ad8857c6-05d2-4874-84f8-c66a84ed66e7,2,assistant,"Tere! Põhiliselt on sul õigus oodata, et teenus vastab kehtestatud nõuetele. Ja kui erinõudeid ei ole, ei tohi teenus seada ohtu sinu elu, tervist või vara. See tähendab, et teenusepakkuja peab teenuse ohutuse tagama.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d3373a05-d1b8-4729-8741-58611d51a7d0,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitseamet järelvalve teenuste ohutuse suhtes. Mis see tähendab tegelikult? Mis on minu õigused teenusena kasutades?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d3373a05-d1b8-4729-8741-58611d51a7d0,2,assistant,"Tere! See tähendab, et teenuseid peab osutatama nii, et need vastaksid kehtestatud nõuetele. Kui erinõudeid ei ole, siis ei tohi teenuse sihipärane kasutamine ohustada sinu elu, tervist või vara. Sul on õigus saada infot teenuse kohta – nii suuliselt kui kirjalikult. Näiteks kasutusjuhendid, lepingud jne. Peamine on, et sa aru saaksid, kuidas teenust kasutada ja millised riskid sellega võivad kaasneda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4a4b9263-1d3a-4645-bdf5-621eda7f3e99,1,user,Tere! Ma kuulsin midagi numbrite eraldamisest Tarbijakaitse ja Tehnilise Järelevalve Ameti poolt. Mis see tähendab?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4a4b9263-1d3a-4645-bdf5-621eda7f3e99,2,assistant,"Tere! See tähendab, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) väljastab numbrilube sideteenuse osutajatele, näiteks mobiili- ja telefonifirmadele. Ka sisuteenuse osutajad, kes kasutavad numbreid teenuste pakkimiseks, saavad loa. Eraseisikutele neid lube aga välja ei antaks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4a45c9d7-2d47-4eb0-9412-c4bfc8b65680,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb numeratsiooniga. Mis see täpsemalt tähendab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4a45c9d7-2d47-4eb0-9412-c4bfc8b65680,2,assistant,"Tere! TTJA väljastab numbrilube sideteenuse osutajatele, ehk sideettevõtjatele, ning ka neile, kes pakuvad muud teenust side teenuse abil, näiteks sisuteenuseid. See tähendab, et nad määravad, kes tohib kasutada erinevaid numbrimudeleid, näiteks telefoninumbreid või mobiilnumbreid.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f4cd5d09-f409-4024-ab39-d75b3246b737,1,user,Tere! Ma just kuulsin midagi numbriloast Tarbijakaitse ja Tehnilise Järelevalve Ameti poolt. Mis see täpselt on?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f4cd5d09-f409-4024-ab39-d75b3246b737,2,assistant,"Tere! See tähendab, et TTJA väljastab numbriload sideteenuse osutajatele – sideettevõtjatele ja ka nendele, kes pakuvad muud teenust, kasutades sidevõrku. Näiteks, kui sa soovid mobiilnumbriga tegutseda, peab sinu sideettevõtjal olema selleks TTJA numbriluba.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4b4a407a-9fd1-4f5a-814f-8ebb77b172f2,1,user,"Tere! Ma kuulsin palju 5G kohta, aga ei saa päris hästi aru, mida see tähendab. Mis on need kolm peamist arengusuunda, mida ITU näeb 5G puhul?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4b4a407a-9fd1-4f5a-814f-8ebb77b172f2,2,assistant,"Tere! ITU näeb 5G puhul kolme peamist arengusuunda: suurendatud andmesidekiirus (eMBB), tagatud ajakriitiline reaalajasüsteemide andmeside (URLLC) ja suurearvuline väikeseadmete kommunikatsioon (mMTC). See tähendab lihtsalt seda, et 5G lubab kiiremaid andmesidegaid, võimaldab reaalajas teenuseid ja toetab rohkemat arvukust seadmete ühenduse kohta.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c58d6ade-287c-497f-88c3-02da35f91d7d,1,user,"Tere! Olen kuulnud palju 5G kohta, aga ma ei ole päris kindel, mis see tähendab. Saaksite mulle öelda, mis on 5G põhilised arengusuunad selle materjali järgi?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c58d6ade-287c-497f-88c3-02da35f91d7d,2,assistant,"Tere! 5G tehnoloogia arenguteks on kolm peamist suunda: suurendatud andmesidekiirus (eMBB), tagatud ajakriitilised reaalajasüsteemid (URLLC) ja suurearvuline väikeseadmete kommunikatsioon (mMTC). Nii et põhimõtteliselt on see kiirem, usaldusväärsem ja suudab rohkem seadmeid korraga toetada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +267c2f8d-2d3f-43d0-8aae-ff9d6071b259,1,user,"Tere! Olen kuulnud palju 5G kohta, aga ma ei saa päris hästi aru, mida see tähendab. Mis on need kolm peamist arengusuunda, mida Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) mainib?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +267c2f8d-2d3f-43d0-8aae-ff9d6071b259,2,assistant,"Tere! TTJA kohaselt on kolm peamist arengusuunda 5G tehnoloogias: suurendatud andmesidekiirus (eMBB), tagatud ajakriitiline reaalajasüsteemide andmeside (URLLC) ja suurearvuline väikeseadmete kommunikatsioon (mMTC). See tähendab, et 5G lubab palju kiiret internetti, usaldusväärseid ühendusi reaalajas rakendusteks ja toetab paljusid seadmeid, mis on ühendatud võrku.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c67bf787-2b5a-4398-a390-111f8d7809ad,1,user,Tere! Ma kuulsin Rail Balticu kohta palju. Kus ma saaksin infot selle keskkonnamõjude hindamise kohta?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c67bf787-2b5a-4398-a390-111f8d7809ad,2,assistant,Tere! Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) avalikustab seonduvaid teateid oma veebilehel. Leiad need siit: https://ttja.ee/eraklient/tarbija-oigused/avalikud-teated/keskkonnamoju-hindamisega-seotud-teated,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +fd0b550d-bff5-41e2-8cd2-da5edb0e91d8,1,user,"Tere! Ma kuulsin Rail Balticu kohta palju, aga ma ei tea, kuidas selle keskkonnamõju kohta infot leida. Kas Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) sellega tegeleb?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +fd0b550d-bff5-41e2-8cd2-da5edb0e91d8,2,assistant,"Jah, TTJA tegeleb Rail Balticu keskkonnamõjude hindamisega (KMH). Kõik avalikud teated selle kohta on leitavad TTJA veebilehel, aadressil https://ttja.ee/eraklient/tarbija-oigused/avalikud-teated/keskkonnamoju-hindamisega-seotud-teated.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a656a8ac-6f34-49b4-a593-48b046fbb667,1,user,Tere! Ma kuulsin Rail Balticu kohta palju juttu. Kus saab infot selle keskkonnamõjude hindamise kohta?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a656a8ac-6f34-49b4-a593-48b046fbb667,2,assistant,Tere! Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) avalikustab selliseid teateid. Need on leitavad siit: https://ttja.ee/eraklient/tarbija-oigused/avalikud-teated/keskkonnamoju-hindamisega-seotud-teated,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e982ddac-ab75-481f-b28d-aa01560da004,1,user,"Tere! Ma lugesin seda artiklit sideteenuste kohta, aga mul on veidi segadus. Mis on kõigepealt oluline teada roamingu kohta?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e982ddac-ab75-481f-b28d-aa01560da004,2,assistant,"Tere! Oluline on teada, et roaming on sideteenus, mis algab sinu koduriigis ja lõppeb teise Euroopa Liidu liikmesriigi telefonil. See on teenus, mille eest tavaliselt tasutakse tegeliku kasutamise põhjal. Artiklis on ka infot selle kohta, kuidas roamingu tasud on piiratud.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +50c68a75-fdbf-4136-87ae-8bda40b20fb9,1,user,"Tere! Ma lugesin midagi liiklusvoodu kohta, aga olen natuke segadus. Kas sa saaksid mulle selgitada, mis on liiklusvoolu piiramine ELis?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +50c68a75-fdbf-4136-87ae-8bda40b20fb9,2,assistant,"Tere! Liiklusvoolu piiramine ELis tähendab, et Euroopa Liidus ei tohi arveldada kõnede ja SMS-ide eest erinevalt riikide kaupa. Näiteks, kui te teete kõne teise EL riigi numbril, siis arvelduse summa ei tohiks olla suurem kui kui te oleksite kõnet teinud oma koduriigis. Täpsemalt, kõneminuti kohta 0,19/0,228 eurot, SMS-i kohta 0,06/0,072 eurot (ilma või koos käibemaksuta).",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +7fdffee7-879b-48d3-bf94-fac89034f55a,1,user,"Ma olen kuulnud, et on mingid reeglid, mis piiravad roaming-tasusid. Kas see on tõsi?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +7fdffee7-879b-48d3-bf94-fac89034f55a,2,assistant,"Jah, see on tõsi! Euroopa Liidus on reeglid, mis piiravad, kui palju sinult roaming-teenuste eest küsitakse. Näiteks kõneminuti kohta ei tohi küsitud tasu ületada 0,19 / 0,228 eurot ja SMS-i kohta 0,06 / 0,072 eurot.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0798a193-78c8-4c22-a662-6996b5136f01,1,user,Tere! Mul on küsimus. Kuidas Eestis raadiosageduste kasutamist reguleeritakse?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0798a193-78c8-4c22-a662-6996b5136f01,2,assistant,"Tere! Raadiosageduste kasutamist Eestis reguleerib seadus, mis tugineb Euroopa Liidu standarditele. Konkreetsemat infot selle kohta saab EFIS andmebaasist, mis on osa Euroopa Raadiosageduste Informatsiooni Süsteemist (EROFrequency Information System).",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0b6657a4-4f33-42f5-9caf-e6a90b8c42ef,1,user,"Tere! Ma kuulsin midagi raadiosageduste kohta, aga ei saa pähe. Kes need Eestis siis reguleerib?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0b6657a4-4f33-42f5-9caf-e6a90b8c42ef,2,assistant,Tere! Raadiosageduste kasutamist Eestis reguleerib Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA). Aga Euroopa tasandil on selleks EFIS (EROFrequency Information System).,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d232a582-cd36-4623-9da2-f9ab5bef4ebb,1,user,Tere! Ma kuulsin midagi raadiosagedustest ja Tarbijakaitse ja Tehnilise Järelevalve Ameti seotusest. Mis see tähendab?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d232a582-cd36-4623-9da2-f9ab5bef4ebb,2,assistant,"Tere! Raadiosageduste kasutamist Eestis reguleeribki Tarbijakaitse ja Tehnilise Järelevalve Amet. See tähendab, et nad jälgivad, kuidas me neid sagedusi kasutame, et ei oleks häireid ja kõik toimiks korrektselt. Suur osa selle info on kättesaadav Euroopa infobaasis, nimega EFIS.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0c015123-4391-47c8-a69a-a294750985a6,1,user,"Tere! Ma kuulen palju tõstukite ohutusest. Kas kellelgi on mingit aimu, kui palju tõstukid Eestis üldse on?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0c015123-4391-47c8-a69a-a294750985a6,2,assistant,"Tere! See on hea küsimus. Täpse arvu ei tea keegi, kuid ettevõtted arvavad, et neid on rohkem kui 100 000, ja võib-olla isegi kuni pool miljonit. Seega on nende ohutuse tagamine päris suur väljakutse.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8e7cd7dd-9520-42b7-a8c0-8ec7b7a901a0,1,user,"Tere! Mul on küsimusi tõstukite ohutuse kohta. Kas teil on aimu, kui palju tõstukse on Eestis kasutusel?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8e7cd7dd-9520-42b7-a8c0-8ec7b7a901a0,2,assistant,"Tere! See on keeruline küsimus. Täpset arvu ei teata, kuid ettevõtted arvavad, et neid on rohkem kui 100 000 ja võib-olla isegi kuni pool miljonit. Seadmete ohutuse tagamine on seega suur ülesanne.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b0e129ab-c290-4d22-8c8a-f5dfd6afadb0,1,user,"Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb tõstukite ohutusega. Kas teil on aimu, mitu tõstukit Eestis umbes on?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b0e129ab-c290-4d22-8c8a-f5dfd6afadb0,2,assistant,"Tere! See on hea küsimus. Täpset arvu ei tea keegi, aga valdkonnaga seotud ettevõtted arvavad, et tõstukite arv on üle 100 000, ja võib küündida isegi kuni poole miljoni ukseni. See on üsna palju, ja seega on oluline tagada nende ohutus.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a63be981-6a5a-4f23-a6dc-5ef9acdbc71b,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) jälgib gaasiseadmete ohutust. Kuidas see täpselt toimib?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a63be981-6a5a-4f23-a6dc-5ef9acdbc71b,2,assistant,"Tere! TTJA peab silmas, et gaasiseadmed müügil ei tohiks ohustada inimeste tervist ja turvalisust. See tähendab, et nad peavad olema valmistatud ja paigaldatud nõuetekohaselt, regulaarselt hooldatud ning kasutama ettenähtud otstarbel. TTJA võib peatada ohtliku gaasiseadme levitamise ja kasutamise, kui tekib kahtlus ning on õigus nõuda selle turult kõrvaldamist, kui ekspertiis näitab, et seade ei vasta nõuetele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4f0ddf4a-f8f7-402b-be51-ed878750948a,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) järelvalvega gaasiseadmete suhtes. Mis see tähendab tegelikult?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4f0ddf4a-f8f7-402b-be51-ed878750948a,2,assistant,"Tere! See tähendab, et TTJA jälgib, et turul olevad gaasiseadmed ei ohusta inimeste tervist ja turvalisust. Oluline on, et need oleksid valmistatud ja paigaldatud õigesti, regulaarselt hooldatud ja kasutatud ettenähtud otstarbel. TTJA võib peatada ohtliku gaasiseadme levitamise ja kasutamise, kui on kahtlusi selle ohutuses.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5872d47f-6cd8-4673-95fa-378885492f2f,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitseamet järelvalvib gaasiseadmete turvalisust. Kuidas see täpsemalt toimib?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +5872d47f-6cd8-4673-95fa-378885492f2f,2,assistant,"Tere! Jah, TTJA järelvalve eesmärk on tagada, et turul olevad gaasiseadmed ei ohusta inimeste tervist ja turvalisust. See tähendab, et seadmed peavad olema valmistatud ja paigaldatud õigesti, regulaarselt hooldatud ning kasutama ettenähtud otstarbel. TTJA võib peatada ohtliku seadme levitamise ja kasutamise ning vajadusel nõuda selle turult kõrvaldamist, kui ekspertiis näitab, et seade ei vasta nõuetele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +34053710-2fdd-4540-bdf0-c9487426249c,1,user,"Tere! Ma olen plaaninud pakettreisi, aga ma ei tea päris täpselt, mida see tähendab. Kuidas ma aru saan, kas mu reis on pakettreis?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +34053710-2fdd-4540-bdf0-c9487426249c,2,assistant,"Tere! Pakettreis on siis, kui reisikorraldaja on kokku pannud reisiteenuste kogumi. See tähendab, et sa tegelikult ostsid ühe paketti, mitte eraldi lennupileti ja hotelli. Aga on ka juhtumisi, kus eraldi lepinguid on sõlmitud, ent see on ikkagi pakettreis, kui sa näiteks valisid kõik teenused ühest kohast ja maksad ühtse hinna eest. Kas Sa soovid, et ma selgitan seda täpsemalt? ",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c6f455a7-642f-4205-abc1-5bbad6f4dd6b,1,user,"Tere! Ma olen plaanimas pakettreisi tellida. Mis asi üldse tähendab ""pakettreis""?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c6f455a7-642f-4205-abc1-5bbad6f4dd6b,2,assistant,"Tere! Pakettreis tähendab, et reisikorraldaja on pannud kokku terve komplekti reisiteenuseid – näiteks lennud, majutus, toitlustus ja ekskursioonid. Sa saad selle paketi ühe lepinguga tellida. See võib tähendada nii ühte lepingut või ka eraldi lepinguid, kui täidad teatud tingimused.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ec92e512-1f64-41de-a922-94c2ea5b42a8,1,user,"Tere! Ma olen plaanimas pakettreisi ja ma ei ole päris kindel, mis see tähendab. Kuidas ma tean, kas ma olen broneerinud pakettreisi?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ec92e512-1f64-41de-a922-94c2ea5b42a8,2,assistant,"Tere! Pakettreis on tegelikult siis, kui reisikorraldaja on kokku pannud terve paketti reisiteenustest - näiteks lend, hotell ja ekskursioonid. Sa võid pidada seda pakettreisiks, kui leping on sõlmitud ühe müügikohaga ja sa oled nõus maksmise kohta enne maksmist. See kehtib ka siis, kui pakutakse ühtset hinda kõigile teenustele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +99a27c7c-fd46-4743-ad31-3b59087417b1,1,user,"Tere! Ma lugesin TTJA lehel midagi isikuandmete töötlemise kohta, aga see oli natuke keeruline. Ühesõnas, mida te TTJA-s isikuandmete kohta teete?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +99a27c7c-fd46-4743-ad31-3b59087417b1,2,assistant,"Tere! TTJA-s kogub töötades teavet inimestest, näiteks kui kirjutate meile või olete menetluse osapool. Me töötleme neid andmeid erinevates infosüsteemides, sh üleeuroopalistes, kus need on kaitstud. Põhiliselt täidame avalike ülesannete täitmiseks ja asjade lahendamiseks vajalikku mahut.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +040524f9-085f-4376-8167-8d4168e06d8f,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) töötleb isikuandmeid. Kuidas see täpselt käib?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +040524f9-085f-4376-8167-8d4168e06d8f,2,assistant,"Tere! Jah, see on õige. TTJA töötleb isikuandmeid, näiteks kui te meile kirjutate või olete mõne menetluse osapoolena. Me töötleme neid erinevates infosüsteemides, sealhulgas ka üleeuroopalistes. Kõigis süsteemides on tagatud isikuandmete kaitse.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +32c32037-31b3-45f2-8626-34976627a72c,1,user,"Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) töötab isikuandmetega. Kuidas see üldiselt käib?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +32c32037-31b3-45f2-8626-34976627a72c,2,assistant,"Tere! See on õige. TTJA töötleb isikuandmeid erinevates infosüsteemides, näiteks kui saame sinult kirja või oled mõne menetluse osaline. Need võivad olla ka tundlikud andmed. Kõik töötlemine toimub kooskõlas isikuandmete kaitse seadusega.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e672fb7b-6d2c-4a66-ab9f-fb7b41950c9d,1,user,"Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Ametil on kaart, mis näitab sideteenuste kättesaadavust. Mis see kaart täpselt on ja milleks see mõeldud?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e672fb7b-6d2c-4a66-ab9f-fb7b41950c9d,2,assistant,"Tere! Jah, meil on sideteenuste kaart. See näitab, kas aadressil on võimalik interneti teenusega liituda. Kaart põhineb sideoperaatorite andmetel nende tavahinnakirjade alusel. Roheline punkt tähendab, et liitumisvõimalus on, punane aga mitte.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +24e90f34-90e9-4e56-b6f0-0261a826bfa9,1,user,"Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Ametil on kaart sideteenuste kohta. Mis see kaart täpselt näitab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +24e90f34-90e9-4e56-b6f0-0261a826bfa9,2,assistant,"Tere! Jah, meil on sideteenuste kaart. See näitab, kus aadressil on võimalus interneti teenusega liituda. Roheline punkt tähendab, et liitumisvõimalus on olemas ja punane, et puudub.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +7b88a8fd-572e-4c13-9f96-6ad48ba7d100,1,user,Tere! Olen kuulnud Tarbijakaitse Ameti sidekaardist. Mis see kaart täpselt kujutab?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +7b88a8fd-572e-4c13-9f96-6ad48ba7d100,2,assistant,"Tere! Sidekaart näitab, kus on võimalik interneti teenusega liituda. See põhineb sideoperaatorite pakutavatel hindakirjades olevatel andmetel. Roheline punkt tähendab, et saadaval on liitumisvõimalus teatud kiirusega, punane punkt aga, et sellist võimalust praegu pole.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +75807246-8a8f-4f27-baf4-819249035a4f,1,user,"Tere! Ma tahan teada, kuhu saan oma vanu elektroonikaseadmeid viia.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +75807246-8a8f-4f27-baf4-819249035a4f,2,assistant,"Tere! Vanu elektroonikaseadmeid saad anda tagastuspunkti. Vanu majapidamismasinaid on Sul võimalik anda ka linna või valla jäätmete kogumispunktidesse või jäätmejaamadesse, patareisid ja akusid aga panna poodides olevatesse spetsiaalsetesse konteineritesse.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +9003b2fa-d57d-4056-bc54-b282e70bd343,1,user,"Tere! Ma olen huvitatud, kuidas jäätmed Eestis käsitletakse. Mis on kõige olulisem asi, mida ma selle kohta teada peaks?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +9003b2fa-d57d-4056-bc54-b282e70bd343,2,assistant,"Tere! Jäätmehoolduse korraldamine on kohaliku omavalitsuse ülesanne. Oluline on teada, et kui elad elanikkudega omavalitsuses, pead sa ise jäätmevedaja valima või viima jäätmed prügilasse/jäätmejaama. Elanikkudega omavalitsused peavad korraldama jäätmeveokonkurssi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a2259d9a-bb2c-46bf-8379-0198f330fd0d,1,user,"Tere! Ma olen huvitatud sellest, mida ma pean tegema, kui mul on probleeme elektriarvete või jäätmeveoga. Kus ma saan abi?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a2259d9a-bb2c-46bf-8379-0198f330fd0d,2,assistant,"Tere! Kui Sul on probleeme elektriarvete või jäätmeveoga, on mitmeid kohti, kust Sa abi leiad. Elektriarvete osas saad pöörduda TTJA tarbijavaidluste komisjoni poole. Jäätmeveo puhul saad esmalt pöörduda kohaliku omavalitsuse poole, kui probleem ei lahene, saad pöörduda TTJAsse. Mis täpsemalt Sind muretseb – elektriarved või jäätmeveo probleem?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e07a4fe4-2206-4ed0-bc62-cfe0ee171dc4,1,user,"Tere! Mul on küsimus. Mis on oluline, kui tellin midagi telefoni teel? Pean ikka sellele kohustusatud olema?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e07a4fe4-2206-4ed0-bc62-cfe0ee171dc4,2,assistant,"Tere! Telefoni teel tellimisel on oluline, et kaupleja oleks sulle kinnitanud lepingu püsival andmekandjal (nt e-kirja) ja sina ise olema kinnitanud oma tahet ka selles vormis. Kui seda ei juhtu, sa ei ole selle lepingu poolt kohustatud. Muidu võib olla keeruline jätta lepingust loobuda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f3f6dc3f-edbd-4eb4-a5c5-ce39447bbfdc,1,user,"Tere! Ma kuulen palju telefoni- ja koduuksemüüki kohta. Mis on kõige tähtsam, mida ma peaks teadma, kui mulle heljutakse või kui kellegi koduuksel kohtan?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f3f6dc3f-edbd-4eb4-a5c5-ce39447bbfdc,2,assistant,"Tere! See on hea küsimus. Oluline on teada, et telefoni- ja koduuksemüük on eriti tähelepanuta nõudmise puhul erinev tavalisest poes ostmisest. Peamine on see, et telefoni teel sõlmitud leping on siduv ainult siis, kui kaupleja on edastanud lepingu tingimused kirjalikult ning Sa oled kinnitanud oma tahet kirjalikus vormis. Koduuksemüügi puhul peab kaupleja andma Sulle taganemise juhendi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +62ca454a-5601-40e8-a7f1-6600cb0c4ecb,1,user,"Tere! Ma olen tootja ja uuenen selle kohta, kuidas tooted ligipääsetavaks muutmine toimib. Saaksite mulle alguses selgeks teha, mis on sellega seotud peamised seadused?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +62ca454a-5601-40e8-a7f1-6600cb0c4ecb,2,assistant,"Tere! Muidugi. Peamiseks seaduseks on Toodete ja teenuste ligipääsetavuse seadus, mis kohaldatakse paljudele toodetele ja teenustele. Lisaks on oluline arvestada määruseid nr 46 ja 47, mis täpsustavad ebaproportsionaalse koormuse hindamist ja teavitamise korra. Üheskoos nendad need reguleerivad ligipääsetavuse nõuete järgimist Eestis.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +32a40f25-970f-49fa-b702-5efb1aac3d2c,1,user,"Tere! Ma olen uus ettevõtja ja tahan teada, milliseid seadusi pean arvestama, kui ma turule viin toote, mis on seotud ligipääsetavusega?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +32a40f25-970f-49fa-b702-5efb1aac3d2c,2,assistant,"Tere! Peamiseks seaduseks on Toodete ja teenuste ligipääsetavuse seadus (LPS). Lisaks pead arvestama määrust nr 45, mis käsitleb ebaproportsionaalse koormuse hindamist, ja määrust nr 46, mis sätestab selle hindamise korra. Samuti on oluline tundida direktiivi I. lisa VII jaos ja harmoneeritud standardeid, millega seadus on seotud.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6ba2a806-4f31-4b6b-83d7-fd9c8e69e137,1,user,Tere! Mul on küsimusi seoses toodete ligipääsetavusega. Kust ma peaksin alustama?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6ba2a806-4f31-4b6b-83d7-fd9c8e69e137,2,assistant,"Tere! Kõigepearem on hea teada, et Eesti seadus toodete ja teenuste ligipääsetavuse kohta tugineb Euroopa Liidu direktiivile. Selle kohta on Eestis koostatud mitmeid määrusi, näiteks määrus nr 46, mis käsitleb ettevõtja ebaproportsionaalse koormuse hindamist. Alustage lugemine eelnimetatud määrusest, kui te olete tootja või teenuse osutaja.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +703183a5-d80a-4754-a8b3-f904cb134017,1,user,"Tere! Ostsin hiljaaegu veebist kaamera Saksamaalt. Mida ma peaksin teadma, kui sellega peaks midagi viga olema?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +703183a5-d80a-4754-a8b3-f904cb134017,2,assistant,"Tere! Saksamaalt ostetud kaamera puhul on oluline teada, et EL-i ja Euroopa Majanduspiirkonna riikides (Saksamaa on ka selles piirkonnas) on tarbijatele sarnane kaitsetase. Sul on õigus esitada kaebus defektide kohta vähemalt kahe aasta jooksul alates ostukuupäevast. Tähtis on, et sul oleks ostutšekk või maksekorralduse väljavõte tõendamaks ostu.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +eef9ca47-5315-4e47-a733-35b7d3165c96,1,user,"Tere! Ostsin hiljaaegu internetist riideid Soomest. Mis juhtub, kui riideid ostes avastan vea? Kas mul on mingid õigused?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +eef9ca47-5315-4e47-a733-35b7d3165c96,2,assistant,"Tere! Küll on. Kuna Soome on Euroopa Liidu riik, siis kehtivad Sinule samad tarbijaõigused, nagu ostes ka Eestis. Sul on õigus esitada kaebus puuduste kohta vähemalt kahe aasta jooksul alates ostukuupäevast. Oluline on, et Sul oleks ostu tõendav dokument, näiteks arve või maksekõrralduse väljavõte.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a149b535-2825-4040-83eb-a51dc0ae1f88,1,user,"Tere! Olen just Soomast uue telefoni ostnud. Mis siis, kui see peagi katki läheb? Kas mul on õigused siin Eestis?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a149b535-2825-4040-83eb-a51dc0ae1f88,2,assistant,"Tere! Soomast ostetud telefoni puhul on hea meel kuulda, et EL ja Euroopa Majanduspiirkonnas (kuhu Soomagi kuulub) on tarbijatele kehtestatud sarnane kaitsetase. Sul on õigus esitada kaebus telefoni defekti kohta vähemalt kahe aasta jooksul alates ostukuupäevast. Oluline on alles hoida ostutšeki või maksekorralduse väljavõte tõendamaks ostu.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +183c1c10-6748-42d6-9d74-c5a7f96c7e5a,1,user,"Tere! Ma lugesin midagi ehitiste ohutuse kohta Tarbijakaitse ja Tehnilise Järelevalve Ameti (TTJA) leheküljel. Kuidas ma saan aru, kas minu maja on ohutu?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +183c1c10-6748-42d6-9d74-c5a7f96c7e5a,2,assistant,"Tere! Ehitise ohutus tähendab, et see peab vastama mitmetele nõuetele. Need tulenevad seadustest, heast tavast või ehitise kasutus- ja hooldusjuhendist. Näiteks, maja peab olema mehaaniliselt vastupidav, tuleohutu, hügieeniline ja kasutajate jaoks ohutu – sealhulgas lihtne evakueerida tulekahju korral.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +73966145-7b0a-4327-9090-e5b912de1a20,1,user,Tere! Ma lugesin midagi Tarbijakaitse Ameti veebilehel ehitiste ohutuse kohta. Mis see tähendab täpselt?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +73966145-7b0a-4327-9090-e5b912de1a20,2,assistant,"Tere! Ehitiste ohutus tähendab seda, et ehitis peab olema vastavuses seadusaktide nõuetele ja kasutatav ohutult. See hõlmab mitmeid aspekte nagu mehaaniline vastupidavus, tuleohutus, hügieen, juurdepääsetavus puudega inimestele ja palju muud. Nõuded tulenevad seadusest, ehitise kasutusjuhendist või heast tavast.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0b286cab-bcc8-4616-ab04-18bb0e810e7c,1,user,Tere! Mida Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tähendab ehitiste ohutuse osas?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0b286cab-bcc8-4616-ab04-18bb0e810e7c,2,assistant,"Tere! TTJA järelvalvega on seotud ehitiste kasutamisele ja korrashoidmisele kehtivad nõuded, mis tulenevad seadusest, heast tavast või ehitise kasutus- ja hooldusjuhendist. Omanik peab tagama, et ehitis vastab kõikidele nõuetele. Meie soov on kaardistada ohtlikud ehitised ja kohustada omanikke neid parandama.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3392531f-2d3f-401a-bac0-12e8573ff764,1,user,"Tere! Ma olen kuulnud, et ehitustoodetega on seoses uusi määruseid. Mis on Tarbijakaitse ja Tehnilise Järelevalve Ameti (TJJA) roll selles osas?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3392531f-2d3f-401a-bac0-12e8573ff764,2,assistant,"Tere! Jah, see on õige. TJJA vastutab ehitustoodete turujärelevalve eest Eestis. Nad veenduvad, et tootjad järgivad vastavaid nõudeid ja et ehitustooted on ohutud ja vastavad standarditele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a4b54daa-3add-48bc-b35c-b3357a7da8e3,1,user,"Tere! Ma kuulsin, et muutub midagi ehitustoodete kohta. Mis on kõige olulisem muudatus, mida Tarbijakaitse ja Tehnilise Järelevalve Amet (TJT) on rõhutanud?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +a4b54daa-3add-48bc-b35c-b3357a7da8e3,2,assistant,"Tere! TJT on rõhutanud, et 2024/3110 määrus asendab 305/2011 määruse. Peamine muudatus on see, et peale toimivuse peab tootja deklareerima ka ehitustoote vastavust ohutus- ja keskkonnanõuetele. See tähendab, et tuleb koostada toimivus- ja vastavusdeklaratsioon selle asemel, mida varem kutsuti lihtsalt toimivusdeklaratsiooniks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c9019316-575d-48c3-a45e-58e8bcbeb0be,1,user,"Tere! Ma olen ostmas uus käevõru, aga ma ei tea, millele peaksin tähelepanu pöörama, et see oleks kvaliteetne ja vastaks õigetele standarditele. Mis on Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) poolt antud juhised selle kohta?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c9019316-575d-48c3-a45e-58e8bcbeb0be,2,assistant,"Tere! See on hea küsimus. TTJA poolt on oluline, et müüdaval käevõrul oleks proovi- ja nimemärgis. Proovimärgis näitab, millist metalli on kasutatud ja selle osa sulamis, näiteks kuld 375 või hõbe 925. Nimemärgiks on märk, mis peab olema riiklikus registris registreeritud. Kontrollimärgistega tooted on ka eelistatud, näiteks lõvi kuju Eestis.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +511d31b5-9e53-4552-b5ff-2420763033fa,1,user,"Tere! Ma ostsin äsja kuldsõrmuka ja ma ei saa aru, mida need märgid sellel tähendavad. Mis need proovi- ja nimemärgid tähendavad?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +511d31b5-9e53-4552-b5ff-2420763033fa,2,assistant,"Tere! Need märgid on väga olulised. Proovimärk näitab, millist metalli on sõrmuse valmistamisel kasutatud ja selle osa sulamis. Näiteks, kui proov on 585, siis see tähendab, et sõrmes on 58,5% puhast kulda tuhande osas. Nimemärk aga näitab tootjat või maaletoimetajat ja see peab olema registreeritud riiklikus registris. Sa leiad selle registreerimise aadressi siin: http://www.proovikoda.ee/register/otsing.php.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +beaf5c6a-b5a8-4657-b6f5-09a39754db78,1,user,"Tere! Ma olen ostmas kuldehit, aga ma ei tea, millele ma peaksin tähelepanu pöörama. Mida peaksin kontrollima?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +beaf5c6a-b5a8-4657-b6f5-09a39754db78,2,assistant,"Tere! Kuldehit ostes on oluline, et sellel oleksid proovi- ja nimemärgid. Proovimärgis näitab, millist metalli on kasutatud ja selle osa sulamis, näiteks 375, 585 või 916. See märk on tuhandeksüsteemis. Nimemärgis on tootja või maaletoja nimi. See peab olema registreeritud riiklikus registris. Kas sa teadsid seda?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ccddc641-7cd2-4589-8191-cd4bdcd77f7f,1,user,"Tere! Ma just hakkasin tarbija õiguste kohta rohkem huvi tundma. Mis on kõige olulisem asi, mida tarbijana teada tuleks?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ccddc641-7cd2-4589-8191-cd4bdcd77f7f,2,assistant,"Tere! Kõige olulisem on see, et Sul on õigus nõuda ja saada kaup või teenus, mis vastab nõuetele ja on ohutu Sinu elule, tervisele ja varale. Lisaks peab kaupleja andma Sulle selle kohta vajalikku teavet, et Sa saaksid teadlikult otsuse teha.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +41987993-c2f8-48bf-b687-a0a8539e925a,1,user,"Tere! Ma olen kuulnud Tarbijakaitse Ametist, aga ei tea täpselt, mis tarbija õigused on. Mis on kõige olulisem asi, mida tarbija peaks teadma?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +41987993-c2f8-48bf-b687-a0a8539e925a,2,assistant,"Tere! Oluline on teada, et sul on õigus nõuda ja saada kaup või teenus, mis vastab nõuetele ja on ohutu sinu elule, tervisele ja varale. Samuti on sul õigus saada infot, et teha teadlik valik. See tähendab, et müüjalt peaks alati küsima vajalikku teavet toote või teenuse kohta.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8c577ab5-a44d-4fd3-9caf-7ce289e1e15c,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitseamet kaitseb tarbija õigusi. Aga mida see täpsemalt tähendab? Millised on mu peamised õigused ostjana?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8c577ab5-a44d-4fd3-9caf-7ce289e1e15c,2,assistant,"Tere! Jah, Tarbijakaitseamet on seal, et kaitsta sind tarbijana. Peamiselt sa saad nõuda kaupa või teenust, mis vastab nõuetele, on ohutu ja mida ei ole keelatud omada või kasutada. Samuti on sul õigus saada vajalikku teavet pakutavate kaupade ja teenuste kohta, et sa saaksid teadlikult valida. Ja kui su õigusi rikutakse, saad nõu ja abi Tarbijakaitseametilt!",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +54fd2207-afb7-4d19-bb26-89fb23ad8eb4,1,user,"Tere! Olen kuulnud, et on võimalik laenu maksepuhkust taotleda. Kuidas see üldiselt toimib?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +54fd2207-afb7-4d19-bb26-89fb23ad8eb4,2,assistant,"Tere! Jah, on õige. Maksepuhkus võimaldab teil laenu tagasimaksmist lühikeseks ajaks peatada, kui teil on raskusi. Üldjuhul antakse see kuni kuueks kuuks, ja sel ajal ei maksa laenu põhiselt, vaid intressi tuleb ikkagi edasi tasuda. Oluline on märkida, et seda tuleks taotleda alles siis, kui sissetuleku vähenemine on probleem ja teised kulud kokku tõmmata ei aita.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +79928fc8-8247-4c79-b0ea-d5b7ebad1582,1,user,"Tere! Sain palju informatsiooni laenude kohta. Kuidas ma saan aru, mida ma peaksin tegema, kui ma hakkasin laenumaksetega hiljaks jääma?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +79928fc8-8247-4c79-b0ea-d5b7ebad1582,2,assistant,"Tere! Kui oled hätta laenumaksetega, on oluline hakata lahendusi otsima enne, kui võlgu jääd. Korrektse maksekäitumisega kliendil on lihtsam laenuandjaga kompromisse leida. Kas sa soovid, et ma rääkiks sellest, milliseid võimalusi pangad üldjuhul pakuvad?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +aeef2154-1ce1-4e4a-a085-31c8cad18dc4,1,user,"Tere! Mul on palju lugemist, aga ma tahan teada, mida tähendab ""tarbija käendusleping""? Mis see üldse on?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +aeef2154-1ce1-4e4a-a085-31c8cad18dc4,2,assistant,"Tere! Tarbija käendusleping on olukord, kus sa kohustud sõbra või sugulase laenu eest tasuma, kui ta ise seda ei suuda. See annab laenuandjale lisakindluse, et laen tagastatakse. Olenemata sellest, kas tegemist on väikelaenu, järelmaksu, tarbimislaenu või eluasemelaenu puhul. Kas soovid, et selgitan täpsemalt sellega seotud nüansse?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3cd61942-da82-4399-ba24-d10997aae7db,1,user,"Tere! Mul on küsimus. Ostsin hiljuti veidrikas eseme ja see ei tööta nii, nagu peaks. Mis on mu õigused kui see on defektne?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3cd61942-da82-4399-ba24-d10997aae7db,2,assistant,"Tere! Oluline on teada, et tarbija õigus on nõuda kauplejalt esmalt tasuta parandamist või asendamist. See, kumb variant on parem, lepitakse teie vahel kokku. Kuid reeglina otsustab müüja ise, kas asendada toode uuega või parandada seda. See kehtib, kui müüjal ei ole võimalik kaupa parandada või asendada, või kui parandamine/asendamine ei õnnestu, või müüja ei kõrvalda defekti mõistlikus ajas, või kui teile tekitatakse põhjendamatuid ebamugavusi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3f7f56af-948b-4fea-bed0-931bdf0f3793,1,user,"Tere! Mul on küsimus defektse toote kohta. Mis on minu õigused, kui kaubal on viga?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3f7f56af-948b-4fea-bed0-931bdf0f3793,2,assistant,"Tere! Kui tootel on viga, on Sul õigus nõuda müüjalt esmalt tasuta parandamist või selle asendamist uue kaubaga. Müüja valib reeglina ise, kas parandada või asendada, kuid saavad kokku leppida. On ka olukorda, kus müüjal ei ole võimalik kaupa parandada või asendada, parandamine ebaõnnestub, müüja ei kõrvalda puudust mõistliku aja jooksul või Sulle tekitatakse põhjendamatuid ebamugavusi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +93796d4d-7afa-4c2d-a9da-4ad554d41a75,1,user,"Tere! Mul on küsimus tarbijakaitse kohta. Mis ma peaksin tegema, kui ostsin toote ja see on defektne?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +93796d4d-7afa-4c2d-a9da-4ad554d41a75,2,assistant,"Tere! Kui ostsite defektse toote, on Teil seadme poolest õigus nõuda müüjalt esmalt tasuta parandamist või selle asendamist uue tooga. Müüja otsustab reeglina ise, kas ta toote parandab või asendab. Selliseid õigusi on Teil seadusega tagatud, isegi kui müüja ei pakka garantiid.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d624f894-d095-488a-a573-66480478cb87,1,user,Tere! Ma lugesin sellest artikkel kohta tarbijavaidluste komisjoni. Mis on selle komisjoni peamine ülesanne?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d624f894-d095-488a-a573-66480478cb87,2,assistant,"Tere! Tarbijavaidluste komisjoni peamine ülesanne on lahendada vaidlusi tarbija ja ettevõtte vahel, mis puudutavad tarbekaupade või -teenuste ostu-müüki. Nad aitavad leida lahendusi, kui tarbija on rahul teenusega või toodetega.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c0dbb5fa-a67d-49fd-a4af-844eede1618d,1,user,"Tere! Ma lugesin mingit teksti tarbija õigustest, aga see oli üsna pikk. Mis on üldjuhul esimene koht, kuhu tarbija peaks pöörduma, kui tal on probleem mingi ettevõttega?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c0dbb5fa-a67d-49fd-a4af-844eede1618d,2,assistant,"Tere! Kui teil on probleem ettevõttega, on tarbijaõiguste puhul hea koht alustuseks tarbijavaidluste komisjon. Nad aitavad lahendada vaidlusi tarbijate ja ettevõtete vahel.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +abd74432-0987-429a-a9fd-f3009af2e431,1,user,Mis on tarbijavaidluste komisjoni ülesanne?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +abd74432-0987-429a-a9fd-f3009af2e431,2,assistant,"Tarbijavaidluste komisjoni ülesanne on lahendada vaidlusi tarbijate ja ettevõtjate vahel, mis on tekkinud tarbimise seadusest või muudest seadusest tulenevate kohustuste rikkumisest. See on viis tarbijale vaidluse lahendamiseks kohtuvälise menetluse kaudu.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +16219012-1d7f-42c7-bcbd-750b15bf8c5d,1,user,Tere! Olen kuulnud midagi ebaausate müügivõtete kohta. Mis see tähendab ja miks on see probleemne?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +16219012-1d7f-42c7-bcbd-750b15bf8c5d,2,assistant,"Tere! Ebaausad müügivõtted on sellised müügitaktikad, mis püüavad sind tarbijana eksitada või survestavad sind ostma. Need on Euroopa Liidus keelatud, sest nad kahjustavad tarbija õigusi ja teevad tehingu keeruliseks ja ebavõrdseks. Näiteks võib müüja anda valeväiteid toote kohta või üritada sind piirata valikuvabaduses.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3fcbb38f-6f93-49ce-94d9-65a4c082aee4,1,user,"Tere! Olen kuulnud, et on olemas ebaausad müügivõtted. Mis see tähendab tarbija jaoks?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3fcbb38f-6f93-49ce-94d9-65a4c082aee4,2,assistant,"Tere! Ebaausad müügivõtted on sellised müügitehnikad, mis püüavad sind tarbijaid eksitada või survestada ostma. Seadus keelab selliseid võtteid Euroopa Liidus. Need võtted võivad olla kas eksitavad või agressiivsed – mõlemad on probleemsed.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e67deeb2-80c9-42cd-83ba-fec587db1d84,1,user,"Tere! Olen kuulnud, et on olemas ebaausad kauplemisvõtted. Mis need täpselt on ja miks need on keelatud?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e67deeb2-80c9-42cd-83ba-fec587db1d84,2,assistant,"Tere! Ebaausad kauplemisvõtted on need, mis sind tarbijana eksitavad või on su suhtes agressiivsed. Need on Euroopa Liidus keelustatud. Eksitav on, kui müüja annab sulle valeinfot kauba või teenuse kohta, või esitab õiget infot viisil, mis võib sind eksitada. Agressiivne on, kui müüja piirab sinu valikuvabadust, ahistades või mõjutades sind liigselt ostma.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d880a28c-8e68-41b0-87bb-691fe6ce754e,1,user,"Tere! Ma olen lapsevanem ja kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet on pannud üles raudteeohutuse õppematerjale. Mis on selle projekti eesmärk?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +d880a28c-8e68-41b0-87bb-691fe6ce754e,2,assistant,"Tere! Jah, see on õige. Projekti „Agamina, väikelaste liiklusohutus“ eesmärgiks on toetada väikelaste vanemaid ja õpetajaid, kes tegelevad laste ohutusega liikluses ning viisaka liikleja kujundamisel. See sobib ka sinule, kui sa soovid koos lapsega raudtee ületamise ohutuse teema arutada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +bf3398a1-6e54-44b3-b090-a6bb80f82f57,1,user,"Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järevalve Amet on teinud materjale raudteeohutuse teema kohta. Mis see täpsemalt on?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +bf3398a1-6e54-44b3-b090-a6bb80f82f57,2,assistant,"Tere! Jah, meil on mitmeid materjale, mis on suunatud nii lapsevanematele, õpetajatele kui ka lastele endadele. Põhiliseks eesmärgiks on tekitada teadlikkust raudteeületamise ohudest ja õpetada, kuidas ohutult käituda raudtee läheduses. Nende materjalide hulka kuuluvad õppematerjalid, õppevideod ja töövihikud.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b76190b7-9a23-4801-9dd4-30a866cddec3,1,user,"Tere! Ma kuulsin, et Rail Balticu ehitamine on keeruline projekt. Mis kõige suuremad väljakutsed sellega seoses on olnud?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b76190b7-9a23-4801-9dd4-30a866cddec3,2,assistant,"Tere! Jah, Rail Balticu ehitamine on tegelikult üsna keeruline. Üks suurim väljakutse on see, et olemasoleva raudtee ümberehitus, näiteks kurvide sirgemaks tegemine või teise rööpapaari lisamine, on tehtud analüüside järgi kallim kui uue trassi ehitamine. Lisaks on oluline arvestada, et Tartu kaudu minev trass ei võimalda kasutada Euroopa ühendamise rahastu toetust samas mahus.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2b062e2d-14ca-450d-a03c-f1524ad2e928,1,user,"Tere! Ma kuulsin palju Rail Balticu kohta. Kas sa saaksid mulle öelda, miks praegune trass kõige kallim variant on?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2b062e2d-14ca-450d-a03c-f1524ad2e928,2,assistant,"Tere! Jah, muidugi. Analüüs näitas, et olemasoleva trassi ümberehitus, nagu kurvide sirgemaks tegemine ja teise rööpapaari lisamine, oleks kallim kui uus trass. Lisaks ei oleks võimalik Euroopa ühendamise rahastu toetust täiendavalt kasutada Tartu ja Valga trassi puhul samas mahus kui uue trassi puhul.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f1abb262-b7a0-4d94-ba8d-124661ce91a4,1,user,"Tere! Mul on Rail Balticu kohta paar küsimust. Näiteks, miks on uus raudtee trass läbi Harju-, Rapla- ja Pärnumaa valiti, kui läbi Tartu oleks ka võimalik?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f1abb262-b7a0-4d94-ba8d-124661ce91a4,2,assistant,"Tere! Hea, et küsid. Ekspertide analüüs näitas, et Tartu kaudu läheks raudtee rajamine märkimisväärselt kallimaks ja mõjuks rohkem nii loodusele kui inimeste elukorraldust. Seega valiti loodust ja inimkeskkonda säästvam ning lühem trass läbi Harju-, Rapla- ja Pärnumaa. Kas see selgitab asja?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +911cce7b-10a8-4474-81a6-42040a1d3130,1,user,"Tere! Ma olen kuulnud palju CE-märgist räägitakse, aga ei tea täpselt, mis see tähendab. Mis on CE-märgise üldpõhimõtted?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +911cce7b-10a8-4474-81a6-42040a1d3130,2,assistant,"Tere! CE-märgise põhimõtted on üsna lihtsad. Põhimõtteliselt kinnitab CE-märgis, et tootja või volitatud esindaja garanteerib, et toode vastab ELi ühtlustatud nõuetele. See tähendab, et toode on ohutu ja vastab kehtivatele standarditele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +7a7bf496-29bb-4d65-86b9-b913e1891fbe,1,user,Tere! Ma kuulsin midagi CE-märgise kohta. Mis see tähendab ja kes sellele tootele kinnitada tohib?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +7a7bf496-29bb-4d65-86b9-b913e1891fbe,2,assistant,"Tere! CE-märgis näitab, et toode vastab EL-i ühtlustatud nõuetele. Selle kinnitada tohib ainult tootja või tema volitatud esindaja. See on nagu kinnitus, et toode on ohutu ja vastab nõuetele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4cea6e73-f43b-4fb2-b9b7-f10b863492d8,1,user,"Tere! Ma olen kuulnud CE-märgist, aga ma ei tea päris täpselt, mis see tähendab. Saaksin natuke selgitust?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4cea6e73-f43b-4fb2-b9b7-f10b863492d8,2,assistant,"Tere! Muidugi. CE-märgist kinnitab tootja või tema volitatud esindaja tootele. See tähendab, et tootja kinnitab, et toode vastab ELi ühtlustatud nõuetele. Mis konkreetsemalt sind CE-märgise kohta huvitab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +603e8e01-6b29-49f0-96c8-c1375367ef26,1,user,"Tere! Ma kuulsin, et Tarbijavaidluste Komisjon (KKK) on olemas. Mis see komisjon tegelikult on ja mis on selle eesmärk?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +603e8e01-6b29-49f0-96c8-c1375367ef26,2,assistant,"Tere! Tarbijavaidluste komisjon on Tarbijakaitse ja Tehnilise Järelevalve Ameti juures tegutsev organ, mis lahendab tarbijavaidlusi kauplejatega. See on koht, kus sa saad pöörduda, kui sul on probleem mingi ostuga ja sa ei saa kauplejaga ise kokkuleppele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3778c58f-bf3b-46bf-acad-47d008822940,1,user,"Tere! Ma kuulsin, et Tarbijavaidluste komisjon KKK aitab probleemidega kauplustega. Kas komisjonis kaebuse esitamine on tasuta?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3778c58f-bf3b-46bf-acad-47d008822940,2,assistant,"Tere! Jah, küllap. Kaebuse esitamine Tarbijavaidluste komisjoni menetlusse võtmine on täiesti tasuta. Samuti on avalduse läbivaatamine tasuta.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b49e83db-41f8-4030-84e6-e9ee36185073,1,user,"Tere! Ma kuulsin, et Tarbijavaidluste Komisjon (KKK) aitab probleemidega, mis kauplejatega tekivad. Kas komisjoni poole pöördumine on tasuta?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b49e83db-41f8-4030-84e6-e9ee36185073,2,assistant,"Tere! Jah, on õige. Tarbijavaidluste komisjoni poole pöördumine, avalduse läbivaatamine ja tarbijavaidluse menetlus on täiesti tasuta. Teil ei pea selle eest midagi maksma.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +689e09fa-0b66-4955-9fdd-3cf43f879b72,1,user,"Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet tegeleb koduohutusega. Mis see täpselt tähendab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +689e09fa-0b66-4955-9fdd-3cf43f879b72,2,assistant,"Tere! Jah, me tegeleme koduohutusega. See tähendab, et me jälgime, et sinu kodu oleks ohutu kasutada – ehitise konstruktsioonid, tehnosüsteemid, kõik see peab olema korras. Ehitise omanik on vastutav selle ohutuse eest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6d861c34-a490-451a-98c4-970a94a94a74,1,user,"Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TJAA) tegelda koduohutusega. Mis see täpsemalt tähendab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6d861c34-a490-451a-98c4-970a94a94a74,2,assistant,"Tere! Nõmme, TJAA jälgib, et ehitis oleks ohutu kogu oma eluea jooksul. Põhimõte on, et omanik vastutab ehitise ohutuse eest. Korterelamus on oluline vaheline erinevus reaalosade ja kaasomandi vahel. Mis on sinu jaoks kõige olulisem selles?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +7b8ba0f8-fcf2-46e3-ac4f-530a84a1e059,1,user,"Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet teeb koduohutusega seoses tööd. Mis see tähendab täpselt?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +7b8ba0f8-fcf2-46e3-ac4f-530a84a1e059,2,assistant,"Tere! Jah, meil on täpselt nii. Peamiselt tähendab see, et ehitise omanik vastutab selle ohutuse eest. Korterelamutes on oluline eristus: reaalosaks on korter ise ja kaasomandiks on ühiskasutuses olevad konstruktsioonid, näiteks küttesüsteem või trepikoda. Sellega seoses on teatud asjades piirangud. Näiteks, ümberehitamine kaasomandis ei ole lubatud omavoolul.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +228d9d10-57b2-4eb6-bf96-a112a76379b7,1,user,"Tere! Mul on küsimus rõivaste ja jalatiste kohta. Mis on oluline, mida ma peaksin ostes silmas pidama märgistuse osas?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +228d9d10-57b2-4eb6-bf96-a112a76379b7,2,assistant,"Tere! Rõivaste ja jalatiste märgistuselt peaksid kindlasti leidma toote nimetuse, suurustähistuse, materjalikoostise (v.a karusnahast ja nahast rõivastel) ning hooldustähistuse. Samuti on oluline, et materjalide kohta oleks täpne info, eriti kui tegu on naturaalse nahaga või karusnahaga.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +989cbbdb-8aef-410c-8f35-374015e68fa2,1,user,"Tere! Mul on küsimusi rõivaste ja jalatste märgistamise kohta. Mis on kõige olulisem, mida ma peaks ostes vaatama?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +989cbbdb-8aef-410c-8f35-374015e68fa2,2,assistant,"Tere! Oluline on vaadata, kas rõival või jalatseid on näidatud toote nimetus, suurus, materjalikoostis (eriti oluline, kui ei ole nahast või karusnahast) ja hooldusjuhised ehk hooldustähistus. Need on põhilised asjad, mis annavad sulle olulist teavet toote kohta.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e1ff6574-dd1b-4342-828c-328774115514,1,user,"Tere! Ma ostsin äsja uued jalanõud ja ma ei saa päris alguses aru, mida kõik need sildid tähendavad. Mida ma peaksin seal märkima?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e1ff6574-dd1b-4342-828c-328774115514,2,assistant,"Tere! Jalanõudel peaksid leidma märkused pealsete, voodri ja talla valmistamisel kasutatud materjalid. Samuti on oluline, et jalatsitel oleks märgistatud nende otstarve – kas need sobivad tänaval kandmiseks või siseruumides kasutamiseks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2edcdae6-a47b-46d1-8bdd-3e4f5c0bfbf0,1,user,"Tere! Mulle on veidi segadus reklaamide osas. Kas saaksid mulle selgitada, mida Tarbijakaitse ja Tehnilise Järelevalve Amet reklaamide kohta ütleb?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2edcdae6-a47b-46d1-8bdd-3e4f5c0bfbf0,2,assistant,"Tere! Muidugi. Tarbijakaitse ja Tehnilise Järelevalve Ameti (TTJA) kohaselt peab reklaam tavaliselt kohe nähtavale hüppama, et teada, et see on reklaam. See peab selgelt eristuma teistest teadetest. Samuti peaks reklaamist selgeks saama, kes reklaami tellija on – näiteks nime, kaubamärgi või domeeninihi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e6b1db6c-9f38-42da-8fbb-cfa46332bb37,1,user,"Tere! Ma nägin ühte reklaami ja mõtlesin, kas Tarbijakaitse Amet kontrollib reklaamide nõuetekohasus?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e6b1db6c-9f38-42da-8fbb-cfa46332bb37,2,assistant,"Jah, küllap! Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib, et reklaam ei rikkuks seadust. Põhilist nõuet on see, et reklaam peab tavaliselt tähelepanu korral olema selgelt eristatav muust teabest – see peab kohe näha, et tegemist on reklaamiga.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4bb46d99-119d-45cb-b38a-2a392aeae91a,1,user,Tere! Mul on küsimus reklaamide kohta. Kas Tarbijakaitse Ameti poolt on reklaamidele mingid reeglid kehtestatud?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4bb46d99-119d-45cb-b38a-2a392aeae91a,2,assistant,"Jah, on küll! Reklaamidele on kehtestatud mitmeid reegleid. Näiteks, reklaam peab tavalise tähelepanu korral selgelt eristuma muust teabest – see peaks kohe aru saada, et tegemist on reklaamiga. Samuti peab selgeks saama, kes reklaami tellija on – näiteks firma nimi või domeeninimi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2ebdb16d-8eb0-4cd3-ab0c-e8ba27267c71,1,user,"Tere! Mul on küsimus raudtee ohutuse kohta. Mida peaksin silmas pidama, kui lähen raudteeülesõidu juurde?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +2ebdb16d-8eb0-4cd3-ab0c-e8ba27267c71,2,assistant,"Tere! Oluline on peata kiirust, et vajaduse korral saaksid peatuda. Kontrolli liiklusmärke ja valgusfoore ning vaata ja kuula, et veenduda, et liikumine on ohutu. Enne üleminekut veendu alati, et lähenev raudteesõidukile anda teed.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +96935453-c686-4f7e-b2fe-e47db9df2472,1,user,"Tere! Ma kuulsin, et Tarbijakaitse Amet on teinud meeleseisundi raudteeohutuse kohta. Mis see tähendab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +96935453-c686-4f7e-b2fe-e47db9df2472,2,assistant,"Tere! Jah, meil ongi meeleseisund, mis tuletab meile kõik olulised punktid raudtee ohutuse kohta. Põhimõtteliselt tuletab see meid kõiki hoiatama, et raudteed on ohtlikud piirkonnad, kui eiratakse liikluseeskirju.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8c754c60-09bd-4c4d-b5e2-ff9d3e78ea8b,1,user,"Tere! Ma olen kuulnud, et raudtee ületamisel on oluline olla eriti tähelepanelik. Mis on kõige olulisem asi, mida ma peaksin meeles pidama, kui ma ületan raudteed autoga?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +8c754c60-09bd-4c4d-b5e2-ff9d3e78ea8b,2,assistant,"Tere! See on väga hea küsimus. Kõigepealt veendu, et sa ei lähene raudteeülesõidukohale, kui raudteesõiduk juba läheneb. See tähendab, et vaata ja kuula hoolikalt. Ja alati anna lähenevale raudteesõidukile teed. Ära ületa foori keelava tulega või suletud tõkkepuu korral.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c471dfc4-b03e-426a-bf13-9a5f8ce0c9ad,1,user,Tere! Mis on seikluspark ja milleks see sobilik on?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c471dfc4-b03e-426a-bf13-9a5f8ce0c9ad,2,assistant,"Seikluspark on koht aktiivseks ja sportlikuks puhkuseks, kus igaüks saab oma võimeid proovile panna, sõltumata varasemast kogemusest. See sobib inimestele, kes otsivad midagi põnevat ja väljakutvaid tegevusi!",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +31368ca4-9761-4016-9310-01dfec0a1a8d,1,user,Tere! Ma kaalun seiklusparki minekut. Mida ma peaksin teadma?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +31368ca4-9761-4016-9310-01dfec0a1a8d,2,assistant,"Tere! Seikluspark on tore valik! Enne kui minna, on oluline tutvuda reeglite ja piirangutega, et kogemus positiivne oleks. Kas Sa soovid, et ma rääkiks reeglite ja piirangute kohta või millest muust Sa huvitatud oled?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c0eb4ca0-5b8c-4777-9df0-a5f926079504,1,user,"Tere! Olen kuulnud seiklusparkidest, aga ei tea väga palju. Mis on seiklusparki mineku reeglid, mida peaksin teadma?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c0eb4ca0-5b8c-4777-9df0-a5f926079504,2,assistant,"Tere! Seikluspark on tore koht aktiivseks puhkuseks. Enne rajale minekut on oluline tutvuda reeglite ja piirangutega. Näiteks, on oluline hinnata oma võimeid ja tervislikku seisundit, eriti kui kavatsete läbida raskemaid radu. Kas Sa oled huvitatud, et ma rääkiks täpsemalt raskusastmete kohta?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +fb22cd8e-efe8-4928-b60a-aa4920fd394e,1,user,"Tere! Ma kuulsin, et Eestis on muutusi tele- ja raadioringhäälingus käsil. Saaksid sa selle kohta midagi rääkida?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +fb22cd8e-efe8-4928-b60a-aa4920fd394e,2,assistant,"Tere! Jah, muutusi on. Alustame ehk raadioraadioga: kui sa tahad Eestis raadioraadioraadiot teha, pead esmalt taotlema meediateenuse tegevusluba TTJA-st. See on esmane samm. Sa leidmaks rohkem infot, vaata https://ttja.ee/ariklient/ametist/load-ja-riigiloivud/meediateenuste-tegevusluba. Tahad midagi raadioraadioga seoses veel teada?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ac6566c5-6061-47e8-830e-f3a113961547,1,user,"Tere! Ma olen kuulnud, et Eestis on tegemisel muutusi tele- ja raadioringhäälingu osas. Mis on kõige olulisem asi, mida tarbijana tulema peab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ac6566c5-6061-47e8-830e-f3a113961547,2,assistant,"Tere! Üks olulisem muutus on üleminek täielikult kõrgresolutsiooniga (HD) pildikvaliteedile DVB-T2 standardil. See tähendab, et kui teil on vana teler, mis ei toeta DVB-T2-d, peate ehk hankima uue või lisades digiboksi, et HD kvaliteediga programme näha.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ceddac9c-12e2-47ac-b275-efc88d21ab97,1,user,"Tere! Ma olen kuulnud, et raadio ja tele ringhäälingu jaoks on vaja luba. Kust ma alustan?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ceddac9c-12e2-47ac-b275-efc88d21ab97,2,assistant,"Tere! Jah, see on õige. Raadioringhäälingu jaoks pead tavaliselt kõigepealt taotlema raadioteenuse tegevusluba. Saad selle kohta rohkem infot selle lingi kaudu: https://ttja.ee/ariklient/ametist/load-ja-riigiloivud/meediateenuste-tegevusluba",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +31f1a5de-da5e-40f9-8179-a7913174ff69,1,user,Tere! Ma olen Tarbijakaitse ja Tehnilise Järelevalve Ameti uus praktikant. Räägime natuke raadiosageduste kasutamisest. Mis on EFIS ja milleks seda kasutada saab?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +31f1a5de-da5e-40f9-8179-a7913174ff69,2,assistant,"Tere! EFIS on Frequency Information System – Euroopa Raadiosageduste Informatsiooni Süsteem. See andmebaas võimaldab näha, kuidas konkreetne riik sagedusala (9kHz kuni 275 GHz) kasutab ja võrrelda seda teiste Euroopa riikidega. Samuti leiad sealt infot erinevates riikides kehtestatud nõuetele raadioseadmete kasutamisele ja sageduste jaotustele elektroonilise side teenuste osutamiseks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4f78e0d4-e8d1-448d-809e-7cbec4fcd49f,1,user,"Tere! Ma olen Tarbijakaitse ja Tehnilise Järelevalve Ameti uus töötaja. Üks minu ülesandete kohta on raadiosageduste kasutamine. Kust ma peaksin alustama, et aru saada, kuidas seda Eestis reguleeritakse?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +4f78e0d4-e8d1-448d-809e-7cbec4fcd49f,2,assistant,"Tere! Suurepärane! Alustada saaks Eesti raadiosagedusplaanist. See on meil avaldatud ja saadaval Riigiteataja veebilehel: https://www.riigiteataja.ee/akt/125012019006. See dokument annab kõige olulisema ülevaate sellest, kuidas Eestis raadiosagedusi kasutatakse ja mis on sellega seotud reeglid.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3ef03aaa-ef2b-4277-befd-f62737cd8bab,1,user,Tere! Ma olen üsna uus Tarbijakaitse ja Tehnilise Järelevalve Ameti tööle ja ma kuulen palju raadiosageduste kasutamisest. Mis see tähendab ja miks on see ametile oluline?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3ef03aaa-ef2b-4277-befd-f62737cd8bab,2,assistant,"Tere! See on hea küsimus. Raadiosageduste kasutamine tähendab lihtsalt seda, kuidas erinevad seadmed – näiteks raadiod, mobiiltelefonid ja WiFi-ruuterid – kasutavad õhuvõrku signaali edastamiseks. Tarbijakaitse ja Tehnilise Järelevalve Amet (TJJA) on sellega seotud, et tagada, et need seadmed ei häiri teineteist ja et kasutatakse sagedusi vastavalt kehtivatele reeglitele ja planeerimisele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +13a2731a-b513-480c-8fb2-9914c1756de3,1,user,Tere! Ma olen huvitatud amatöörraadiosideks tööloa saamisest. Kuidas see üldiselt toimib? Milliseid liike neid tööloasid on?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +13a2731a-b513-480c-8fb2-9914c1756de3,2,assistant,"Tere! Amatöörraadiojaama tööloa saamiseks peate esitama taotluse Tarbijakaitse ja Tehnilise Järelevalve Ametile (TTJA). Töölube on mitu liiki. On füüsilisele isikule kuuluv raadiojaam, mis on mõeldud ainuisikuliselt kasutamiseks, juriidilisele isikule või ERAÜ-le/raadioklubile/seltsile kuuluv ühiskasutusega jaam, automaatselt töötav jaam kindlas kohas ja viimaseks, automaatselt töötav jaam, mida kasutatakse antennide häälestamiseks või raadioorienteerumiseks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +615958c2-b073-4e20-9bf7-0d4dedb7b97a,1,user,"Tere! Ma olen huvitatud amatöörraadiosidega alustamisest. Kust ma peaksin teadma, millist raadiojaama tööloa liiki mulle vaja oleks?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +615958c2-b073-4e20-9bf7-0d4dedb7b97a,2,assistant,"Tere! Selleks, et teada, millist tööloa liiki sulle vaja on, oleneb see sellest, kas sa kasutad raadiojaama isiklikult või ühiskasutusena. Füüsilisele isikule sobiks isiklik raadiojaam. Kui saama kasutatakse ühiselt, näiteks raadioklubis, siis sobib ühiskasutusega ARJ.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3c122be6-a0c1-497d-9413-a62b546892b0,1,user,Tere! Ma olen huvitatud amatöörraadiosidest. Kuidas ma saan loa raadiojaama töötamiseks?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3c122be6-a0c1-497d-9413-a62b546892b0,2,assistant,"Tere! Selleks peab esitama taotluse Tarbijakaitse ja Tehnilise Järelevalve Ametile (TTJA). Esiteks, on oluline teada, millist tüüpi raadiojaam teil on - kas see on isiklikuks kasutuseks, ühiskasutusraadiojaam, automaatne majakas või ehk raadioorienteerumise majakakompleks?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +aba46c3b-a12e-4d8a-a231-e5c2cf8fa2a3,1,user,"Tere! Ma pole päris kindel, mida ehitamine seadustiku mõistes tähendab. Saaksid seda mulle selgeks teha?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +aba46c3b-a12e-4d8a-a231-e5c2cf8fa2a3,2,assistant,"Tere! Ehitamine seadustiku mõistes hõlmab mitmeid tegevusi: ehitise püstitamist, rajamist, paigaldamist, lammutamist ja muid tegevusi, mille tulemusena ehitis tekib või muutuvad selle füüsikalised omadused. Ehitusloa või ehitusteatis on teatud juhtudel nõutav.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6ddeda50-8a28-4944-ae4d-9a2e1efa18c7,1,user,"Tere! Ma olen kuulnud, et kui lend hiljub, on reisijatel teatud õigused. Kuidas ma saan teada, mida need õigused on?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6ddeda50-8a28-4944-ae4d-9a2e1efa18c7,2,assistant,"Tere! See on õige. Euroopa Liidu lennureisijate õigused on määratletud kindlas määruses. Üks viis on tutvuda Euroopa Liidu tarbija nõustamiskeskuse veebilehega, kust leiad infot: [https://consumer.ee/](https://consumer.ee/). Samuti on saadaval Euroopa Liidu veebileht, kus on selgelt kirjeldatud reisijaõigused: [https://europa.eu/youreurope/citizens/travel/passenger-rights/air/index_et.htm](https://europa.eu/youreurope/citizens/travel/passenger-rights/air/index_et.htm). Mis täpselt sind kõige rohkem paksub?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0628358f-4ea0-4d86-b5c9-38338453a6a0,1,user,"Tere! Mul on küsimus lennureisijate õiguste kohta. Mis juhtub, kui mu lend hiljub?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0628358f-4ea0-4d86-b5c9-38338453a6a0,2,assistant,"Tere! Kui sinu lend hiljub, on sul õigus hüvitisele, aga see sõltub hilinemise põhjustust. Kui lend hiljub kolm tundi või rohkem, ja hilinemine on vedaja süül, saad hüvitist. Kui aga hilinemine on erakorraliste asjaolude tõttu (mis on vedaja kontrolli alt väljas), hüvitist ei maksta. Täpsemalt on hüvitise summa olenev lennu pikkusest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c74ff87c-64b6-4081-974b-0a7a1ff11087,1,user,"Tere! Mul on lend ees ja tahan teada, mida ma peaksin tegema, kui lend hiljub. Mis on mu õigused?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c74ff87c-64b6-4081-974b-0a7a1ff11087,2,assistant,"Tere! Kui lend hiljub, on sul mitmeid õigusi. Need sõltuvad aga sellest, kas lend väljus Eestist või saabus siia kolmandast riigist. Mis riigist su lend väljub?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e4960418-ee4c-4658-a61d-baf1cfa97205,1,user,"Tere! Mul on küsimus laenude kohta. Ma nägin, et TTJA leheküljel on kirjutatud vastutustundliku laenamise põhimõtted. Mis see tähendab ja kuidas see mind puudutab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +e4960418-ee4c-4658-a61d-baf1cfa97205,2,assistant,"Tere! Vastutustundliku laenamise põhimõtted tähendavad, et nii laenuandjad kui ka tarbijad peavad teatud reeglitele järgima. Laenuandjad peavad hindama sinu maksevõimet ja pakutama sulle sobivat lahendust, samas pead sina olema aus oma väljaminekute kohta. Põhimõte on, et laen ei tohiks olla sulle üle jõu.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0540f0bb-fe90-4839-b732-51917a261265,1,user,"Tere! Ma tahaksin teada, mida ma peaksin silmas pidama, kui ma võtan laenu. Mis on vastutustundliku laenamise põhimõtted?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0540f0bb-fe90-4839-b732-51917a261265,2,assistant,"Tere! Vastutustundliku laenamise põhimõtted tähendavad, et nii laenuandjad kui ka tarbijad peavad olema ettevaatlikud. Enne laenu, järelmaksu või liisingu võtmist, võta pakkumisi paarilt-kolmelt eri firmalt ja küsi tingimuste võrdlemiseks koos pakkumisega teabelehte. See leht sisaldab kõiki olulisi laenutingimusi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6a54c1be-2286-491d-b8a7-cd251a3f472d,1,user,"Tere! Ma olen kuulnud, et enne laenu või järelmaksu võtmist on oluline midagi kontrollida. Mis on kõige tähtsam, mida ma peaksin teadma?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6a54c1be-2286-491d-b8a7-cd251a3f472d,2,assistant,"Tere! See on väga hea küsimus. Enne kui laenu, järelmaksu või liisingu võtate, peaksite kindlasti erinevate krediidiandjate pakkumisi võrdlema ja küsima teabelehte. Teabelehel on kirjas kõik olulised laenutingimused. Samuti võite lepinguprojekti koopia küsida, et see enne allkirjastamist põhjalikult läbi lugeda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ccaf6e47-76bc-4334-89ce-ee2215b8eeff,1,user,"Tere! Mul on küsimus Tarbijakaitse ja Tehnilise Järelevalve Ameti komisjoni töös. Mis juhtub, kui mul on probleem kauplejaga ja pöördun nende poole?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +ccaf6e47-76bc-4334-89ce-ee2215b8eeff,2,assistant,"Tere! Üldjuhul annab Tarbijakaitse Ameti komisjon kauplejale aega, et ta saaks oma seisukoha kujundada ja vastata sinu väidetele. Neil on võimalik vaadata läbi kõik olulised asjaolud, et saaksite koos lahendust leida.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6789de4a-c4af-42ec-9b25-c5d1d35ca0ff,1,user,"Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Ametil on komisjonid, mis lahendavad vaidlusi kauplejatega. Kuidas see üldiselt töötab? Mis juhtub, kui mul on probleem ostuga?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6789de4a-c4af-42ec-9b25-c5d1d35ca0ff,2,assistant,"Tere! Jah, meil on tarbijavaidlustekomisjon. Üldjuhul, kui teil on probleem ostuga, esitate avalduse. Amet annab kauplejale aja, et ta saaks oma seisukohti kujundada ja vastata väidetele. See annab võimaluse leida lahendus enne komisjoni istungit.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +82ab8993-8049-4faf-8932-dd23a6753b83,1,user,"Tere! Ma lugesin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb ka kauplejate komisjoni töoga. Kuidas see üldiselt toimib, kui mul on mingi probleem kauplusega?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +82ab8993-8049-4faf-8932-dd23a6753b83,2,assistant,"Tere! Üldjuhul, kui sul on probleem, esitad avalduse TTJAs. Komisjon annab kauplejale aega oma seisukohti selgitada ja vastata sinu väidetele. See annab neile võimaluse probleemi lahendada enne, kui asi jõuab komisjoni istungini.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f7f321ae-963e-4b8b-8076-d75bc9baa9b1,1,user,Tere! Mida tähendab kemikaal Tarbijakaitse ja Tehnilise Järelevalve Ameti definitsiooni järgi?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f7f321ae-963e-4b8b-8076-d75bc9baa9b1,2,assistant,"Tere! Tarbijakaitse ja Tehnilise Järelevalve Ameti definitsiooni järgi on kemikaal mistahes aine või segu, mis sisaldab või tekitab üht või enamat toimeainet ja mille eesmärk on kahjulike organismide hävitamine, tõrjumine, kahjutuks muutmine või võitlemine nendega muul viisil kui puhtfüüsikaliste toimingutega. See võib olla ka näiteks puhastamiseks ettenähtud pindaktiivseid aineid sisaldav aine.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c30a4660-66fe-4056-b170-b1b01e5babea,1,user,"Tere! Mida täpsemalt tähendab see, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TJAA) reguleerib kemikaale?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +c30a4660-66fe-4056-b170-b1b01e5babea,2,assistant,"Tere! TJAA reguleerib kemikaale selleks, et kaitsta inimese elu ja tervist, asja ja keskkonda ning tagada kaupade vaba liikumine. Kemikaaliseadus seab piiranguid kemikaali käitlemisele ja sellega seotud majandustegevusele. See puudutab nii tootmist, importi kui ka müüki.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +1c8d2e01-dd48-4371-b370-33f98ce56cf3,1,user,Tere! Ma kuulsin midagi digiligipääsetavuse kohta Tarbijakaitse ja Tehnilise Järevalve Ametiga. Mis see täpselt tähendab?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +1c8d2e01-dd48-4371-b370-33f98ce56cf3,2,assistant,"Tere! Digiligipääsetavus tähendab seda, et avaliku sektori digiteenused ja -informatsioon peavad olema kättesaadavad ja kasutatavad kõikidele inimestele, olenemata nende võimekustest. See on Tarbijakaitse ja Tehnilise Järevalve Ameti (TTJA) tähelepanu all, et tagada kõikidele inimestele ligipääsetavad digiteenused.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +32fc7438-003c-49dd-b5e5-e941873f98f0,1,user,Tere! Ma kuulsin midagi digiligipääsetavusest Tarbijakaitse ja Tehnilise Järevalve Ameti poolt. Mis see tähendab?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +32fc7438-003c-49dd-b5e5-e941873f98f0,2,assistant,"Tere! Digiligipääsetavus tähendab, et digitaalsetel sisu ja teenustel peab olema kättesaadav kõikidele inimestele, kaasa arvatud inimestele, kel on puueteid. Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) tegeleb sellega, et avaliku sektori teenused oleksid digitaalselt kättesaadavad.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +bd15026a-62e3-4571-9b3b-8ffca2b40fcd,1,user,"Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TJAT) tegeleb digiligipääsetavusega. Mis see täpselt tähendab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +bd15026a-62e3-4571-9b3b-8ffca2b40fcd,2,assistant,"Tere! Digiligipääsetavus tähendab, et digisisu ja -teenused on kättesaadavad ja kasutatavad kõigile, sealhulgas inimestele, kel on puueteid. TJAT järelvalvega seoses on see oluline, et avaliku sektori teenused oleksid digitaalselt kättesaadavad kõigile kodanikele. Võite tutvuda selle teema kohta rohkem TJAT poolt loodud koolitustest – nendest on mitu osa YouTube'is.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +374c3fc0-6103-4ce5-83ff-80ba2988a06f,1,user,"Tere! Mul on küsimus selle energiamärgise kohta. Mis see tähendab, et toodete energiamärgistus on Euroopa Liidus ühtne?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +374c3fc0-6103-4ce5-83ff-80ba2988a06f,2,assistant,"Tere! See tähendab, et erinevate tootegruppide, näiteks külmkappide või pesumasinate, toodete energia tarbimise nõuetele ja märgistusele on kehtestatud ühtsed reeglid. See tagab, et märgistuse vorm on võimalikult sarnane ja selgelt nähtav kõikides riikides. Näiteks, kui näete A-klassi külmkappi, teate, et see on ühesugult energiasäästlik ka teistes EL riikides.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f2d4afa3-2ff1-423a-8dd4-cedebee87344,1,user,"Tere! Ma olen kuulnud, et energiamärgistused muutuvad. Mis see tähendab täpselt? Mis toodete puhul seda muutust ootada?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f2d4afa3-2ff1-423a-8dd4-cedebee87344,2,assistant,"Tere! Jah, energiamärgistused muutuvad Euroopa Liidus. Peamiselt muutub skaala lihtsustatuks – tagasi algsele A-st G-ni. Muutus puudutab mitmeid toodete gruppe, sealhulgas nõudepesumasinaid, pesumasinaid, külmutus- ja jahutusseadmeid, televiisoreid ja valgusallikaid. Muudatused on järk-järgult sisse viidud alates 2021. aasta 1. märtsist, valgusallikate osas aga 1. septembrist 2021.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +7d022e8d-d0db-4e2e-807c-7030bc03d37a,1,user,"Tere! Ma nägin midagi energiamärgistuse kohta, aga ei saa pähe. Mis see tähendab ja miks see oluline on?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +7d022e8d-d0db-4e2e-807c-7030bc03d37a,2,assistant,"Tere! Energiamärgistus näitab, kui palju energiat seade kulutab. See on oluline, sest see aitab teil valida energiasäästlikumaid tooteid, mis aitab vähendada teie energiakulusid ja ka keskkonna mõju. ELis on kehtestatud ühtsed nõuded mitmele tootegrupile, näiteks külmikutele, pesumasinatele ja televiisioritele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b91289c3-6f6d-49f4-82b3-9936721ec6eb,1,user,"Tere! Ma lugesin midagi tarbija õiguste kohta, aga ma ei ole kindel, kust alustada, kui mul on probleemiga kauplus. Mis on esimene asi, mida ma peaksin tegema?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +b91289c3-6f6d-49f4-82b3-9936721ec6eb,2,assistant,"Tere! Kui teil on probleem kauplusega, on esimene asi, mida peaksite tegema, kirjutada neile kirja. Selgitage kirjas probleem ja millist lahendust te soovite. See on oluline samm, enne kui jätkate edasiste meetmete juurde.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +01b22a21-d735-4745-a725-548b740765da,1,user,Tere! Ma kuulsin midagi tarbijavaidluste komisjoni kohta. Mis see on ja millega saab seal abi?,output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +01b22a21-d735-4745-a725-548b740765da,2,assistant,"Tere! Tarbijavaidluste komisjon on koht, kus saab abi, kui sul on probleeme kauplejaga – näiteks kui sa ei saa oma raha tagasi ostetud toote eest või kui kaupleja ei ole täitnud oma lubadusi. Komisjon aitab vaidlusi lahendada, kui kauplejaga ise kokkulepe saavutada ei õnnestu.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3d19499f-24db-4b38-b709-b70df533d950,1,user,"Tere! Ma olen kuulnud, et Tarbijakaitseamet aitab probleemide puhul. Mis on esimene asi, mida ma peaksin tegema, kui mul on probleem mingi ostuga?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +3d19499f-24db-4b38-b709-b70df533d950,2,assistant,"Tere! Absoluutselt, Tarbijakaitseamet on abiks. Kõigepealt peaksite kirjutama kaebusena kirja kauplejale. Kui kaebus ei lahenda probleemi, saate pöörduda tarbijavaidluste komisjoni poole.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6d99066a-b6d7-4339-b315-2e6d8f18f8f0,1,user,"Tere! Ma olen kuulnud, et e-ostmine on mugav, aga ka riskantne. Mis peab silmas pidama, kui tellin midagi veebipoest?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6d99066a-b6d7-4339-b315-2e6d8f18f8f0,2,assistant,"Tere! Täiesti õige, mugavus käib käpa sees riskiga. Kõigepealt on oluline teada, kas veebipood kuulub juriidilisele isikule või eraisikule. Vaata, kelle pangakontole raha läheb ja kas ettevõte on registreeritud äriregistris. Eraisikult ostes ei kehti sulle seadusest tulenev kaitse, näiteks 14-päevane taganemisõigus.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +fe5bfc7f-8e7c-4e1a-a2a9-10ab7db0f91c,1,user,"Tere! Ma olen kuulnud, et e-poodidest ostmine on mugav, aga natuke ka riskantne. Millest ma peaksin aru saama, kui ostan midagi veebist?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +fe5bfc7f-8e7c-4e1a-a2a9-10ab7db0f91c,2,assistant,"Tere! On hea, et küsid. E-poodidest ostes on oluline teada, kas müüja on juriidiline isik või eraisik. Eraisikult ostes ei kehti seadusest tulenevad kaitseõigused, näiteks 14-päevane taganemisõigus. Veendu, kas raha läheb firmapanga arvele või eraisiku arvele ja kas ettevõte on äriregistris registreeritud.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6f5ec663-a344-4871-9798-43433dfdb439,1,user,"Tere! Ma hakkan e-poest ostma ja natuke olen segadus. Kust ma peaksin algama, et end kaitsta?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +6f5ec663-a344-4871-9798-43433dfdb439,2,assistant,"Tere! Hea, et küsid. Kõigepealt on oluline teada, kas veebipood kuulub juriidilisele isikule või eraisikule. Vaata, kelle pangakontole raha läheb. Eraisikult ostes ei saa sa tarbijana mingeid seadusest tulenevaid kaitseõigusi, näiteks 14-päevast taganemisõigust. Sa ei saa ka Tarbijakaitse Ameti poole pöörduda probleemide korral.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f6f74ef5-937d-427e-98c0-83efac9bb2f1,1,user,"Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) on tellinud mitmeid uuringuid. Kas sa saaksid rääkida neist?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +f6f74ef5-937d-427e-98c0-83efac9bb2f1,2,assistant,"Tere! Jah, TTJA on tellinud mitmeid uuringuid. Üks olulisi on uuring passiivse lairibataristu rajamise kohta maapiirkondadesse, mille tellis Majandus- ja Kommunikatsiooniministeerium. Lisaks on tehtud uuring Eesti elanike tarbijate õiguste teadlikkusest ja e-kaubanduse turust.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +37c6b6a3-93d4-48c3-9bcd-3e02485fedd7,1,user,"Tere! Olen kuulnud, et Tarbijakaitseamet on uuringuid teinud. Mis neid puudutab kõige viimati?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +37c6b6a3-93d4-48c3-9bcd-3e02485fedd7,2,assistant,"Tere! Jah, viimase aja uuringuid on mitmeid. Näiteks on valminud uuring, mis annab hinnangu 1 Gbit/s kiirust võimaldava passiivse lairibataristu rajamise maksumus maapiirkondades. Samuti on EKI teinud uuringu tarbijate teadlikkusest nende õigustest veebiküsitlusega.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +050fdacc-aa95-4da4-8edd-4ca3abaa0c68,1,user,"Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet on tellinud uuringuid. Mis neid puudutab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +050fdacc-aa95-4da4-8edd-4ca3abaa0c68,2,assistant,"Tere! Jah, neid on mitmeid. Üks olulisemaid on uuring, mis annab hinnangu 1 Gbit/s kiirust võimaldava passiivse lairibataristu rajamiseks maapiirkondadesse. Samuti on tehtud uuring Eesti elanike teadlikkusest tarbijate õigustest ja veebiküsitlus nende käitumisest probleemide korral.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +0cdd8025-5cc9-4484-8e0b-566ac36a0f7d,1,user,"Tere! Olen ajakirjanik ja tahan teada, kuidas Politsei- ja Piirivalveameti pressilisti liikmeks saada. Kas see on võimalik?",output_Politsei-_ja_Piirivalveamet +0cdd8025-5cc9-4484-8e0b-566ac36a0f7d,2,assistant,"Tere! Jah, on võimalik. Politsei- ja Piirivalveamet jagab pressilistidele regulaarselt infot. Kui soovid liikmeks saada, saad kirjutada soovi aadressil, mis on antud ametis, ning märkida oma andmed: nimi, e-posti aadress ja väljaande nimi.",output_Politsei-_ja_Piirivalveamet +4686a844-83d4-4f9c-8399-0357c24b663f,1,user,Tere! Ma olen ajakirjanik ja ma huvitatav politsei pressilisti liikmeks saamiseks. Kuidas ma saan liituda?,output_Politsei-_ja_Piirivalveamet +4686a844-83d4-4f9c-8399-0357c24b663f,2,assistant,"Tere! Politsei- ja Piirivalveamet jagab pressilistidega pressiteateid, eelmisel ööpäeval politseis registreeritud sündmuste kokkuvõtteid ja inimkannatanutega liiklusõnnetuste ülevaateid. Liitumissoovi saad saata aadressile, mis on leitav meie veebilehel. Saada kiri, kus märkige palun oma nimi, e-posti aadress ja väljaande nimi.",output_Politsei-_ja_Piirivalveamet +9b91b38f-7a5a-4818-926e-916b0cc81f88,1,user,"Tere! Ma olen ajakirjanik ja tahaksin teada, kuidas Politsei- ja Piirivalveametiga pressilisti liikmeks saada. Mis on selleks vaja?",output_Politsei-_ja_Piirivalveamet +9b91b38f-7a5a-4818-926e-916b0cc81f88,2,assistant,"Tere! Liitumine on lihtne. Saate saata e-kirja aadressil, mis on kirjeldatud ametisõnumites. Kirjutage kirja enda nimi, väljaande nimi ja e-posti aadress.",output_Politsei-_ja_Piirivalveamet +8f7c4d0c-3ecc-45b0-ab56-783132880cb4,1,user,"Tere! Mul on küsimusi selle kohta, kuidas mu täisealisele lapsele, kes elab välismaal, elamisluba Eestisse saada. Mis see üldse tähendab, elamisluba pereliikmega liitumiseks?",output_Politsei-_ja_Piirivalveamet +8f7c4d0c-3ecc-45b0-ab56-783132880cb4,2,assistant,"Tere! Elamisluba pereliikmega liitumiseks annab sinule võimaluse kutsuda oma välismaalast täisealist last Eestisse elama, kui oled Eesti kodanik või Eestis elamisloa alusel elav välismaalane. See lubab tal siin elada sinuga lähedasena või koos sinuga Eestisse kolida.",output_Politsei-_ja_Piirivalveamet +4a28da9f-0ac5-4174-b368-e2a1337cec67,1,user,Tere! Ma olen välismaalane ja mu täisealine laps soovib tulla Eestisse elama minu juurde. Kas mul on võimalik talle elamisluba taotleda?,output_Politsei-_ja_Piirivalveamet +4a28da9f-0ac5-4174-b368-e2a1337cec67,2,assistant,"Jah, see on võimalik! Elamisluba pereliikmega liitumiseks annab sinule võimaluse, kui oled Eesti kodanik või Eestis elamisloa alusel elav välismaalane, kutsuda oma täisealise lapse Eestisse elama, et ta saaks siin sinuga ühiselt elama asuda.",output_Politsei-_ja_Piirivalveamet +a175e7ca-6f2c-4535-86d0-902a55d458c7,1,user,"Tere! Ma kuulsin, et täisealise lapse saab Eestisse elama kutsuda. Mis see täpselt tähendab, mis on elamisluba pereliikmega liitumiseks?",output_Politsei-_ja_Piirivalveamet +a175e7ca-6f2c-4535-86d0-902a55d458c7,2,assistant,"Tere! Elamisluba pereliikmega liitumiseks tähendab, et kui oled Eesti kodanik või Eestis elamisloa alusel elav välismaalane, saad kutsuda oma täisealise lapse Eestisse elama, et ta saaks siin sinuga koos elada. Elamisluba annab selleks õiguse ja väljastatakse elamisloakaartina.",output_Politsei-_ja_Piirivalveamet +5d9ff142-e609-4ebb-ba5a-083a3e1072b5,1,user,Tere! Ma olen Euroopa Liidu kodanik ja plaanin Eestisse kolida. Mis on mu võimalused siin elamiseks?,output_Politsei-_ja_Piirivalveamet +5d9ff142-e609-4ebb-ba5a-083a3e1072b5,2,assistant,"Tere! Euroopa Liidu kodanikuna on sul Eestis kolmel võimalusel: ajutine viibimine (kuni 3 kuud), tähtajaline elamine (kuni 5 aastat) ja alaline elamine. Mida sa soovid Eestis teha ja kui kauaks sa plaanid jääda?",output_Politsei-_ja_Piirivalveamet +f465eabc-dafb-4d22-baf0-7e7ab7e454f7,1,user,Tere! Ma olen Euroopa Liidu kodanik ja plaanin Eestisse kolida. Mis võimalusi mul on siin elamiseks?,output_Politsei-_ja_Piirivalveamet +f465eabc-dafb-4d22-baf0-7e7ab7e454f7,2,assistant,"Tere! Euroopa Liidu kodanikuna on sul Eestis kolm võimalust: ajutine viibimine kuni 3 kuud, tähtajaline elamine kuni 5 aastat ja alaline elamine. Millist varianti sa silmas pead?",output_Politsei-_ja_Piirivalveamet +edae0758-e61d-48a2-b3c6-007a012fea12,1,user,Tere! Ma olen Euroopa Liidu kodanik ja tahan Eestisse elama tulla. Mis võimalused mul on selleks?,output_Politsei-_ja_Piirivalveamet +edae0758-e61d-48a2-b3c6-007a012fea12,2,assistant,"Tere! Eestisse saab tulla kolmel viisil: ajutine viibimine (kuni 3 kuud), tähtajaline elamine (kuni 5 aastat) või alaline elamine. Mis eesmärgiga ja kui pikaks ajaks sa Eestisse jääda soovid?",output_Politsei-_ja_Piirivalveamet +95595f72-4b82-4a1f-a841-3b730c1a69e9,1,user,Tere! Ma olen huvitatud lapsele Eesti kodakondsuse taotlemisest. Kuidas see menetlus üldiselt toimib?,output_Politsei-_ja_Piirivalveamet +95595f72-4b82-4a1f-a841-3b730c1a69e9,2,assistant,"Tere! Põhiliselt on nii, et kui te esitate taotluse, siis Politsei- ja Piirivalveamet edastab dokumendid kolme kuu jooksul Vabariigi Valitsusele otsustamiseks. See on selle taotluse esialgne menetlus.",output_Politsei-_ja_Piirivalveamet +ee011a28-7b1e-495b-a654-885f8f808109,1,user,"Tere! Ma tahan teada, kuidas toimib Eesti kodakondsuse menetlus lapsele. Mis juhtub pärast taotluse esitamist?",output_Politsei-_ja_Piirivalveamet +ee011a28-7b1e-495b-a654-885f8f808109,2,assistant,"Tere! Pärast lapse kodakondsuse taotluse esitamist, edastatakse kõik dokumendid kolme kuu jooksul Vabariigi Valitsusele. Just nemad teevad otsuse lapse Eesti kodakondsuse andmise kohta.",output_Politsei-_ja_Piirivalveamet +f42fac1b-87ff-4755-ac13-11dc0986280c,1,user,"Tere! Ma tahan teada, kuidas toimib lapsele Eesti kodakondsuse taotluse menetlus. Mis juhtub pärast taotluse esitamist?",output_Politsei-_ja_Piirivalveamet +f42fac1b-87ff-4755-ac13-11dc0986280c,2,assistant,"Tere! Pärast taotluse esitamist edastatakse kõik dokumendid kolme kuu jooksul Vabariigi Valitsusele. Valitsus teeb seejärel otsuse, kas kodakondsus lapsele antakse.",output_Politsei-_ja_Piirivalveamet +7d01a9ad-613b-42f6-87b6-453aa06c150a,1,user,"Tere! Olen kuulnud, et politsei teeb piiri ehitamist. Kas sa saaksid rääkida selle kohta midagi?",output_Politsei-_ja_Piirivalveamet +7d01a9ad-613b-42f6-87b6-453aa06c150a,2,assistant,"Tere! Jah, see on õige. Politsei- ja Piirivalveamet (PPA) on käivitanud projekti Eesti välispiiri taristu väljaehitamine. Alustati kagupiiri ehitamisega. Ühes etapis ehitatakse 23,5 kilomeetrit piiri Võrumaal, alates kolmikpunktist kuni Luhamaa piiripunktini.",output_Politsei-_ja_Piirivalveamet +cef2a4c3-1736-4094-af94-8449d87129c0,1,user,"Tere! Kuulin, et Eesti piiri ehitustööd käivad. Mis on selle hanke üldine eesmärk?",output_Politsei-_ja_Piirivalveamet +cef2a4c3-1736-4094-af94-8449d87129c0,2,assistant,"Tere! Põhiliselt on eesmärk tugevdada Eesti välispiiri turvalisust. Politsei- ja Piirivalveamet (PPA) on käivitunud mitme etapina jagatud piiriehituse hankega, et tagada piiriturvalisus ka tulevikus.",output_Politsei-_ja_Piirivalveamet +ca126228-ec63-48f3-9b08-7aa750c00fde,1,user,"Tere! Olen kuulnud, et Eesti piiri juures on käimas ehitustööd. Mis seal täpselt ehitatakse?",output_Politsei-_ja_Piirivalveamet +ca126228-ec63-48f3-9b08-7aa750c00fde,2,assistant,"Tere! Jah, seda on tõesti käimas. Politsei- ja Piirivalveamet (PPA) on käivutanud piiriehituse hanked. Esimene etapp keskendus 23,5 kilomeetri pikkusel piirilõigul Võrumaal, alates kolmikpunktist kuni Luhamaa piiripunktini. Teine etapp on samuti käimas, see hõlmab 39,5 kilomeetrit Tserebi ja Võmmorski külade vahel.",output_Politsei-_ja_Piirivalveamet +6c896611-0317-410a-a679-2156bd69d1a4,1,user,Tere! Mul on tähtajaline elamisluba töötamiseks Eestis ja see lõppeb peagi. Kuidas ma saan selle pikendada?,output_Politsei-_ja_Piirivalveamet +6c896611-0317-410a-a679-2156bd69d1a4,2,assistant,"Tere! Elamisluba töötamiseks saab pikendada. Peate esitama pikendamise taotluse vähemalt 2 kuud enne praeguse loa lõppemist. Vastus antakse hiljemalt 10 päeva enne kehtiva loa lõppemist, kui taotlus on esitatud õigeaegselt.",output_Politsei-_ja_Piirivalveamet +bdf00661-6558-4e7a-959d-e96ada72ec3b,1,user,Tere! Mul on tähtajaline elamisluba töötamiseks Eestis ja see lõppeb peatselt. Kuidas ma saan seda pikendada?,output_Politsei-_ja_Piirivalveamet +bdf00661-6558-4e7a-959d-e96ada72ec3b,2,assistant,"Tere! Elamisluba töötamiseks saab pikendada. Peate taotlema pikendamist vähemalt 2 kuud enne praeguse elamisloa lõppemist. Taotluse menetlemisel esitage nõutavad dokumendid ja tasuge riigilõiv. Kui esitate taotluse õigeaegselt, saate vastuse hiljemalt 10 päeva enne kehtiva loa lõppemist.",output_Politsei-_ja_Piirivalveamet +6f09dfc2-5898-4ab9-bf46-fa773c63a05f,1,user,Tere! Mul on tähtajaline elamisluba töötamiseks Eestis ja see aegub peagi. Kuidas ma saan seda pikendada?,output_Politsei-_ja_Piirivalveamet +6f09dfc2-5898-4ab9-bf46-fa773c63a05f,2,assistant,"Tere! Elamisluba töötamiseks saab pikendada. Vähemalt kaks kuud enne loa lõppemist pead sa pikendamise taotluse esitama. Seejärel esita nõutud dokumendid ja tasu riigilõiv. Saad vastuse hiljemalt 10 päeva enne kehtiva loa lõppemist, kui taotlus on õigeaegselt esitatud.",output_Politsei-_ja_Piirivalveamet +a9922fd9-ae2f-40a2-81b2-0061c5c62e70,1,user,"Tere! Ma olen välismaalane ja plaanin Eestis ettevõtlusega alustada. Kas te saaksite mulle rääkida, millised on elamisloa tingimused füüsilisest isikust ettevõtjale (FIE)?",output_Politsei-_ja_Piirivalveamet +a9922fd9-ae2f-40a2-81b2-0061c5c62e70,2,assistant,"Tere! Jah, muidugi. Ettevõtluseks elamisluba FIE-le saada, on mitmeid tingimusi. Peamiseks on see, et olete oma ärisse Eestis vähemalt 16 000 eurot investeerinud ja olete registreeritud Eesti Äriregistris. Kas te olete juba Äriregistris registreeritud?",output_Politsei-_ja_Piirivalveamet +54b5c751-c5d0-467c-9f0d-a080ed60b976,1,user,"Tere! Ma olen välismaalane ja plaanin Eestis füüsilisest isikust ettevõtjana (FIE) alustada. Kas sa saaksid mulle rääkida, millised on elamisloa tingimused selleks?",output_Politsei-_ja_Piirivalveamet +54b5c751-c5d0-467c-9f0d-a080ed60b976,2,assistant,"Tere! Jah, muidugi. Elamisluba ettevõtluseks FIE-le on võimalik saada, aga on ka mitmeid tingimusi. Kõigepealt pead Eestisse investeerinud vähemalt 16 000 eurot oma ärisse. Veel pead olema Äriregistris registreeritud FIE-na.",output_Politsei-_ja_Piirivalveamet +1e44db7d-42c1-4d46-8bfd-4a1821a80dbd,1,user,"Tere! Ma olen välismaalane ja plaanin Eestis ettevõtlusega alustada. Kas ma saan elamisluba, kui olen füüsilisest isikust ettevõtja (FIE)?",output_Politsei-_ja_Piirivalveamet +1e44db7d-42c1-4d46-8bfd-4a1821a80dbd,2,assistant,"Tere! Jah, FIE-le ettevõtluseks elamisluba antakse. Kuid selleks on mitmed tingimused, mis peavad täidetud olema. Üks oluline tingimus on see, et olete oma ärisse Eestis vähemalt 16 000 eurot investeerinud.",output_Politsei-_ja_Piirivalveamet +eba4a735-f801-4e39-8d25-7a39a9261a7e,1,user,Tere! Olen Ukraina kodanik ja kardustan tagasipöördumist sinna sõja tõttu. Kas ma saan Eestis taotleda rahvusvahelist kaitset?,output_Politsei-_ja_Piirivalveamet +eba4a735-f801-4e39-8d25-7a39a9261a7e,2,assistant,"Tere! Jah, sa saad. Rahvusvahelist kaitset võivad taotleda välismaalased, sh Ukraina kodanikud, kes kardavad kodumaal tagakiusamist või kellele ähvardab ohtu tagasipöördumine seoses relvakonfliktiga. See kehtib, kui kardad näiteks surmanuhtlust, piinamist või inimväärikust alandavat kohtlemist.",output_Politsei-_ja_Piirivalveamet +ee654a41-8486-4e97-acdf-b8d59e5e125d,1,user,"Tere! Mul on küsimusi rahvusvahelise kaitse taotlemise kohta, eriti seoses olukorraga Ukrainas. Kuidas ma saan seda taotleda?",output_Politsei-_ja_Piirivalveamet +ee654a41-8486-4e97-acdf-b8d59e5e125d,2,assistant,"Tere! Rahvusvahelise kaitse taotlemiseks võite pöörduda nelja kohta: Tallinnas Pärnu mnt 139, Pärnus H. Tammsaare pst 61, Jõhvis Rahu tn 38 või Tartus Riia tee 132. Samuti on saadaval ankeedid Politsei- ja Piirivalveameti veebilehel. Aluseks on põhjendus, et te kardate tagakiusamist Ukrainas rassi, rahvuse, usu, ühiskondliku rühmitusse kuulumise või poliitiliste vaadete tõttu või et tagasipöördumine Ukrainasse kujutab endast tõsist ohtu.",output_Politsei-_ja_Piirivalveamet +3b0d0e1e-c3e4-4495-9bab-c1e8cceafbf0,1,user,"Tere! Ma kuulsin, et Ukrainast saab rahvusvahelist kaitset Eestis taotlema. Kuidas see täpselt töötab?",output_Politsei-_ja_Piirivalveamet +3b0d0e1e-c3e4-4495-9bab-c1e8cceafbf0,2,assistant,"Tere! Jah, see on õige. Rahvusvahelist kaitset saab taotlema igaüks, kes kardab Ukrainas tagakiusamist, näiteks rassi, usu või poliitiliste vaadete tõttu. Samuti, kui tagasipöördumine Ukrainasse kujutab endast tõsist ohtu, näiteks surmanuhtluse, piinamise või vägivalla tõttu relvakonflikti tõttu. Mis sind selle osas kõige rohkem huvitab?",output_Politsei-_ja_Piirivalveamet +1ecb2d71-4481-4341-b7b7-b20961f52e3f,1,user,"Tere! Ma saan aru, et see on Politsei- ja Piirivalveameti kontaktinfo? Mis on PPA registrikood?",output_Politsei-_ja_Piirivalveamet +1ecb2d71-4481-4341-b7b7-b20961f52e3f,2,assistant,"Jah, õige! PPA registrikood on 70008747. See on oluline number ametniku jaoks, kui sa seda näiteks vormistama pead.",output_Politsei-_ja_Piirivalveamet +8ec8400e-60f8-4b5f-aead-d842409bbc0f,1,user,"Tere! Ma olen kuulnud, et Politsei- ja Piirivalveamet on oluline riigiasutus. Milline on PPA registrikood ja aadress?",output_Politsei-_ja_Piirivalveamet +8ec8400e-60f8-4b5f-aead-d842409bbc0f,2,assistant,"Tere! Jah, see on õige. PPA registrikood on 70008747 ja nende aadress on Pärnu mnt 139, 15060 Tallinn.",output_Politsei-_ja_Piirivalveamet +8c12a6c4-f0d9-4ee3-8c17-2c402af34ccb,1,user,"Tere! Ma imestan, kuidas Politsei- ja Piirivalveametiga (PPA) ühendust saada, kui mul on küsimus?",output_Politsei-_ja_Piirivalveamet +8c12a6c4-f0d9-4ee3-8c17-2c402af34ccb,2,assistant,"Tere! PPA registrikood on 70008747 ja aadress on Pärnu mnt 139, 15060 Tallinn. Sa saad meiega ühendust faksi teel, number on 612 3009. Üldiselt on riigiinfo telefon 1247 ka hea võimalus alustuseks.",output_Politsei-_ja_Piirivalveamet +ffd2ed5e-48a4-4191-96e3-dfc6eed360e9,1,user,Tere! Ma olen välisriigi kodanik ja plaanin Eestis äri alustada. Kas ma kindlasti vajan elamisluba selleks?,output_Politsei-_ja_Piirivalveamet +ffd2ed5e-48a4-4191-96e3-dfc6eed360e9,2,assistant,"Tere! Vaelisriigi kodanikuna jah, enamikul juhtudel vajad. Kui sa ei ole Euroopa Liidu riikide, Islandi, Liechtensteini, Norra või Šveitsi kodanik, siis ettevõtluseks Eestis on vaja elamisluba.",output_Politsei-_ja_Piirivalveamet +2e1d66a9-7910-42a6-9540-f23f6cdef94d,1,user,Tere! Ma olen välisriigi kodanik ja plaanin Eestis äri alustada. Kas mulle on vaja mingit erist elamisluba?,output_Politsei-_ja_Piirivalveamet +2e1d66a9-7910-42a6-9540-f23f6cdef94d,2,assistant,"Tere! Jah, kui sa ei ole Euroopa Liidu riigi, Islandi, Liechtensteini, Norra või Šveitsi kodanik, siis Eestis ettevõtlusega tegelneda on vaja elamisluba ettevõtluseks. See on Politsei- ja Piirivalveameti väljastatav dokument.",output_Politsei-_ja_Piirivalveamet +55c733b7-ab14-4fcc-b62d-b9955fd86a5c,1,user,Tere! Ma olen välisriigi kodanik ja plaanin Eestis ettevõtlusega alustada. Kas mulle on vaja elamisluba?,output_Politsei-_ja_Piirivalveamet +55c733b7-ab14-4fcc-b62d-b9955fd86a5c,2,assistant,"Tere! Jah, kui sa ei ole Euroopa Liidu, Islandi, Liechtensteini, Norra või Šveitsi kodanik, siis Eestis ettevõtlusega tegelemiseks on sulle vaja elamisluba.",output_Politsei-_ja_Piirivalveamet +b0676f86-ac0a-4850-869e-edcd619ec50e,1,user,Tere! Mul on küsimus Politsei- ja Piirivalveameti lipudest. Alustame lihtsast: mis on Politseiameti lipu põhivärv?,output_Politsei-_ja_Piirivalveamet +b0676f86-ac0a-4850-869e-edcd619ec50e,2,assistant,Tere! Politseiameti lipu põhivärv on sinine. See on üks meie riigilipu värve ja sümboliseerib rahu ja stabiilsust.,output_Politsei-_ja_Piirivalveamet +406270b4-211d-4b26-a200-2084e46a17b6,1,user,Tere! Mul on küsimus Eesti politsei lipust. Mis on selle peamine värv ja mida see sümboliseerib?,output_Politsei-_ja_Piirivalveamet +406270b4-211d-4b26-a200-2084e46a17b6,2,assistant,"Tere! Eesti politsei lipu peamine värv on sinine. See värv sümboliseerib rahu ja stabiilsust, sest see on ka üks meie riigilipu värve.",output_Politsei-_ja_Piirivalveamet +05ea43e5-6558-48bb-b711-fe23d2174432,1,user,"Tere! Ma tegelikult ei mäleta, kuidas Politsei- ja Piirivalveameti lipud välja näevad. Saaksid sa mulle natuke rääkida?",output_Politsei-_ja_Piirivalveamet +05ea43e5-6558-48bb-b711-fe23d2174432,2,assistant,"Tere! Muidugi. Politsei lipu põhiline värv on sinine, mis on ka meie riigilipuvärv ja sümboliseerib rahu ja stabiilsust. Sellel on valge püstitõusnud lõvi, kes hoiab käes väikest Eesti riigivappi. Lipu mõõdud on 105x135 cm ja see on ääristatud kuldsete narmastega.",output_Politsei-_ja_Piirivalveamet +7a68bb61-1b56-4f00-a5c6-82ad2cbb5a96,1,user,"Tere! Ma sain trahviteate ja seal on mainitud kiiruskaamera. Kas mulle automaatselt saadetakse foto, mille alusel trahv määrati?",output_Politsei-_ja_Piirivalveamet +7a68bb61-1b56-4f00-a5c6-82ad2cbb5a96,2,assistant,"Tere! Ei, trahviteatele ei lisata koopia kiiruskaamera pildist. Kuid, kui sa oled sõiduki eest vastutav isik ja soovid pildi koopiat, saad selle taotleda.",output_Politsei-_ja_Piirivalveamet +f548acfe-7e15-47a6-b1c6-97fc5850630a,1,user,Tere! Mulle tekkis küsimus kiiruskaamera fotode kohta. Kas trahviteatega kaasneb ka foto koopia?,output_Politsei-_ja_Piirivalveamet +f548acfe-7e15-47a6-b1c6-97fc5850630a,2,assistant,"Tere! Ei, trahviteatega fotokoopiat ei lisata. Aga kui sa oled sõiduki eest vastutav isik ja soovid fotot, mille alusel liiklusrikkumine tuvastati, siis saad selle taotleda.",output_Politsei-_ja_Piirivalveamet +f090093c-56fd-4ddd-8590-d3447e7c9977,1,user,"Tere! Ma sain trahviteate kiiruskaamerast, aga ma ei saa aru, kas fotot saab ka saada?",output_Politsei-_ja_Piirivalveamet +f090093c-56fd-4ddd-8590-d3447e7c9977,2,assistant,"Tere! Põhimõtteliselt jah, saad. Trahviteatega ei saadetata fotot automaatselt. Kuid mootorsõiduki eest vastutav isik saab taotleda fotokoopiat. Seda tuleb teha aga eraldi taotlusega, millele on lisatud digitaal- või tavaallkiri.",output_Politsei-_ja_Piirivalveamet +73a79a60-1fcb-48e5-b334-ce12ea2414aa,1,user,"Tere! Kas te saate öelda, kus Kuressaare politseijaoskond asub?",output_Politsei-_ja_Piirivalveamet +73a79a60-1fcb-48e5-b334-ce12ea2414aa,2,assistant,"Muidugi! Kuressaare politseijaoskond asub Transvaali 58, Kuressaare. See on väga mugav, sest sealt on lihtne bussidega sinna jõuda.",output_Politsei-_ja_Piirivalveamet +c1f53b76-5b26-4d17-b41f-01f72a34362b,1,user,"Tere! Kas te oskate öelda, kus Kuressaare politseijaoskond asub?",output_Politsei-_ja_Piirivalveamet +c1f53b76-5b26-4d17-b41f-01f72a34362b,2,assistant,"Tere! Kuressaare politseijaoskond asub aadressil Transvaali 58, Kuressaare.",output_Politsei-_ja_Piirivalveamet +9314defd-c899-4ca6-90cc-576491ef6a62,1,user,"Tere! Kas te oskate öelda, kus Kuressaare politseijaoskonn asub?",output_Politsei-_ja_Piirivalveamet +9314defd-c899-4ca6-90cc-576491ef6a62,2,assistant,"Muidugi! Kuressaare politseijaoskonn asub Transvaali 58, Kuressaare. See on väga mugav koht, sest sinna on lihtne jõuda bussiga. Kas te soovite teada, millise peatuses on see?",output_Politsei-_ja_Piirivalveamet +afcd00b8-b72e-4795-8ae4-dbfb6a859f40,1,user,"Tere! Ma tahan teada, mis on Eesti kodaniku passi tellimisel riigilõivu suurus?",output_Politsei-_ja_Piirivalveamet +afcd00b8-b72e-4795-8ae4-dbfb6a859f40,2,assistant,Tere! Eesti kodaniku passi riigilõivu suurust saad teada Politsei- ja Piirivalveameti veebilehel. Link on https://www.politsei.ee/et/telli-dokument-selverisse. Täpset summat saad seal vaadata.,output_Politsei-_ja_Piirivalveamet +19cfaa48-f74e-43e4-851f-050d5e9e77d2,1,user,"Tere! Ma tahan teada, mis riigilõivu pean maksma, kui tahan uut Eesti kodaniku passi tellida. Leidsin, et link on politsei.ee lehel, aga ma ei leia täpselt seda infot seal.",output_Politsei-_ja_Piirivalveamet +19cfaa48-f74e-43e4-851f-050d5e9e77d2,2,assistant,"Tere! Jah, see on tavaline küsimus. Eesti kodaniku passi tellimisel on riigilõivu. Sa saad selle tellida e-taotluse kaudu: https://etaotlus.politsei.ee/#/login. Konkreetset riigilõivu summat ma hetkel siin öelda ei suuda, aga see on seotud tellimusega. Vaata politsei.ee lehel olevat riigilõivude loendit: https://www.politsei.ee/et/telli-dokument-selverisse, et täpsed summad näha.",output_Politsei-_ja_Piirivalveamet +5a494f85-751d-4e36-9a55-8b1a8697ba77,1,user,"Tere! Ma tahan teada, palju maksab Eesti kodaniku passi tellimine. Ma nägin midagi selle kohta Politsei- ja Piirivalveameti lehel.",output_Politsei-_ja_Piirivalveamet +5a494f85-751d-4e36-9a55-8b1a8697ba77,2,assistant,"Tere! Eesti kodaniku passi tellimisel on riigilõiv. Selle kohta saad infot Politsei- ja Piirivalveameti veebilehel, täpsemalt siin: https://www.politsei.ee/et/telli-dokument-selverisse. Veebilehel leiad ka viitenumbri, mis on 2900082443.",output_Politsei-_ja_Piirivalveamet +7b5baef4-56e3-488c-86f1-6f99e5d29368,1,user,"Tere! Ma olen huvitatud Eesti kodakondsuse taotlemisest. Kui ma esitan taotluse, mis siis juhtub järgmisena?",output_Politsei-_ja_Piirivalveamet +7b5baef4-56e3-488c-86f1-6f99e5d29368,2,assistant,Tere! Pärast taotluse esitamist edastatakse dokumendid Vabariigi Valitsusele. Nad langetavad otsuse sinu kodakondsuse andmise kohta.,output_Politsei-_ja_Piirivalveamet +04cf3b64-38e2-4681-9f6e-8ccaf0b8e155,1,user,"Tere! Ma olen kuulnud, et saab täiskasvanuna Eesti kodakondsust taotlema. Kuidas see menetlus käib umbes?",output_Politsei-_ja_Piirivalveamet +04cf3b64-38e2-4681-9f6e-8ccaf0b8e155,2,assistant,"Tere! Jah, see on võimalik. Pärast kodakondsuse taotluse esitamist saadetakse dokumendid Vabariigi Valitsusele. Nad teevad otsust, kas kodakondsus antakse.",output_Politsei-_ja_Piirivalveamet +c6cff9cb-a84b-4b10-b49a-543daffa83bf,1,user,"Tere! Ma tahan teada, kuidas toimib Eesti kodakondsuse taotlus täiskasvanule. Mis juhtub siis, kui ma olen kodakondsuse taotluse esitanud?",output_Politsei-_ja_Piirivalveamet +c6cff9cb-a84b-4b10-b49a-543daffa83bf,2,assistant,"Tere! Pärast kodakondsuse taotluse esitamist edastatakse kõik dokumendid kuue kuu jooksul Vabariigi Valitsusele. Nad teevad otsust, kas sulle kodakondsus antakse.",output_Politsei-_ja_Piirivalveamet +672947fa-e1e6-4bde-b53c-940d64cf8a7c,1,user,"Tere! Ma kuulin, et Politsei- ja Piirivalveamet haldab piiripunkte. Mis see tähendab tegelikult?",output_Politsei-_ja_Piirivalveamet +672947fa-e1e6-4bde-b53c-940d64cf8a7c,2,assistant,"Tere! See tähendab, et amet vastutab piiripunktide korralduse eest. Näiteks, piiripunkti juures ei ole alaliselt politseinikke, vaid nad on kohal väljakutsel piirikontrolli teostamiseks.",output_Politsei-_ja_Piirivalveamet +6ce5bb7c-a926-4c97-91b3-c4127a29c245,1,user,"Tere! Ma kuulsin, et piiripunktis ei ole alati politseinikke. Kas see on tõsi?",output_Politsei-_ja_Piirivalveamet +6ce5bb7c-a926-4c97-91b3-c4127a29c245,2,assistant,"Jah, see on õige. Politsei ametnikud ei viibi piiripunktis alaliselt. Nad teostavad piirikontrolli väljakutsel.",output_Politsei-_ja_Piirivalveamet +33411ace-19dd-4053-bf04-970e6e000529,1,user,"Tere! Ma kuulsin, et piiripunktides ei ole alati politseinikke. Kas see on tõsi?",output_Politsei-_ja_Piirivalveamet +33411ace-19dd-4053-bf04-970e6e000529,2,assistant,"Jah, see on õige. Politseiametnikud ei viibi piiripunktides alaliselt. Piirikontrolli teostamine toimub väljakutsel.",output_Politsei-_ja_Piirivalveamet +29cf682a-2d68-4b68-8972-3250e15e8572,1,user,"Tere! Ma sooviksin teada, kuidas ma saaksin Politsei- ja Piirivalveametilt (PPA) mõnda dokumenti saada. Milline on selleks õige viis?",output_Politsei-_ja_Piirivalveamet +29cf682a-2d68-4b68-8972-3250e15e8572,2,assistant,"Tere! Selleks tuleb pöörduda PPA poole e-posti teel või postiaadressil Pärnu mnt 139, 15060, Tallinn. Kui sa saadad faili e-posti teel, palume krüpteerida need sertifikaadi „Politsei- ja Piirivalveamet (PPA) 70008747“ abil turvalisuse tagamiseks.",output_Politsei-_ja_Piirivalveamet +5b8fbca9-131c-47a9-bc92-1a06d76f492f,1,user,Tere! Mul on küsimus Politsei- ja Piirivalveameti poole pöördumise kohta. Kuidas ma saan teavet taotleda?,output_Politsei-_ja_Piirivalveamet +5b8fbca9-131c-47a9-bc92-1a06d76f492f,2,assistant,"Tere! Teavet saab taotleda mitmel viisil. Saab esitada teabenõude konkreetse dokumendi saamiseks, selgitustaotluse, kui on vaja täpsemat vastust, või märgukirja, et anda ettepanekuid PPA tööle. Kui soovite tutvuda enda andmetega, saate esitada andmesubjekti päringu.",output_Politsei-_ja_Piirivalveamet +e906f80c-06b1-47cb-be0e-f11284a26062,1,user,"Tere! Ma tahaksin teada, kuidas ma saan Politsei- ja Piirivalveametilt teavet küsida. Mis on kõige lihtsam viis?",output_Politsei-_ja_Piirivalveamet +e906f80c-06b1-47cb-be0e-f11284a26062,2,assistant,"Tere! Pöördumised PPA poole saab saata e-posti teel või postiaadressil Pärnu mnt 139, 15060 Tallinn. Kui saata e-postiga faile, palun krüpteeri need sertifikaadiga „Politsei- ja Piirivalveamet (PPA) 70008747“ turvalisuse tagamiseks.",output_Politsei-_ja_Piirivalveamet +eb56e0ba-fb26-471f-a002-3d577f2b69ab,1,user,Tere! Ma olen kuulnud midagi avalikest koosolekutest. Mis see täpselt tähendab?,output_Politsei-_ja_Piirivalveamet +eb56e0ba-fb26-471f-a002-3d577f2b69ab,2,assistant,"Tere! Avalik koosolek on oluline mõiste. See tähendab inimeste kogunemist avalikus kohas ühise eesmärgiga oma meelt väljendada või kujundada. Näiteks väljakul, pargis või isegi tänaval toimuv demonstratsioon või miiting kuulub selle alla.",output_Politsei-_ja_Piirivalveamet +2a829cd1-3877-4ab5-88df-792396bf4769,1,user,"Tere! Ma olen kuulnud, et avalikku koosolekut tuleb politseist registreerida. Mis täpselt see avalik koosolek on?",output_Politsei-_ja_Piirivalveamet +2a829cd1-3877-4ab5-88df-792396bf4769,2,assistant,"Tere! Avalik koosolek on inimeste kogunemine avalikus kohas ühise eesmärgiga, näiteks meeliste väljendamiseks või mõne asja kujundamiseks. See võib olla näiteks väljaku-, pargi-, tee- või tänavakorraldatav kohtumine, demonstratsioon, miiting, pikett, religioosne üritus või rongkäik.",output_Politsei-_ja_Piirivalveamet +b8d7b98f-21c5-4f7e-b0c8-f55c62a8c5f6,1,user,"Tere! Ma lugesin, et politsei teab avalike koosolekute kohta palju. Mis siis täpselt avalik koosolek on?",output_Politsei-_ja_Piirivalveamet +b8d7b98f-21c5-4f7e-b0c8-f55c62a8c5f6,2,assistant,"Tere! Hea, et küsisid. Avalik koosolek on inimeste kogunemine avalikus kohas ühise eesmärgiga. See võib olla näiteks väljaku, pargi, tee või tänava peal toimuva koosoleku, demonstratsiooni, miitingu, piketi, religioosse ürituse või rongkäigu vormis.",output_Politsei-_ja_Piirivalveamet +cd745c34-96fe-43dc-b612-f8f22ffb1f72,1,user,"Tere! Ma tahaksin teada, kuidas ma saan lapsele passi taotleda. Mis on esimene asi, mida ma pean tegema?",output_Politsei-_ja_Piirivalveamet +cd745c34-96fe-43dc-b612-f8f22ffb1f72,2,assistant,"Tere! Esiteks, pead broneerima aja politsei teeninduses. Sa leiad teenindused ja saad aja broneerida siin: https://broneering.politsei.ee/. Või saad dokumendi kaupluses kätte, aga sel juhul pead arvestama lisatasuga.",output_Politsei-_ja_Piirivalveamet +0477b0a2-6497-425a-af6b-b6d2c1403e60,1,user,"Tere! Ma tahaksin teada, kuidas ma saan lapsele Eesti passi taotleda. Kus ma pean selleks aja broneerima?",output_Politsei-_ja_Piirivalveamet +0477b0a2-6497-425a-af6b-b6d2c1403e60,2,assistant,"Tere! Eesti passi taotlemiseks lapsele saad aja broneerida PPA teeninduses. Broneerimiseks kasuta veebilehte https://broneering.politsei.ee/. Kas soovid teada ka, kuidas dokumendi kätte saab?",output_Politsei-_ja_Piirivalveamet +f8129230-6b1d-4222-903c-e257fe2c2146,1,user,"Tere! Ma tahaksin teada, kuidas lapsele passi taotlemine toimib. Kus ma peaksin selle aja broneerima?",output_Politsei-_ja_Piirivalveamet +f8129230-6b1d-4222-903c-e257fe2c2146,2,assistant,Tere! Lapse passi taotluse menetluse aega saad broneerida Politsei- ja Piirivalveameti teeninduses. Broneeringu võimalus on saadaval aadressil https://broneering.politsei.ee/ või vaata teenindusi aadressil (/et/asukohad/teenindused).,output_Politsei-_ja_Piirivalveamet +8868fc38-b17f-4172-bb64-53935aa2ca27,1,user,"Tere! Ma elan välismaal ja tahan teada, kuidas saan elamisloakaarti taotleda. On see võimalik?",output_Politsei-_ja_Piirivalveamet +8868fc38-b17f-4172-bb64-53935aa2ca27,2,assistant,"Tere! Jah, välismaal saab elamisloakaarti taotleda, aga erandkorras. See on võimalik, kui sa ei ole Euroopa Liidu liikmesriigi kodanik, sul on kehtiv Eesti elamisluba või elamisõigus ning sinu välismaa elukoht on registreeritud Eesti rahvastikuregistris. Kas sa vastad neile tingimustele?",output_Politsei-_ja_Piirivalveamet +0270365a-5c84-4943-a16f-705ef152867a,1,user,"Tere! Ma elan välismaal ja tahan teada, kuidas ma saan elamisloakaarti taotleda. Kas see on üldse võimalik?",output_Politsei-_ja_Piirivalveamet +0270365a-5c84-4943-a16f-705ef152867a,2,assistant,"Tere! Jah, see on võimalik, aga erandkorras. Sa saad elamisloakaarti taotleda välismaal, kui sa ei ole Euroopa Liidu liikmesriigi kodanik, sul on kehtiv Eesti elamisluba või elamisõigus ja sinu välismaa elukoht on registreeritud rahvastikuregistris. Kas sa vastad nendele tingimustele?",output_Politsei-_ja_Piirivalveamet +9cea2168-ff67-4f16-8c9d-2bacb561ad23,1,user,"Tere! Ma olen välismaal ja tahaksin teada, kuidas ma saan elamisloakaarti taotleda. On see võimalik?",output_Politsei-_ja_Piirivalveamet +9cea2168-ff67-4f16-8c9d-2bacb561ad23,2,assistant,"Tere! Jah, on võimalik. Välismaal saab elamisloakaarti taotleda erandkorras, kui oled välismaalane, kes ei ole EL liikmesriigi kodanik, sul on kehtiv Eesti elamisluba või elamisõigus ning sinu välismaa elukoht on Eesti rahvastikuregistris registreeritud.",output_Politsei-_ja_Piirivalveamet +43911ec2-9798-4ce3-8363-4bd5c73fa456,1,user,"Tere! Ma kuulin, et Politsei ja Piirivalveameti koduleht on tehtud nii, et see oleks kõigile kättesaadav. Kuidas see täpselt tagatakse?",output_Politsei-_ja_Piirivalveamet +43911ec2-9798-4ce3-8363-4bd5c73fa456,2,assistant,"Tere! See on õige. Meie koduleht on ehitatud vastavalt WCAG 2.1 AA juurdepääsetavuse suunistele. See tähendab, et oleme kasutanud nii tehnilisi vahendeid kui ka sisu koostamise põhimõtteid, et puuetega inimesed saaksid seda kasutajakogemust nautida. See kehtib näiteks pimese, vaegnägemise, kurtuse, vaegkuulmise, õpiraskuste ja teiste inimeste jaoks, kellel on erinevaid erivajadusi.",output_Politsei-_ja_Piirivalveamet +0cc64933-3f60-43d3-916e-40eb688204df,1,user,"Tere! Ma kuulsin, et Politsei- ja Piirivalveameti koduleht on tehtud juurdepääsetavaks. Mis see täpsemalt tähendab?",output_Politsei-_ja_Piirivalveamet +0cc64933-3f60-43d3-916e-40eb688204df,2,assistant,"Tere! See tähendab, et leht on ehitatud nii, et see vastaks WCAG 2.1 AA suunistele. See tähendab, et oleme kasutanud nii tehnilisi vahendeid kui ka sisu koostamise põhimõtteid, mis aitavad puuetega inimestel kodulehte kasutada. Näiteks pimesed, vaegnägevad, kurtud, vaegkuulvad, inimesed õpiraskustega või kognitiivsete piirangutega – kõik need saavad lehte paremini kasutada.",output_Politsei-_ja_Piirivalveamet +88ddf800-a3da-41af-ae81-3acfd892a3b9,1,user,Tere! Ma sain just elamisloa ettevõtluseks suurinvestorina. Kust ma saan selle elamisloakaardi kätte?,output_Politsei-_ja_Piirivalveamet +88ddf800-a3da-41af-ae81-3acfd892a3b9,2,assistant,"Õnnitleme! Elamisloakaardi saad kätte isiklikult kas Eesti välisesinduses või Politsei- ja Piirivalveameti teeninduses. Tõenäoliselt märkisid sa seda juba oma tähtajalise elamisloa taotluses, kus said valida sobiva koha.",output_Politsei-_ja_Piirivalveamet +33c821bf-90e0-4a79-993c-c8ec1ab47e84,1,user,"Tere! Mul on küsimus elamisloakaardi kohta. Kust ma selle saan, kui mul on suurinvestori elamisluba?",output_Politsei-_ja_Piirivalveamet +33c821bf-90e0-4a79-993c-c8ec1ab47e84,2,assistant,Tere! Suurinvestori elamisluba andes saad elamisloakaardi kas Eesti välisesindusest või Politsei- ja Piirivalveameti teenindusest. Mis asukoht oli märgitud sinu tähtajalise elamisloa taotluses?,output_Politsei-_ja_Piirivalveamet +77b1dabe-9143-46df-872b-d109d4b2df32,1,user,"Tere! Mul on küsimus elamisloakaardi kohta. Kust ma selle saan, kui mul on elamisluba ettevõtluseks suurinvestorina?",output_Politsei-_ja_Piirivalveamet +77b1dabe-9143-46df-872b-d109d4b2df32,2,assistant,"Tere! Elamisloakaardi saad kätte isiklikult Eesti välisesinduses või Politsei- ja Piirivalveameti teeninduses. Sa pead valima selle koha, mille märkisid oma tähtajalise elamisloa taotlusesse.",output_Politsei-_ja_Piirivalveamet +2afc1210-9b6c-4725-bdb4-4a58e87a156f,1,user,"Tere! Kas ma saan Eesti kodakondsuse taotleda, kui ma olen eestkostetav?",output_Politsei-_ja_Piirivalveamet +2afc1210-9b6c-4725-bdb4-4a58e87a156f,2,assistant,"Tere! Jah, võimalik on. Aga on mõned tingimused, mida pead täistõttu täitma. Peamine asi on see, et pead olema vähemalt 15-aastane. Kas te seda teate?",output_Politsei-_ja_Piirivalveamet +ee8bf2dd-a179-46fe-af36-167cc5a7927a,1,user,"Tere! Ma arvan, et minu tüli tüdruk pole pärit Eestist. Ta küsib, kas ta saab Eesti kodakondsust, sest ta on siin elamine luba saanud. Mida ta peab tegema?",output_Politsei-_ja_Piirivalveamet +ee8bf2dd-a179-46fe-af36-167cc5a7927a,2,assistant,"Tere! Jah, seda on võimalik. Arvatavasti sa mõtled, mida ta peab tegema, et Eesti kodakondsus taotleda. Alguspunkt on see, et ta peab olema vähemalt 15-aastane. Kas ta on selline?",output_Politsei-_ja_Piirivalveamet +9d7f35f6-e344-44f7-8302-1b95e257b5f9,1,user,"Tere! Mul on küsimus kodakondsuse kohta. Ma olen eestkostetav, ja tahaksin teada, millised on need tingimused, et Eesti kodakondsust saan taotleda?",output_Politsei-_ja_Piirivalveamet +9d7f35f6-e344-44f7-8302-1b95e257b5f9,2,assistant,"Tere! Päev vastu! Tegelgelt, eestkostetav saab Eesti kodakondsust taotleda, kui täidab mitmeid tingimusi. Peamised need on: sa pead olema vähemalt 15-aastane, ja sul peab olema pikaajalise elaniku elamisluba või alaline elamisõigus. Kas sa tead, mida see pikaajaline elanik on?",output_Politsei-_ja_Piirivalveamet +75842cb8-3e01-4d9a-83c2-1026b1291d19,1,user,"Tere! Ma tahan Eesti passi taotleda. Kutsun teid abi, palun.",output_Politsei-_ja_Piirivalveamet +75842cb8-3e01-4d9a-83c2-1026b1291d19,2,assistant,Tere! Muidugi aitame. Millist passi soovite taotleda – tavapass või kiirkorras?,output_Politsei-_ja_Piirivalveamet +b15cb028-7935-4e2f-9d45-2a022ed5f646,1,user,Tere! Ma tahan uue passi taotleda. Mis ma pean tegema?,output_Politsei-_ja_Piirivalveamet +b15cb028-7935-4e2f-9d45-2a022ed5f646,2,assistant,"Tere! Noh, uue passi taotlemine on suht lihtne. Alustage kõigepealt leidmaks sobiv teenindus. Palju teeninduskohasid on üle Eesti. Vaadake kindlasti (/et/asukohad/teenindused) kohta, kus teil on mugavam kohapeal abi saada. Peate ka isikut tõendava dokumendi kaasa võtma.",output_Politsei-_ja_Piirivalveamet +188018e9-0815-4b21-a6a1-d3329f5408ed,1,user,Tere! Ma kuulsin midagi piirkondlikust politseitööst. Mis see tähendab täpselt?,output_Politsei-_ja_Piirivalveamet +188018e9-0815-4b21-a6a1-d3329f5408ed,2,assistant,"Tere! Piirkondlik politseitöö tähendab, et politseinikud keskenduvad konkreetses piirkonnas probleemide lahendamisele ja kogukonna vajaduste täitmisele. See on üks politsei töövorme, mis tugineb kogukonnakeskse lähenemise põhimõtetele.",output_Politsei-_ja_Piirivalveamet +2786fa7d-a6e1-405f-afc4-cc28b22cc4e9,1,user,Tere! Mul on küsimus politsei piirkondliku töö kohta. Mis see tähendab ja kuidas see erineb tavalisest politseist?,output_Politsei-_ja_Piirivalveamet +2786fa7d-a6e1-405f-afc4-cc28b22cc4e9,2,assistant,"Tere! Piirkondlik politseitöö on tegevus, mis keskendub konkreetses piirkonnas olevate kogukonna vajaduste lahendamisele. See tugineb kogukonnakesksele politseitööle ja eesmärgiks on lähendada politseid kogukonnale, leida piirkonna turvalisust ohustavaid probleeme ning arendada koostööd asutuste ja kohalike omavalitsustega. See erineb tavalisest politseist selle poolest, et fookus on probleemide ennetamisel ja laiemaid lahendusi otsimisel, mitte ainult reageerimisel sündmustele.",output_Politsei-_ja_Piirivalveamet +61fb8638-2ed6-43f9-96f9-6f58f3142431,1,user,Tere! Ma kuulsin midagi piirkondliku politseitöö kohta. Mis see tähendab?,output_Politsei-_ja_Piirivalveamet +61fb8638-2ed6-43f9-96f9-6f58f3142431,2,assistant,"Tere! Piirkondlik politseitöö on politseitöö vorm, mis keskendub probleemide lahendamisele konkreetses piirkonnas ja tugineb kogukonnakesksetele põhimõtetele. See tähendab, et politsei püüab lähemalt tulla kogukonnale ja arvestada selle vajadusi.",output_Politsei-_ja_Piirivalveamet +91667460-f27f-4c8f-ab03-5d8161e17e82,1,user,"Tere! Ma kuulsin, et Politsei- ja Piirivalveamet on saanud rahastust piiriehituseks. Mis see täpsemalt tähendab?",output_Politsei-_ja_Piirivalveamet +91667460-f27f-4c8f-ab03-5d8161e17e82,2,assistant,"Tere! See on õige. PPA on perioodil 2014-2020 saanud toetust Euroopa Liidu Sisejulgeolekufondi välispiiride ja viisade rahastamisvahendist, et rahastada erinevaid piiriehitusprojekte.",output_Politsei-_ja_Piirivalveamet +68952028-8657-4be4-b4ec-33773f1d0fa0,1,user,"Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on saanud raha piiriehituseks. Kas see on tõsi?",output_Politsei-_ja_Piirivalveamet +68952028-8657-4be4-b4ec-33773f1d0fa0,2,assistant,"Jah, see on tõsi. Politsei- ja Piirivalveamet on saanud toetust piiriehituse rahastamiseks Euroopa Liidu Sisejulgeolekufondi välispiiride ja viisade rahastamisvahendist perioodil 2014-2020.",output_Politsei-_ja_Piirivalveamet +a9cdbe85-fef0-45a6-85ad-776a502b7749,1,user,"Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on saanud raha piiriehituse jaoks. Kas see on tõsi?",output_Politsei-_ja_Piirivalveamet +a9cdbe85-fef0-45a6-85ad-776a502b7749,2,assistant,"Jah, see on tõsi! Politsei- ja Piirivalveamet on saanud toetust piiriehituse rahastamiseks Euroopa Liidu Sisejulgeolekufondi välispiiride ja viisade rahastamisvahendist perioodil 2014-2020.",output_Politsei-_ja_Piirivalveamet +ff5b2c42-5778-4120-bb91-a5956ee6ab66,1,user,"Tere! Ma nägin, et Politsei- ja Piirivalveamet kasutab sotsiaalmeediat. Kas teil on Facebooki leht?",output_Politsei-_ja_Piirivalveamet +ff5b2c42-5778-4120-bb91-a5956ee6ab66,2,assistant,"Jah, meil on! Meie peamine Facebooki leht on https://www.facebook.com/politseijapiirivalveamet/. Meie Põhja, Lääne ja Lõuna prefektuuridele on ka oma Facebooki lehed. Kas sa tahad teada nende lingid?",output_Politsei-_ja_Piirivalveamet +c8550aa4-71e5-4e6f-9589-b567fcd7ab5a,1,user,"Tere! Ma nägin, et Politsei- ja Piirivalveamet on Facebookis. Kas te mulle ökaks, kus on PPA peamine Facebooki leht?",output_Politsei-_ja_Piirivalveamet +c8550aa4-71e5-4e6f-9589-b567fcd7ab5a,2,assistant,"Tere! Jah, PPA on aktiivne Facebookis. PPA peamine Facebooki leht on https://www.facebook.com/politseijapiirivalveamet/. Seal saab leida palju teavet politseitöö kohta ja küsida küsimusi. Kas sulle see leht huuma?",output_Politsei-_ja_Piirivalveamet +ad4d4725-5f92-482c-9641-a80e3666a70c,1,user,"Tere! Ma nägin, et Politsei- ja Piirivalveamet on Facebookis. Kas teil on palju erinevaid Facebooki lehti?",output_Politsei-_ja_Piirivalveamet +ad4d4725-5f92-482c-9641-a80e3666a70c,2,assistant,"Tere! Jah, PPA täidab oma suhtluse strateegiat mitmete Facebooki lehtede kaudu. Peaasi on Põhja, Lääne, Lõuna ja Ida prefektuurid, aga ka PPA end seltsi. Lisaks on olemas PPA fännileht ja Politseikoerte Instagram! Kas sul on mõni leht, mille kohta sa tahad rohkem teada? Näiteks kas sa olid huvitatud Põhja prefektuuri lehest? https://www.facebook.com/pohjaprefektuur",output_Politsei-_ja_Piirivalveamet +1cd110e1-1aec-4ab4-a3eb-2639aeb50dba,1,user,"Tere! Ma elan välismaal ja tahaksin teada, kuidas ma saan Eesti passi taotleda? Mis on kõige lihtsam viis?",output_Politsei-_ja_Piirivalveamet +1cd110e1-1aec-4ab4-a3eb-2639aeb50dba,2,assistant,"Tere! Välismaal on kõige lihtsam passi taotleda kas iseteeninduses aadressil etaotlus.politsei.ee või Eesti Vabariigi välisesinduses. Kas sa soovid, et ma annaksin sulle nimekirja välisesindustest?",output_Politsei-_ja_Piirivalveamet +50e5bb5e-7521-4b66-956b-4729887066a6,1,user,"Tere! Ma elan välismaal ja tahan teada, kuidas ma saan Eesti passi taotleda. Mis on kõige lemmikum viis seda teha?",output_Politsei-_ja_Piirivalveamet +50e5bb5e-7521-4b66-956b-4729887066a6,2,assistant,"Tere! Välismaal saab Eesti passi taotleda kas iseteeninduses (https://etaotlus.politsei.ee/#/login) või Eesti Vabariigi välisesinduses. Kas sa tead, millises välisesinduses sa soovid dokumendi taotleda?",output_Politsei-_ja_Piirivalveamet +2d94d8b4-db98-40bc-8691-9be84db04e86,1,user,"Tere! Ma elan välismaal ja tahan teada, kuidas ma saan Eesti passi taotleda. Mis on kõige lihtsam viis?",output_Politsei-_ja_Piirivalveamet +2d94d8b4-db98-40bc-8691-9be84db04e86,2,assistant,Tere! Välismaal saad passi taotleda kas iseteenindusportaali (https://etaotlus.politsei.ee/#/login) või Eesti Vabariigi välisesinduse kaudu. Milline variant sulle sobivam on?,output_Politsei-_ja_Piirivalveamet +082f985d-08e4-4af2-9d63-eda88970fcfb,1,user,Tere! Mul on küsimus elamisõiguse kohta. Kuidas täpselt seda taotletakse? Kas on mingi formaalnev protsess?,output_Politsei-_ja_Piirivalveamet +082f985d-08e4-4af2-9d63-eda88970fcfb,2,assistant,"Tere! Jah, elamisõiguse taotlus on menetlus. Politsei- ja Piirivalveamet kontrollib, et sa oled esitanud kõik tarvilikud dokumendid. See tähendab, näiteks isikutunnusraamat, kinnituse leping või muu dokument, mis kinnitab teie suhteid. Peamaksud on näiteks dokumentatsioon, mis kinnitab, et te elate koos ja et teil on leping kinnitab teineteise toetust.",output_Politsei-_ja_Piirivalveamet +57a15fce-e005-405b-87eb-09c410a27be4,1,user,"Tere! Ma arvan, et tahaksin teada rohkem elamisõigusest, eriti kui ma soovin elada peresega, kelle kohta on juba elamisõigus tehtud taotlus. Kuidas see menetlus toimub?",output_Politsei-_ja_Piirivalveamet +57a15fce-e005-405b-87eb-09c410a27be4,2,assistant,"Tere! Noh, elamisõiguse menetlemine on algusest lõpuni suhtelooline. Politsei- ja Piirivalveamet (PPA) kontrollib, et taotlus on tehtud nõuetekohaselt ja et kõik elamisõiguse saamiseks vajalikud tingimused on täidetud. See tähendab, et kontrollime näiteks isikutunnistust ja muu dokumentatsiooni. Kui on midagi puudu või vaja rohkem infot, siis teeme teiega ühendust.",output_Politsei-_ja_Piirivalveamet +c7b7e1d4-b7ff-44f2-8747-7e87a2bffce4,1,user,"Tere! Ma kaotasin oma passi ja ma ei tea, kust ma saan uue passi tellida. Kas Põlva politseiosakond saab mulle aidata?",output_Politsei-_ja_Piirivalveamet +c7b7e1d4-b7ff-44f2-8747-7e87a2bffce4,2,assistant,"Tere! Jah, Põlva politseiosakond saab teid aidata. Parim viis passi taotlemiseks on PPA iseteenindus veebilehel: https://www.politsei.ee/et/iseteenindus. Võite ka tulla kohale. Põlva teenindus asub Võru 12, 63308 Põlva.",output_Politsei-_ja_Piirivalveamet +4181236d-dbef-4a78-b0e8-f4dcdcbd420f,1,user,"Tere! Ma olen Põlva maast ja tahan teada, kuidas saaksin passi taotleda. Mis on kõige lihtsam viis?",output_Politsei-_ja_Piirivalveamet +4181236d-dbef-4a78-b0e8-f4dcdcbd420f,2,assistant,Tere! Kõige turvalisem ja kiireim viis dokumendi taotlemiseks on PPA iseteenindus veebilehel: https://www.politsei.ee/et/iseteenindus. Seal saad kogu protsessi algatada. Kas sa soovid ka digikioski kasutamist kaaluda?,output_Politsei-_ja_Piirivalveamet +a67c8ad3-1bcc-4f17-9e7a-4411414854d1,1,user,"Tere! Ma kaalun passi taotlemist. Kas te teate, kuidas ma Põlva politseijaoskonna teeninduses sellega hakkama saan?",output_Politsei-_ja_Piirivalveamet +a67c8ad3-1bcc-4f17-9e7a-4411414854d1,2,assistant,"Tere! Parim ja kiireim viis on kasutada PPA iseteeninduskeskkonda: https://www.politsei.ee/et/iseteenindus. Kui eelistate, saate kasutada digikioski Põlva teeninduses. Teiste võimaluste kohta küsige aga edasi!",output_Politsei-_ja_Piirivalveamet +a5c5743e-2b22-48cd-987d-69c31220b12f,1,user,"Tere! Kuulsin, et Politsei- ja Piirivalveamet annab nõu digiturvalisuse kohta. Millised on kõige olulisemad asjad, mida peaksime enda ja laste turvalisuse tagamiseks silmas pidama?",output_Politsei-_ja_Piirivalveamet +a5c5743e-2b22-48cd-987d-69c31220b12f,2,assistant,"Tere! Kõige olulisem on tarkvara uuendada – nii seadmete kui ka veebilehitsejate. Aegunud tarkvara teeb meid kergesti pahavara ohvriks. Lisaks, paroolide turvalisus on oluline! Kasutame pikki, raskesti äraarvatavaid parole, mis sisaldavad suuri ja väikseid tähti, numbreid ja kirjavahemärke.",output_Politsei-_ja_Piirivalveamet +4e91f779-50b9-4ff6-8c48-08288b8d0833,1,user,"Tere! Olen kuulnud, et digiturvalisus on oluline. Kust ma peaksin alustama enda ja laste turvalisuse tagamise osas?",output_Politsei-_ja_Piirivalveamet +4e91f779-50b9-4ff6-8c48-08288b8d0833,2,assistant,"Tere! See on väga hea küsimus. Alustage tarkvara uuendamisest kõikides seadmetes – telefonis, tabletis, arvutis. Aegunud tarkvaras on turvalisi auke, mida pahavara võib ära kasutada. See on lihtne, aga oluline samm!,",output_Politsei-_ja_Piirivalveamet +1135da90-42b5-4e45-9130-ec6b336d2b0e,1,user,"Tere! Olen kuulnud palju digiturvalisusest, aga ma ei tea, kust ma peaksin alustama. Mis on kõige olulisem asi, mida ma peaksin oma ja laste turvalisuse tagamiseks silmas pidama?",output_Politsei-_ja_Piirivalveamet +1135da90-42b5-4e45-9130-ec6b336d2b0e,2,assistant,"Tere! Alguspunktiks on kindlasti tarkvara uuendamine kõikides seadmetes – telefonides, arvutites, tahvlitel. Aegunud tarkvara on pahavara jaoks lihtne tee sisse. Veelgi olulisem on luua tugevad paroolid. Need peaksid olema pikad, raskesti äraarvatavad ja erinevad igas kontol. Riigi Infosüsteemide Ametil on selle kohta väga hea lehekülg: https://www.ria.ee/et/kuberturvalisus/nouanded/parooli-loomine.html",output_Politsei-_ja_Piirivalveamet +25ef7a6f-0121-483d-99a1-f4d470f632fc,1,user,"Tere! Kas ma saan lapse välismaalase passi taota? Kas pean minema kohapeale? Kuulsin, et teenindused on olukorraseisu kohta",output_Politsei-_ja_Piirivalveamet +25ef7a6f-0121-483d-99a1-f4d470f632fc,2,assistant,"Tere! Jah, lapse välismaalase passi saab kindlasti taota. Teeninduses saad seda teha. Kuigi teeninduste asukohad on avaldatud veebilehel (/et/asukohad/teenindused), on oluline, et teada saada, millises teeninduses on hetkel vähem rahvusvaheliste passi taotluste kohta. Kuna piirkondades võib olukord olla erinev, on hea idee enne kohtumist telefoni teel küsida.",output_Politsei-_ja_Piirivalveamet +a5bbb668-9238-4fec-9f6f-816449189526,1,user,"Tere! Tahaks infot saada, kuidas lapse välismaalase passi taotleda?",output_Politsei-_ja_Piirivalveamet +a5bbb668-9238-4fec-9f6f-816449189526,2,assistant,"Tere! Noh, põhiline on see, et lapse välismaalase passi taotlemine toimub teeninduses. Alustame kõigepealt sellega, et leiate sobiva teeninduskoja (nt PPA kontor). Ja siis on vaja isikut tõendava dokumendi, aga lapsele peab ju tohtivõti olema. Kas teil on küllagi selge, kus ja kuidas seda teenindust leida? Kas teadate, kus PPA kontorid asuvad?",output_Politsei-_ja_Piirivalveamet +994ed3d6-9062-4bcf-9d3c-146cea681423,1,user,Tere! Kas ma saan lapse välismaalase passi taotleda Politsei- ja Piirivalveametist?,output_Politsei-_ja_Piirivalveamet +994ed3d6-9062-4bcf-9d3c-146cea681423,2,assistant,"Muidugi! Jah, saab. Aga kõigepealt on oluline teada, kus ja kuidas seda kõige paremini teha. Kui laps on alla 18aastane, siis passi taotlemine toimub tavaliselt Politsei- ja Piirivalveametis. Küll aga on oluline mõelda, kust taotlus kõige paremini vastuvõetakse ja mida sinu lapse puhul tähendab kiireks tegemiseks (kiire kohalviisa).",output_Politsei-_ja_Piirivalveamet +d4ad487e-8beb-440a-8026-5c978eea51f2,1,user,Tere! Ma tahan küsida Kuressaare arestimaja kohta. Kas sinna saab pakke tuua?,output_Politsei-_ja_Piirivalveamet +d4ad487e-8beb-440a-8026-5c978eea51f2,2,assistant,Tere! Kuressaare arestimajas ei saa arestalustele pakke tuua. Vahistatule aga lubatakse üks pakk kuus alates 01.08.2020.,output_Politsei-_ja_Piirivalveamet +634b55a8-7d97-4841-9ed7-cd3a1e793d4a,1,user,"Tere! Ma tahaks teada, kas Kuressaare arestimajasse saab saata pakke sugulastele või sõpradele, kes seal viibivad?",output_Politsei-_ja_Piirivalveamet +634b55a8-7d97-4841-9ed7-cd3a1e793d4a,2,assistant,Tere! Kuressaare arestimajasse ei tohi pakke üldse tuua. Arestialustele neid lubatud ei ole.,output_Politsei-_ja_Piirivalveamet +9e24a88c-a1ad-4e81-9999-4665230cbf06,1,user,Tere! Ma tahan küsida Kuressaare arestimajast. Kas sinna saab pakke toimetada?,output_Politsei-_ja_Piirivalveamet +9e24a88c-a1ad-4e81-9999-4665230cbf06,2,assistant,Tere! Kuressaare arestimajja ei tohi arestalustele pakke tuua. Vahistatule on aga lubatud üks pakk kalendrikuus.,output_Politsei-_ja_Piirivalveamet +4005dbf6-4e92-44bb-903f-72a951ea1951,1,user,"Tere! Ma uurisin Politsei ja Piirivalve ameti veebilehel infot lapse kodakondsuse kohta. Kuidas saan teada, millal saan lapse kodakondsustunnistuse?",output_Politsei-_ja_Piirivalveamet +4005dbf6-4e92-44bb-903f-72a951ea1951,2,assistant,"Tere! Kui laps saab Eesti kodakondsuse, saadetakse kodakondsustunnistus teile posti teel. Täpne ajakavast sõltub menetluse kiirusest, aga tunniste tulek postiga on tavapärane viis dokumentide edastamine.",output_Politsei-_ja_Piirivalveamet +0c95e755-4dc5-48d3-b3f0-32718d1091fa,1,user,"Tere! Ma just sain teada, et meil on uus beebi. Mida me peame nüüd kodakondsuse osas tegema?",output_Politsei-_ja_Piirivalveamet +0c95e755-4dc5-48d3-b3f0-32718d1091fa,2,assistant,"Õnne! Kui teie laps saab Eesti kodakondsuse, siis teile saadetakse posti teel kodakondsustunnistus. See on tähtis dokument! Meil on Politsei- ja Piirivalveamet vastutab selle väljastamise.",output_Politsei-_ja_Piirivalveamet +362e95d1-a3df-4610-af28-1b9fe7debe2c,1,user,"Tere! Ma olen uudishimulik, kuidas Eesti kodakondsus lapsele antakse. Saabus mulle kiri selle kohta, aga ma ei tea, mida edasi teha.",output_Politsei-_ja_Piirivalveamet +362e95d1-a3df-4610-af28-1b9fe7debe2c,2,assistant,"Tere! Hästi, siis sa oled saanud kirja selle kohta. Kui lapsele Eesti kodakondsus antakse, siis tavaliselt saadakse selle kohta teada posti teel saadetud kodakondsustunnistuse näol.",output_Politsei-_ja_Piirivalveamet +2b8ef781-52ac-4129-b367-66f0834cdb87,1,user,"Tere! Ma just saime Digi-ID ja ma natuke muretsen, mida teha, kui kaart kaotaks. Mida ma peaks esimesena tegema?",output_Politsei-_ja_Piirivalveamet +2b8ef781-52ac-4129-b367-66f0834cdb87,2,assistant,"Tere! See on täiesti normaalne. Esiteks, kui kaart on kadunud või varastatud, helista kohe numbrile 677 3377, et sertifikaadid peatada. See on oluline, et keegi ei saaks sinu nimele toiminguid teha.",output_Politsei-_ja_Piirivalveamet +dc93d66d-5196-4340-9a2f-3ad99604a745,1,user,"Tere! Ma olen kuulnud Digi-ID taotlemisest, aga ma ei tea, kust ma alustama peaksin. Mis on esimene asi, mida ma peaksin tegema?",output_Politsei-_ja_Piirivalveamet +dc93d66d-5196-4340-9a2f-3ad99604a745,2,assistant,Tere! Digi-ID taotlemiseks peate esmalt pöörduma Politsei- ja Piirivalveameti teenindusse. Seal saate taotlusprotsessi käima panna. Täpsem info asukohad on saadaval aadressil https://www.politsei.ee/et/asukohad/teenindused.,output_Politsei-_ja_Piirivalveamet +a4bf2347-316a-4516-a53a-8f62fd1bdb85,1,user,Tere! Ma kaotanud oma digi-ID kaardi. Mida ma peaksin tegema?,output_Politsei-_ja_Piirivalveamet +a4bf2347-316a-4516-a53a-8f62fd1bdb85,2,assistant,"Oh, see on kahjuks mitteideaalne olukord. Esimene asi, mida pead tegema, on helistada numbril 677 3377, et sertifikaadid peatada. See takistab kellegi muu kasutamas su digi-IDd. Seejärel saad minna Politsei- ja Piirivalveameti teenindusse või Eesti Vabariigi välisesindusse, et kas sertifikaadid taas aktiveerida, kui leidsid dokumendi, või taotle uut dokumenti.",output_Politsei-_ja_Piirivalveamet +b77b1d38-3d0e-408f-bfc5-40ad8c0c07f7,1,user,"Tere! Kas te oskate öelda, kus asub Rakvere politseijaoskond?",output_Politsei-_ja_Piirivalveamet +b77b1d38-3d0e-408f-bfc5-40ad8c0c07f7,2,assistant,"Muidugi! Rakvere politseijaoskond asub aadressil Fr. R. Kreutzwaldi 5a, 44314 Rakvere. See on suht mugav, kust bussid nr 5 ja 5A peatuvad.",output_Politsei-_ja_Piirivalveamet +049919d1-68f9-4973-89e3-9f76afda23a0,1,user,"Tere! Kas te saate öelda, kus Rakvere politseijaoskond asub?",output_Politsei-_ja_Piirivalveamet +049919d1-68f9-4973-89e3-9f76afda23a0,2,assistant,"Muidugi! Rakvere politseijaoskond asub aadressil Fr. R. Kreutzwaldi 5a, 44314 Rakvere. See on väga kerge kohas leida, järgige peatus „Teater“ bussid nr 5 või 5A! Kas sulle täpsem info selle kohta vaja on?",output_Politsei-_ja_Piirivalveamet +f0cb2432-685b-4b49-b01f-4e72dbed1b91,1,user,"Tere! Kas te saate öelda, kus asub Rakvere politseijaoskond?",output_Politsei-_ja_Piirivalveamet +f0cb2432-685b-4b49-b01f-4e72dbed1b91,2,assistant,"Muidugi! Rakvere politseijaoskond asub aadressil Fr. R. Kreutzwaldi 5a, 44314 Rakvere. See on väga lihtne kohaks leida!",output_Politsei-_ja_Piirivalveamet +ca543460-33d8-4c9d-90cd-8298dc5ed66f,1,user,"Tere! Mul on plaanil avalik üritus korraldada, ja mul on palju küsimusi. Alustaksime põhjaosas. Kuidas ma peaksin seda kõigepealt kohalikele omavalitsustele teavitama?",output_Politsei-_ja_Piirivalveamet +ca543460-33d8-4c9d-90cd-8298dc5ed66f,2,assistant,"Tere! See on väga hea, et alustate etteval valmist. Avaliku ürituse korraldamiseks peab teid kõigepealt konsulteerima kohaliku omavalitsusega. Kuigi eelnimetatud üritus ei ole koosolek, tuleb sellest teatada kohalikule omavalitsusele, kelle piires soovite ürituse korraldada. Palun külastage oma kohaliku omavalitsuse kodulehte, et leida taotlusvorm ja lisateave. Nad hindavad taotlust ja vajadusel kaasavad Politsei- ja Piirivalveameti, Päästeameti ja/või Tarbijakaitse ja Tehnilise Järelevalve Ameti loa kooskõlastamisse.",output_Politsei-_ja_Piirivalveamet +e19ebf3e-a8ce-458e-a9cc-882c07a9b582,1,user,"Tere! Ma plaanisin avaliku ürituse korraldada. Mis on kõige tähtsam, mida pean silmas pidama, et Politsei- ja Piirivalveamet sellega toetaks?",output_Politsei-_ja_Piirivalveamet +e19ebf3e-a8ce-458e-a9cc-882c07a9b582,2,assistant,"Tere! See on väga hea küsimus! Avaliku ürituse korraldamisel on mitmeid olulisi asju, millele tasub tähelepanu juhtida. Esiteks, pead pöörduma avaliku ürituse toimumise kohalikku omavalitsusse ja saama nende poolt luba. Neil on kohustus kontrollida, kas üritus kohalikele elanikele ei tekita liigset olumajanduskoormust. Kui sa saad luba, Politsei- ja Piirivalveamet soovivad, et saaksid ka ürituse korraldamisega algust juba ette mõelda. Kas sa saaksid selgeks, millest muidu peaksime arutama?",output_Politsei-_ja_Piirivalveamet +24dbe480-4c65-4929-812a-9399340d88fd,1,user,"Tere! Kas te muldite, kuidas avalikku üritust korraldada Eestis? Aga täpselt Politsei- ja Piirivalveameti poolt.",output_Politsei-_ja_Piirivalveamet +24dbe480-4c65-4929-812a-9399340d88fd,2,assistant,"Tere! Muidugi, ma saan teid selles aidata. Avaliku ürituse korraldamiseks peate esmalt konsulteerima kohaliku omavalitsusega, kelle piires soovite ürituse korraldada. Nad annavad luba ja hindavad, et üritus ei põhusta probleeme. Nende koduleht on parim koht, et leida nende kontaktid ja taotlusvormi. Kas soovite teada, mis need nõuded kohalikel omavalitsustel on?",output_Politsei-_ja_Piirivalveamet +ffcc68d7-ac93-489b-a9fb-b2e0877891a3,1,user,"Tere! Ma just sain ID-kaardi ja tahan teada, mida ma pean sellega seoses meeles pidama. Mis on kõige olulisem?",output_Politsei-_ja_Piirivalveamet +ffcc68d7-ac93-489b-a9fb-b2e0877891a3,2,assistant,"Tere! Kõige olulisem on hoia PIN-koode salajas ja turvalises kohas. Ära anna neid kellegi teise kätte ja proovi need pähe õppida, kui võimalik.",output_Politsei-_ja_Piirivalveamet +a2db8b61-bb00-4b26-8c46-fcf5014827cf,1,user,"Tere! Mul on küsimus ID-kaardi kohta. Ma tahaks teada, mida ma pean tegema, kui kaotan ID-kaardi.",output_Politsei-_ja_Piirivalveamet +a2db8b61-bb00-4b26-8c46-fcf5014827cf,2,assistant,"Tere! Oluline on kohe tegutseda. Esimene asi, mida peaks tegema, on helistada numbril 677 3377, et sertifikaadid peatada. See takistab kellegi teise oma isik andmetega kuritarvitamist. Kas sa tead, kust seda infot leidsid?",output_Politsei-_ja_Piirivalveamet +0152a364-fdf2-4451-af70-6bd1e455f957,1,user,"Tere! Mul on küsimusi ID-kaardi kasutamise kohta. Mis on olulisim asi, mida ma pean meeles pidama, et seda turvaliselt kasutada?",output_Politsei-_ja_Piirivalveamet +0152a364-fdf2-4451-af70-6bd1e455f957,2,assistant,"Tere! Olulisim asi on kindlasti hoia oma PIN-koode salajas ja turvalises kohas. Võimaluse korral õpi need pähe, et sa ei peaks PIN-koodi üles kirjutama ja seda edastama. Ära anna ka oma ID-kaarti kellegi teise kätte!",output_Politsei-_ja_Piirivalveamet +fbb9c698-e43f-4401-89d8-857ca059aa84,1,user,Tere! Ma plaanin meresõidutunnistust taotleda. Kuidas ma riigilõivu maksmiseks pean tegema?,output_Politsei-_ja_Piirivalveamet +fbb9c698-e43f-4401-89d8-857ca059aa84,2,assistant,"Tere! Riigilõivu tasumiseks pead saama vajaliku info. Saaja on Rahandusministeerium. Viitenumber on 2900082443. Pangakontod on erinevad: SEB: EE891010220034796011, Swedbank: EE932200221023778606, Luminor Bank: EE701700017001577198 ja LHV Pank: EE777700771003813400.",output_Politsei-_ja_Piirivalveamet +aacdc7b2-046e-4d7a-a6d2-ef50cb1d8e28,1,user,"Tere! Ma tahan meresõidutunnistust taotleda, aga ma ei teagi, kuidas riigilõivu maksta. Kust ma alustan?",output_Politsei-_ja_Piirivalveamet +aacdc7b2-046e-4d7a-a6d2-ef50cb1d8e28,2,assistant,"Tere! Riigilõivu tasumiseks on mitu võimalust. Saaja on Rahandusministeerium. Viitenumber on 2900082443. Pangad, kuhu sa selle maksta saad, on SEB, Swedbank, Luminor Bank ja LHV Pank. Nüüd, mis pank sulle kõige mugavam on?",output_Politsei-_ja_Piirivalveamet +083f8fb6-5200-40a1-8426-70a3be8d4afd,1,user,"Tere! Ma olen huvitatud, kus Paides asub Kesk-Eesti politseijaoskond?",output_Politsei-_ja_Piirivalveamet +083f8fb6-5200-40a1-8426-70a3be8d4afd,2,assistant,"Tere! Kesk-Eesti politseijaoskond Paides asub aadressil Tallinna 12, 72720 Paide.",output_Politsei-_ja_Piirivalveamet +cc12bb15-2952-4b9c-8737-7e84a97d9ff1,1,user,"Tere! Ma kahtlen, kuidas Paides asuva Kesk-Eesti politseijaoskonna töösüsteem on korraldatud. Saaksid sa mulle natuke infot anda?",output_Politsei-_ja_Piirivalveamet +cc12bb15-2952-4b9c-8737-7e84a97d9ff1,2,assistant,"Tere! Kesk-Eesti politseijaoskond Paides asub Tallinnas 12, postikoht 72720. Nad on avatud esmaspäevast reedeni, kell 9.00 kuni 17.00 igal tööpäeval.",output_Politsei-_ja_Piirivalveamet +de4477da-8837-4621-a6af-878b37ff3e6b,1,user,"Tere! Ma just kahtlen, kuidas Paides asuva Kesk-Eesti politseijaoskonna leida. Kas te teate selle aadressi?",output_Politsei-_ja_Piirivalveamet +de4477da-8837-4621-a6af-878b37ff3e6b,2,assistant,"Tere! Kesk-Eesti politseijaoskond Paides asub aadressil Tallinna 12, 72720 Paide.",output_Politsei-_ja_Piirivalveamet +0ec5fc59-21c3-40e8-baa8-a4e99d485e08,1,user,Tere! Mul on küsimus elamisloa pikendamise kohta. Ma õpin siin Eestis ja mu elamisluba lõppeb peagi. Millal ma peaksin selle pikendamist taotlema?,output_Politsei-_ja_Piirivalveamet +0ec5fc59-21c3-40e8-baa8-a4e99d485e08,2,assistant,"Tere! Elamisloa pikendamist õppimiseks peaksid saama esitada vähemalt 2 kuud enne kehtiva elamisloa lõppemist. See tagab, et saaksid vastuse õigeaegselt.",output_Politsei-_ja_Piirivalveamet +dd9c24e4-99da-4ece-9b2b-b14773694693,1,user,Tere! Mul on küsimus elamisloa pikendamise kohta. Ma õpin Eestis ja mu elamisluba õppimiseks lõppeb pea olemas. Mis ma pean tegema?,output_Politsei-_ja_Piirivalveamet +dd9c24e4-99da-4ece-9b2b-b14773694693,2,assistant,"Tere! Elamisloa pikendamiseks õppimiseks pead saama taotluse esitada vähemalt 2 kuud enne kehtiva loa lõppemist. Pead esitama mitmeid dokumente ja tasuma riigilõivu. Kas sa soovid, et ma mainiksin täpsemalt, milliseid dokumente vaja on?",output_Politsei-_ja_Piirivalveamet +5ea8a36d-8aa4-4c1e-bb1f-57ad6b342b2b,1,user,"Tere! Mul on küsimus elamisloa kohta. Mis juhtub, kui minu elamisluba täisealisele lapsele pereliikme juurde elama asumiseks lõppeb?",output_Politsei-_ja_Piirivalveamet +5ea8a36d-8aa4-4c1e-bb1f-57ad6b342b2b,2,assistant,"Tere! Kui sinu elamisluba lõppeb, saad Eestisse jääda veel kuni 270 päevaks. Sel perioodil on sul ka võimalus Eestis töötada.",output_Politsei-_ja_Piirivalveamet +24b10167-b657-4cc3-b13e-659108658a55,1,user,"Tere! Mul on küsimus elamisloa kohta. Mis juhtub, kui mu elamisluba täisealisele lapsele pereliikme juurde elama asumise põhjal lõppeb?",output_Politsei-_ja_Piirivalveamet +24b10167-b657-4cc3-b13e-659108658a55,2,assistant,"Tere! Kui sinu elamisluba lõppeb, saad sa Eestisse veel jääda kuni 270 päevaks. Sel ajal on sul ka võimalus Eestis töötada.",output_Politsei-_ja_Piirivalveamet +3b766720-90e1-4b95-a0d8-fe5c76d810c3,1,user,"Tere! Mul on küsimus elamisloa kohta. Mis juhtub, kui mu elamisluba, mis on mul täisealise lapsega siia pere juurde elama asumise põhjal, lõppeb?",output_Politsei-_ja_Piirivalveamet +3b766720-90e1-4b95-a0d8-fe5c76d810c3,2,assistant,"Tere! Kui sinu elamisluba lõppeb, saad Eestisse jääda veel kuni 270 päeva. Sel ajal on sul ka võimalus Eestis töötada. See on tähtis teada, et saaksid asjad korralikult planeerida.",output_Politsei-_ja_Piirivalveamet +5736a0dc-b4f6-4145-9621-3211e9ea8229,1,user,"Tere! Ma soovin tööle minna Eestis ettevõtluse pidamiseks, aga mul pole hetkel Eestis elamisloa. Kuidas saan selle taotleda?",output_Politsei-_ja_Piirivalveamet +5736a0dc-b4f6-4145-9621-3211e9ea8229,2,assistant,"Tere! Palju õnne otsusega Eestis ettevõtlust alustada! Elamisluba ettevõtluseks saab taotleda, kui investeerid Eestis oma äriühingusse. Esiteks pead investeerima. Kas sa oled juba seda tegema planeerinud? Ja siis pead kohutama nõutud dokumentidele ja riigilõive.",output_Politsei-_ja_Piirivalveamet +5f611ca8-13aa-4405-9924-a8b44382f307,1,user,"Tere! Ma mõtlen, kuidas Eestisse ettevõtlasena elama jääda. Kas ma saan elamisluba ettevõtluseks?",output_Politsei-_ja_Piirivalveamet +5f611ca8-13aa-4405-9924-a8b44382f307,2,assistant,"Tere! Jah, seda on võimalik. Ettevõtluseks elamisluba saamiseks pead investeerima Eestisse oma äriühingusse. Kas sa juba tead, millisega äri tegeleda või on sul äriidee veel arenduses?",output_Politsei-_ja_Piirivalveamet +e616de12-cf5a-4ef7-8276-17b0e9726e61,1,user,Tere! Mul on küsimus elamislubade kohta. Kas ma saan Eestis ettevõtluseks elamisloa?,output_Politsei-_ja_Piirivalveamet +e616de12-cf5a-4ef7-8276-17b0e9726e61,2,assistant,"Tere! Jah, seda saab. Kui soovite Eestis ettevõtluseks elamisloa, siis see on võimalik. See nõuab investeeringut Eestis oma äriühingusse. Mida veel täpsemalt teada peaksite?",output_Politsei-_ja_Piirivalveamet +de2d986f-d611-483e-aec7-5d3f516bd330,1,user,"Tere! Ma soovisin teada, kus Ida-Harju politseijaoskond asub? Kas see on Tallinnas?",output_Politsei-_ja_Piirivalveamet +de2d986f-d611-483e-aec7-5d3f516bd330,2,assistant,"Jah, Ida-Harju politseijaoskond asub Tallinnas, aadressil P. Pinna 4, 13615 Tallinn.",output_Politsei-_ja_Piirivalveamet +a2757f01-1029-4def-94fe-a9dc868441d9,1,user,Tere! Ma olen kuulnud Ida-Harju politseijaoskonnast. Kus see asub täpselt?,output_Politsei-_ja_Piirivalveamet +a2757f01-1029-4def-94fe-a9dc868441d9,2,assistant,"Tere! Ida-Harju politseijaoskond asub aadressil P. Pinna 4, 13615 Tallinn.",output_Politsei-_ja_Piirivalveamet +ec42b1d6-5664-427b-8bcd-22b5f6cf494b,1,user,"Tere! Ma tahan teada, kuidas Ida-Harju politseijaoskonda saab. Kus see asub?",output_Politsei-_ja_Piirivalveamet +ec42b1d6-5664-427b-8bcd-22b5f6cf494b,2,assistant,"Tere! Ida-Harju politseijaoskond asub aadressil P. Pinna 4, 13615 Tallinn.",output_Politsei-_ja_Piirivalveamet +99dda6ed-5bbf-4058-826c-078daac4fd85,1,user,"Tere! Ma kuulsin, et piiri tugevdamiseks on rajatud lõiketraati. Kas see ei mõju metsloomadele? Palju nad sellega hädaldavad?",output_Politsei-_ja_Piirivalveamet +99dda6ed-5bbf-4058-826c-078daac4fd85,2,assistant,"Tere! See on väga hea küsimus. Tegelikult on seda küsimust juba arutletud. Mure on küll sees, et lõiketraat võib loomade liikumist piirata. Kuid politsei on Keskkonnaametiga kooskõlastanud, kuidas olukorda lahendada, kui loomad juhtu takerduma.",output_Politsei-_ja_Piirivalveamet +1630e57f-c4ff-45b3-b331-f26028943a23,1,user,"Tere! Olen kuulnud, et piiri tugevdamisel kasutatakse lõiketraati. Kas see ei mõju metsloomadele? Mida politsei selleks teeb?",output_Politsei-_ja_Piirivalveamet +1630e57f-c4ff-45b3-b331-f26028943a23,2,assistant,"Tere! See on väga hea küsimus. Jah, lõiketraadi rajamine võib mõjutada metsloomade liikumist. Politsei on teadlik sellest probleemist ja on Keskkonnaametiga konsulteerinud, kuidas sellega tegeleda. Üldiselt on väiksemad loomad suutelised lõiketraadist läbi pääsema, kuid suuremad loomad võivad takerduda.",output_Politsei-_ja_Piirivalveamet +57560250-f963-43dc-8c1b-06d50b13889c,1,user,"Tere! Olen kuulnud, et piiri tugevdamisel on kasutatud lõiketraati. Kas see ei mõju metsloomadele? On neil raskusi sellega? ",output_Politsei-_ja_Piirivalveamet +57560250-f963-43dc-8c1b-06d50b13889c,2,assistant,"Tere! Jah, see on küllaltki oluline küsimus. Paljud kohalikud ja jahimehed on murelikud. Üldjuhul on väikesed loomad piisavalt paindlikud, et lõiketraadist läbi pääseda. Kuid suuremate loomade puhul on ohtu, et nad sellega kinni jooksevad.",output_Politsei-_ja_Piirivalveamet +7ff36ce6-0297-4ba3-956c-9ee181dd9b58,1,user,"Tere! Ma pean külastama Jõgeva politseijaoskonda. Kas sa tead, kus see asub?",output_Politsei-_ja_Piirivalveamet +7ff36ce6-0297-4ba3-956c-9ee181dd9b58,2,assistant,"Tere! Jõgeva politseijaoskond asub aadressil Suur 1, 48306 Jõgeva.",output_Politsei-_ja_Piirivalveamet +223a6cae-8637-4e5b-8614-41612d7754ac,1,user,"Tere! Ma kaotanud oma isikutunnistuse ja tahan teada, kust ma saan selle kohta politseisse pöörduda Jõgeva läheduses.",output_Politsei-_ja_Piirivalveamet +223a6cae-8637-4e5b-8614-41612d7754ac,2,assistant,"Tere! Jõgeva politseijaoskond asub aadressil Suur 1, 48306 Jõgeva. Nad on avatud esmaspäevast reedeni kell 9.00-17.00. Kas saaksite mulle kinnitada, kas see on teile sobiv asukoht?",output_Politsei-_ja_Piirivalveamet +ac84cbfb-dde9-42c6-9952-5aa30fcdd2b2,1,user,Tere! Ma kaotanud oma isikut tõendava dokumendi. Kus ma saaksin Jõgeva politseioskonda leida?,output_Politsei-_ja_Piirivalveamet +ac84cbfb-dde9-42c6-9952-5aa30fcdd2b2,2,assistant,"Tere! Jõgeva politseioskond asub aadressil Suur 1, postikoht 48306 Jõgeva. See on kõige lähedasem koht, kus sa saad oma dokumendi kaotamise kohta avalduse esitada.",output_Politsei-_ja_Piirivalveamet +42ee09fa-7e8d-4782-a181-85361d139b39,1,user,Tere! Ma tahan lapsele ID-kaarti taotleda. Kuidas ma riigilõivu maksta saan?,output_Politsei-_ja_Piirivalveamet +42ee09fa-7e8d-4782-a181-85361d139b39,2,assistant,"Tere! ID-kaardi taotlemisel lapsele riigilõivu tasumisel on mitmeid võimalusi. Kui taotled iseteeninduses, saad riigilõivu tasuda otse sealsamas. Kui aga teed pangaülekande, siis saaja on Rahandusministeerium, viitenumber on 2900082443. Kontopõhjad on erinevate pankade jaoks erinevad: SEB EE891010220034796011, Swedbank EE932200221023778606, Luminor Bank EE701700017001577198, LHV Pank EE777700771003813400.",output_Politsei-_ja_Piirivalveamet +3b02b4d9-066d-46e2-8029-c9d0de286ab6,1,user,Tere! Ma tahan lapsele ID-kaarti taotleda. Kuidas riigilõivu maksta?,output_Politsei-_ja_Piirivalveamet +3b02b4d9-066d-46e2-8029-c9d0de286ab6,2,assistant,"Tere! ID-kaardi taotlemisel lapsele on mitmeid võimalusi. Kui taotled iseeningukeskuses, saad riigilõivu koheselt seal tasuda. Kui maksad pangaülekandega, saadaval on erinevad panga kontod. Kas soovid, et ma need kontod loetle?",output_Politsei-_ja_Piirivalveamet +615b6c9a-8204-408d-9960-65ed1f2991dd,1,user,Tere! Ma tahan lapsele ID-kaarti taotleda. Kuidas ma selle riigilõivu kõige paremini maksta saan?,output_Politsei-_ja_Piirivalveamet +615b6c9a-8204-408d-9960-65ed1f2991dd,2,assistant,"Tere! ID-kaardi taotlemisel iseteeninduses saad riigilõivu koheselt tasuda, pole vaja eraldi pangaülekannet. Kui aga soovid pangast maksta, siis saaja on Rahandusministeerium, viitenumber on 2900082443. Selgitusse märgi „ID-kaardi taotlemine“ ja lapse nimi, isikukood või sünniaeg. Kas soovid, et ma sulle pangakontode numbreid ütleksin?",output_Politsei-_ja_Piirivalveamet +1e12aec6-d800-4a59-9e1e-e29a0f630fd5,1,user,Tere! Kuulisin midagi ID-kaartide sertifikaatide kohta. Mis asi see tähendab ja miks see oluline on?,output_Politsei-_ja_Piirivalveamet +1e12aec6-d800-4a59-9e1e-e29a0f630fd5,2,assistant,"Tere! Õige, alates 1. aprillist 2018 tunnistati turvanõrkusega ID-kaardi sertifikaadid kehtetuks. See tähendab, et kui sa ei uuendanud oma ID-kaardi sertifikaate 31. märtsiks 2018, siis elektrooniliselt ei saa seda enam kasutada nii nagu varem.",output_Politsei-_ja_Piirivalveamet +6147dbf9-4603-4007-b2bc-a85cbcc79618,1,user,Tere! Ma kuulsin midagi ID-kaartide sertifikaatide kohta. Mis juhtus?,output_Politsei-_ja_Piirivalveamet +6147dbf9-4603-4007-b2bc-a85cbcc79618,2,assistant,"Tere! Jah, 2018. aasta 1. aprillist tunnistati turvanõrkusega ID-kaardi sertifikaadid kehtetuks. See tähendab, et kui sa neid ei uuendanud 31. märtsiks 2018, siis on need sertifikaadid nüüd kasutuselt maha võetud.",output_Politsei-_ja_Piirivalveamet +03bd0229-4d40-4e0b-9033-7f717d790cb7,1,user,"Tere! Kuulsin, et ID-kaartidega on midagi juhtunud. Mis siis täpselt on juhtunud sertifikaatidega?",output_Politsei-_ja_Piirivalveamet +03bd0229-4d40-4e0b-9033-7f717d790cb7,2,assistant,"Tere! Jah, 2018. aasta 1. aprillist tunnistati turvanõrkusega ID-kaardi sertifikaadid kehtetuks. See tähendab, et kui sa ei uuendanud neid 31. märtsiks 2018, siis elektrooniliselt ID-kaarti enam ei saa kasutada.",output_Politsei-_ja_Piirivalveamet +904feb12-fc56-409b-84cc-175e5309b6cc,1,user,Tere! Ma plaanin elamisloakaarti taotleda. Kus ma teenindusse ma peaksin minema ja mida ma kaasa võtma peaksin?,output_Politsei-_ja_Piirivalveamet +904feb12-fc56-409b-84cc-175e5309b6cc,2,assistant,Tere! Elamisloakaardi taotlemiseks saad valida sobiva teeninduse kohta Politsei- ja Piirivalveameti veebilehelt. Kindlasti kaasa isikut tõendav dokument. Sa saad aja broneerida veebilehel https://broneering.politsei.ee/ kui soovid.,output_Politsei-_ja_Piirivalveamet +542a956b-3bb2-4438-b01e-5697455abce8,1,user,Tere! Mul on küsimus elamisloakaardi taotlemise kohta. Kuidas ma üldiselt sellele protsessile alustan?,output_Politsei-_ja_Piirivalveamet +542a956b-3bb2-4438-b01e-5697455abce8,2,assistant,Tere! Elamisloakaardi taotlemiseks pead sul olema sobiv teenindus ja isikut tõendav dokument. Sa saad teeninduses aega broneerida või tulla lihtsalt kohale. Broneeringu sooritamiseks vaata seda linki: https://broneering.politsei.ee/,output_Politsei-_ja_Piirivalveamet +ffd55367-9f17-415b-bc4a-1cd45aa8b487,1,user,Tere! Ma tahaksin elamisloakaarti taotleda. Kust ma selleks pöörata saaksin ja mida ma kaasa võtta tuleks?,output_Politsei-_ja_Piirivalveamet +ffd55367-9f17-415b-bc4a-1cd45aa8b487,2,assistant,Tere! Elamisloakaardi taotlemiseks saad pöörduda Politsei- ja Piirivalveameti teenindusse. Kindlasti võta kaasa isikut tõendav dokument. Soovi korral saad aja ka broneerida nende veebilehel: https://broneering.politsei.ee/,output_Politsei-_ja_Piirivalveamet +a14c1e10-3908-4057-8ad2-1e5e88c92456,1,user,"Tere! Ma lugesin Politsei- ja Piirivalveameti veebilehe ligipääsetavuse teatisest. Mis see üldse tähendab, et veebileht on osaliselt vastavuses ligipääsetavusnõuetele?",output_Politsei-_ja_Piirivalveamet +a14c1e10-3908-4057-8ad2-1e5e88c92456,2,assistant,"Tere! See tähendab, et Politsei- ja Piirivalveameti veebileht www.politsei.ee ei vasta täielikult kõikidele ligipääsetavusnõuetele. See vastab neile osaliselt, aga teatud funktsioonid ja sisud ei ole veel täies mahus ligipääsetavad kõigile kasutajatele. Seda on tehtud välispartneri hindamise põhjal.",output_Politsei-_ja_Piirivalveamet +10247c96-a4d7-4866-a5b5-8409e9d27ed4,1,user,Tere! Ma nägin Politsei- ja Piirivalveameti veebilehel mingit ligipääsetavuse teatis. Mis see tähendab?,output_Politsei-_ja_Piirivalveamet +10247c96-a4d7-4866-a5b5-8409e9d27ed4,2,assistant,"Tere! Ligipääsetavuse teatis on avalik dokument, mis teavitab kasutajaid veebilehe ligipääsetavuse olukorra kohta. See tähendab, et seal on kirjas, kui hästi on veebileht tehtud nii, et seda saaks kasutada inimestel, kel on näiteks puuete või erinevate vajadustega.",output_Politsei-_ja_Piirivalveamet +aae84c6b-f646-4bf9-bf89-272f5b149ab7,1,user,Tere! Ma nägin Politsei- ja Piirivalveameti veebilehel mingit ligipääsetavuse teatis. Mis see tähendab?,output_Politsei-_ja_Piirivalveamet +aae84c6b-f646-4bf9-bf89-272f5b149ab7,2,assistant,"Tere! See teatis on seadusest tulenev avaldus, mis kinnitab, et Politsei- ja Piirivalveamet püüab oma veebilehte www.politsei.ee (https://www.politsei.ee/) muuta ligipääsetavaks kõigile, kaasa arvatud inimestele, kel on puueteid. See teatis ütleb ka, mis on selle hetke olukord ligipääsetavuse osas.",output_Politsei-_ja_Piirivalveamet +999f48fa-031a-4b56-b11d-ee99b8ed69c4,1,user,Tere! Ma olen veidi segadus ID-kaardi taotlemisel. Kuidas liikumisvõimetule inimesele ID-kaart taotleda saab? Mis on kõige lihtsam viis?,output_Politsei-_ja_Piirivalveamet +999f48fa-031a-4b56-b11d-ee99b8ed69c4,2,assistant,"Tere! Liikumisvõimetule inimesele on mitmeid võimalusi ID-kaardi taotlemiseks. Iseteeninduses saab taotleda, kui te olete täiskasvanu või alaealine laps, ja teil on ID-kaart, mobiil-ID või Smart-ID. Seal peate üles laadima foto ja maksma riigilõivu pangalingi kaudu. Kas teil on iseteenindusega kogemust?",output_Politsei-_ja_Piirivalveamet +dc5255fa-8070-4a26-9ce5-17addb0bda3a,1,user,Tere! Ma olen liikumisvõimetu ja tahan ID-kaarti taotleda. Kust ma peaksin alustama?,output_Politsei-_ja_Piirivalveamet +dc5255fa-8070-4a26-9ce5-17addb0bda3a,2,assistant,"Tere! Esimese samuna saad ID-kaarti taotleda kas iseteeninduses, posti teel või kui sind aitab sotsiaaltöötaja. Iseteenindus on lihtsam, kui suudad ise dokumenditaotluse esitada ja tasuda riigilõivu. Posti teel pead täitma ankeedi ja saama dokumendifoto. Kui sind aitab sotsiaaltöötaja, siis on protsess teine.",output_Politsei-_ja_Piirivalveamet +3872ae6e-07cc-4ba7-ac9b-3291da2685db,1,user,"Tere! Ma olen liikumisvõimetu ja tahaksin teada, kuidas ma ID-kaarti taotleda saan. Kas mul peab sõrmejäljed andma?",output_Politsei-_ja_Piirivalveamet +3872ae6e-07cc-4ba7-ac9b-3291da2685db,2,assistant,"Tere! Küsimus on hea. Alates 12. eluaastast on ID-kaardi taotlemiseks vaja sõrmejäljed. Kui te olete üle 70-aastane ja teilt on varem sõrmejäljed võetud, siis uut dokumenti saate taotleda ilma uusi sõrmejälgi andmata. Muudel juhtudel, kui sõrmejäljed on vähem kui 11 aastat tagasi hõivatud, ka siis uusi anda ei pea. Kui sõrmejälgede andmine on vajalik, aga te ei saa ise teenindusse tulla, saadame sõrmejälgede hõivamiseks teeninduse teie juurde. Saate meile selleks taotlus ja arstitõendi saata.",output_Politsei-_ja_Piirivalveamet +55a6b2f5-f51e-4321-a096-f251dab3d5fa,1,user,"Tere! Ma kaotasin oma rahakoti ja tahan teada, kust ma saan politseisse minna, kui elan Lääne-Harjus. Kas te teate, kus Lääne-Harju Kesklinna politseijaoskond asub?",output_Politsei-_ja_Piirivalveamet +55a6b2f5-f51e-4321-a096-f251dab3d5fa,2,assistant,"Tere! Jah, Lääne-Harju Kesklinna politseijaoskond asub Kolde puiestee 65, 10321 Tallinn. See on Lääne-Harju piirkonna politseijaoskond.",output_Politsei-_ja_Piirivalveamet +c2c80ffd-d684-4aa1-a131-acbcf56937de,1,user,"Tere! Ma kahtlustan, et minu autoga on midagi viga. Kas ma saan selle kohta infot Lääne-Harju Kesklinna politseijaoskonna käest?",output_Politsei-_ja_Piirivalveamet +c2c80ffd-d684-4aa1-a131-acbcf56937de,2,assistant,"Tere! Politseijaoskond ise teab tegelikult vähe sellest. Autovariante saaksite küsida näiteks liiklusregistrist. Lääne-Harju Kesklinna politseijaoskond asub Kolde pst 65, Tallinn, ja on avatud esmaspäevast reedeni kell 9.00-17.00.",output_Politsei-_ja_Piirivalveamet +c2f97974-4088-4664-95e1-1a41083eb5ab,1,user,Tere! Ma uurisin politseijaoskondi ja nägin Lääne-Harju Kesklinna politseijaoskonna nime. Kus see asub?,output_Politsei-_ja_Piirivalveamet +c2f97974-4088-4664-95e1-1a41083eb5ab,2,assistant,"Tere! Lääne-Harju Kesklinna politseijaoskond asub Kolde puiestee 65, 10321 Tallinn.",output_Politsei-_ja_Piirivalveamet +4640d71b-8a04-4847-941e-fe96cddaade3,1,user,Tere! Kuulin midagi piirangutest Venemaa kodanikele Eestisse sisenemisel. Kas see on tõsi ja millest see tuleneb?,output_Politsei-_ja_Piirivalveamet +4640d71b-8a04-4847-941e-fe96cddaade3,2,assistant,"Jah, see on tõsi. Vabariigi valitsuse korraldusega piiratakse alates 19. septembrist 2022 ajutiselt Eestisse sisenemist Venemaa Föderatsiooni kodanikele, kellel on kehtiv lühiajaline Schengeni viisa. Korralduse number on 247 ja leiate selle siit: https://www.riigiteataja.ee/akt/310092022002",output_Politsei-_ja_Piirivalveamet +2c8213d6-4cbe-4e5a-9bf7-73890bd33e84,1,user,"Tere! Olen kuulnud, et Venemaa kodanikele on piirangud Eestisse sisenemisel. Kas see on tõsi ja millest see tingitud on?",output_Politsei-_ja_Piirivalveamet +2c8213d6-4cbe-4e5a-9bf7-73890bd33e84,2,assistant,"Tere! See on tõsi. Alates 19. septembrist 2022 on Eestisse sisenemine piiratud Venemaa Föderatsiooni kodanikele, kellel on kehtiv Schengeni lühiajaline viisa. See piirang on kehtestatud Vabariigi Valitsuse korraldusega 8. septembri 2022 nr 247 järgi.",output_Politsei-_ja_Piirivalveamet +4e8e773e-5008-4c86-9adb-defdb605945c,1,user,Tere! Kuulin midagi piirangute kohta Venemaa kodanikele Eestisse sisenemisel. Kas see on ikka tõene?,output_Politsei-_ja_Piirivalveamet +4e8e773e-5008-4c86-9adb-defdb605945c,2,assistant,"Jah, see on tõene. Alates 2022. aasta 19. septembrist on kehtinud piirangud Eestisse sisenemisel Venemaa Föderatsiooni kodanikele, kel on kehtiv lühiajaline Schengeni viisa. See on seotud Vabariigi Valitsuse korraldusega.",output_Politsei-_ja_Piirivalveamet +70fcb223-b21d-468c-a577-5caabd0bbfc3,1,user,"Tere! Ma lugesin, et Politsei- ja Piirivalveamet avaldab liiklusinfo kohta andmeid. Mis seal täpselt näha on?",output_Politsei-_ja_Piirivalveamet +70fcb223-b21d-468c-a577-5caabd0bbfc3,2,assistant,"Tere! Politsei- ja Piirivalveameti lehel on ülevaade liiklusõnnetustest, mille puhul on inimesed kannatanud. See tähendab, et seal näidatakse liiklusõnnetuste arvu, kus keegi on saanud vigastusi või vaja on olnud meditsiiniabi.",output_Politsei-_ja_Piirivalveamet +edf9562e-dd75-418e-904c-ab8fa4638b94,1,user,"Tere! Ma lugesin, et Politsei- ja Piirivalveamet avaldab liiklusinfo kohta andmeid. Mis seal täpselt näha saab?",output_Politsei-_ja_Piirivalveamet +edf9562e-dd75-418e-904c-ab8fa4638b94,2,assistant,"Tere! Politsei- ja Piirivalveameti veebilehel ongi ülevaade liiklusõnnetustest, kus on inimesed saanud vigastusi. Seal on info liiklusõnnetuste kohta, millega seoses on inimkannatanuid olnud.",output_Politsei-_ja_Piirivalveamet +7e57c544-1369-4990-aff1-9b6f2645ceec,1,user,"Tere! Kus ma leian infot liiklusõnnetuste kohta, kus keegi viga sai?",output_Politsei-_ja_Piirivalveamet +7e57c544-1369-4990-aff1-9b6f2645ceec,2,assistant,"Tere! Politsei- ja Piirivalveameti lehel on ülevõtte infoga liiklusõnnetuste kohta, kus on registreeritud inimkannatanuid.",output_Politsei-_ja_Piirivalveamet +6dc319b6-42e6-4252-a524-9718af1e12cb,1,user,"Tere! Mul on küsimus – kui ma olen jäänud kelmuse ohvriks, kuhu ma peaksin kõigepealt pöörduma?",output_Politsei-_ja_Piirivalveamet +6dc319b6-42e6-4252-a524-9718af1e12cb,2,assistant,"Tere! Esimese asjana on väga oluline kohe ühendust võtta oma pangaga. Nad saavad sageli kiiresti reaalseid meetmeid võtma, et vältida lisakahjustusi. Swedbank, SEB, LHV ja Luminor kõik pakuvad selleks oma turvalisuse leheküljed, näiteks Swedbanki oma on siin: https://www.swedbank.ee/private/d2d/ebanking/secureBanking",output_Politsei-_ja_Piirivalveamet +1cd58624-d781-463b-83ef-e49d7ab2ec1e,1,user,"Tere! Ma kuulsin, et on kelmidega ohvriks sattumisel, kuhu pöörduda. Mis on kõige tähtsam asi, mida ma kohe teha peaks, kui kahtlustan, et olen kelmidele raha saatnud?",output_Politsei-_ja_Piirivalveamet +1cd58624-d781-463b-83ef-e49d7ab2ec1e,2,assistant,"Tere! Kõige olulisem asi on kohe oma pangaga ühendust võtta. Nad saavad sul pangamakse peatada või tagastada, kui see veel võimalik on. Lisainfot leiad näiteks Swedpanki, SEB Panga, LHV ja Luminori veebisaitidelt.",output_Politsei-_ja_Piirivalveamet +47761392-645e-407c-b1d6-4ca04db0c8ed,1,user,"Tere! Ma kuulsin, et on oluline teada, kuhu pöörduda, kui satud kelmuse ohvriks. Mis on esimene asi, mida ma peaksin tegema?",output_Politsei-_ja_Piirivalveamet +47761392-645e-407c-b1d6-4ca04db0c8ed,2,assistant,"Tere! See on väga hea küsimus. Esiteks, kui sa avastad, et oled pangamakse teinud kelmidele, siis võta kohe ja alati ühendust oma pangaga. Swedbanki lehel on selle kohta täiendav info: https://www.swedbank.ee/private/d2d/ebanking/secureBanking , samuti SEB Pangal: https://www.seb.ee/foorum/kuberkuritegevus-kuidas-end-kaitsta , LHV-l: https://www.lhv.ee/et/turvalisus ja Luminoril: https://luminor.ee/petuskeemid?static=true",output_Politsei-_ja_Piirivalveamet +75641c93-6bff-4c3e-ad40-d1baffa307a1,1,user,Tere! Ma olen huvitatud Politsei- ja Piirivalveameti riigihanketest. Kust ma nende kohta infot leian?,output_Politsei-_ja_Piirivalveamet +75641c93-6bff-4c3e-ad40-d1baffa307a1,2,assistant,Tere! Kõik Politsei- ja Piirivalveameti hanketeated ja riigihangete aruanded on kättesaadavad riigihangete registri veebilehelt: https://riigihanked.riik.ee/. Seal leiad infot erinevate hankete kohta.,output_Politsei-_ja_Piirivalveamet +67d3fc3a-bcbf-4080-92f9-6bc649313d54,1,user,Tere! Ma huvitasin end Politsei- ja Piirivalveameti riigihanketest. Kust ma nende kohta infot leian?,output_Politsei-_ja_Piirivalveamet +67d3fc3a-bcbf-4080-92f9-6bc649313d54,2,assistant,Tere! Kõik Politsei- ja Piirivalveameti hanketeated ja riigihangete aruanded on avalikult kättesaadavad riigihangete registri veebilehel: https://riigihanked.riik.ee/. Seal leiad kõik vajalikud andmed.,output_Politsei-_ja_Piirivalveamet +0a195f95-113f-4747-98d3-b7302b146c2e,1,user,"Tere! Ma olen kuulnud, et Politsei- ja Piirivalveamet teeb palju riigihankeid. Kust ma saaksin infot nende hankete kohta leida?",output_Politsei-_ja_Piirivalveamet +0a195f95-113f-4747-98d3-b7302b146c2e,2,assistant,"Tere! Jah, meil on riigihankeid mitmeid. Kõik avalikud hanketeated ja aruanded on leitavad riigihangete registri veebilehel: https://riigihanked.riik.ee/ . Seal leiad kogu vajaliku info.",output_Politsei-_ja_Piirivalveamet +283a1b6e-5351-4d52-a74b-2d8c2dfb0077,1,user,Tere! Mul on küsimus – mida tähendab välismaalase passi taotlemine? Mida see üldse on?,output_Politsei-_ja_Piirivalveamet +283a1b6e-5351-4d52-a74b-2d8c2dfb0077,2,assistant,"Tere! Välismaalase pass on reisidokument, mille Eesti Vabariik annab välismaalasele. See on nagu reisipass, aga just sinule, kui sa ei ole Eesti kodanik.",output_Politsei-_ja_Piirivalveamet +86dd4a93-95c1-4989-9e7e-6983583929c0,1,user,Tere! Ma olen välisriigi kodanik ja mul on Eesti elamisluba. Mis on välismaalase pass ja kas mul on selleks õigus?,output_Politsei-_ja_Piirivalveamet +86dd4a93-95c1-4989-9e7e-6983583929c0,2,assistant,"Tere! Välismaalase pass on Eesti Vabariigi poolt välismaalasele antav reisidokument. Selleks on sul õigus, kui sul on kehtiv Eesti elamisluba või elamisõigus ning sa suudad tõestada, et sul puudub välisriigi reisidokument ja sa ei saa seda ka hankida.",output_Politsei-_ja_Piirivalveamet +14c1907d-3eae-4721-9468-7579ef8f5f44,1,user,"Tere! Ma kuulsin, et saab välismaalase passi taotleda. Mis see täpselt on?",output_Politsei-_ja_Piirivalveamet +14c1907d-3eae-4721-9468-7579ef8f5f44,2,assistant,"Tere! Välismaalase pass on Eesti Vabariigi poolt välismaalasele väljastatav reisidokument. See ongi selline pass, mida sa kasutad välismaal kui sul on kehtiv elamisluba või elamisõigus Eestis.",output_Politsei-_ja_Piirivalveamet +f3d3f371-d430-4c38-a5c0-4bc187e417d4,1,user,"Tere! Ma tahaksin teada, kuidas Narva arestimajas külastajatele lubatud on asju toimetada. Kas saab sinna pakke saata?",output_Politsei-_ja_Piirivalveamet +f3d3f371-d430-4c38-a5c0-4bc187e417d4,2,assistant,Tere! Narva arestimajasse pakke ei saa toimetada. Ainult vahistatul on lubatud isiklikult toimetada asju.,output_Politsei-_ja_Piirivalveamet +907ef0a2-7bda-4b44-8b5d-ea7e27c37ee3,1,user,Tere! Ma olen huvitatud Narva arestimajast. Kas ma saan sinna keegi poeletti tuua?,output_Politsei-_ja_Piirivalveamet +907ef0a2-7bda-4b44-8b5d-ea7e27c37ee3,2,assistant,"Tere! Narva arestimajja ei saa arestalistele pakke tuua. See on oluline reegel, mida tuleb järgida.",output_Politsei-_ja_Piirivalveamet +b9ea212d-f68c-4a17-ace9-e96fe97f5652,1,user,"Tere! Ma saan teada, et Narva arestimajas on mingid reeglid pakikandmise kohta. Kas ma saan sinna saates käimas käibele pakki viia kellelegi?",output_Politsei-_ja_Piirivalveamet +b9ea212d-f68c-4a17-ace9-e96fe97f5652,2,assistant,Tere! Kahjuks mitte. Arestialustele pakke üldiselt tuua ei saa. On teisi küsimusi selle kohta?,output_Politsei-_ja_Piirivalveamet +921495d7-ee25-49b7-a40d-3a85c124168a,1,user,"Tere! Ma kuulsin, et kui mulle viisast keeldutakse, siis ma saan sellega midagi teha. Kas ma saan selle otsuse vaidlustada?",output_Politsei-_ja_Piirivalveamet +921495d7-ee25-49b7-a40d-3a85c124168a,2,assistant,"Jah, see on õige. Välismaalasel on õigus vaidlustada viisa andmisest keeldumise, viisa kehtetuks tunnistamise või viisa tühistamise otsust. Sa saad selle kohta vaide esitada Politsei- ja Piirivalveametile (PPA).",output_Politsei-_ja_Piirivalveamet +3ac0dded-acd8-407b-8c10-34ea811d286c,1,user,Tere! Ma olen välismaalane ja mul on küsimusi viisa kehtetuks tunnistamise kohta. Kui ma saan selle otsuse vaidlustada?,output_Politsei-_ja_Piirivalveamet +3ac0dded-acd8-407b-8c10-34ea811d286c,2,assistant,"Tere! Jah, teil on õigus vaidlustada viisa andmisest keeldumise, viisa kehtetuks tunnistamise või viisa tühistamise otsust. Teil on selleks 30 päeva aega alates otsuse teada saamisest.",output_Politsei-_ja_Piirivalveamet +5de31a0c-f2b0-4bc1-9496-76d53b72385d,1,user,"Tere! Ma tahan teada, mida ma saan teha, kui mulle keeldutakse viisast. Mis on mu võimalused?",output_Politsei-_ja_Piirivalveamet +5de31a0c-f2b0-4bc1-9496-76d53b72385d,2,assistant,"Tere! Kui Teile keeldutakse viisast, on Teil õigus selle otsuse vaidlustada. Teil on võimalik PPA-le esitada vaideavaldus keelduvast otsusest.",output_Politsei-_ja_Piirivalveamet +3a3046c8-ebb7-48fa-aaef-59addcd34121,1,user,"Tere! Ma tean, et võin taotleda alaline elamisoigust EL-ist pärit inimesena. Aga kuidas see poliisiasutus selle menetletakse?",output_Politsei-_ja_Piirivalveamet +3a3046c8-ebb7-48fa-aaef-59addcd34121,2,assistant,"Tere! Jah, seda saab teha. Politsei- ja Piirivalveamet kontrollib taotluse. Nad peaksid veenduma, et teil on kõik nõuetekohased dokumendid, ja et teil on täidetud kõik elamisõiguse saamiseks vajalikud tingimused. Küll aga ei pruugiks teil midagi lisaks kuuldagi, kui kõik on korras. Kas te soovite rohkem teada?",output_Politsei-_ja_Piirivalveamet +3e2ccb3b-7a73-4896-8fa8-9dd78ee5158b,1,user,"Tere! Ma arvan, et mul on küsimus elamisõiguse kohta. Kuidas see üldse menetletakse Politsei- ja Piirivalveametis?",output_Politsei-_ja_Piirivalveamet +3e2ccb3b-7a73-4896-8fa8-9dd78ee5158b,2,assistant,"Tere! Jah, küsimus on väga mõistlik. Menetlemine algab sellega, et Politsei- ja Piirivalveamet kontrollib, et kõik taotluses esitatud dokumendid on nõuetekohased. Peab olema näiteks kehtiv ID kaart. Seejärel kontrollitakse, et sa soodsad tingimused elamisõiguse saamiseks täidetakse. Kui on midagi puuduvat, võtame sind teada.",output_Politsei-_ja_Piirivalveamet +214c8418-f19d-4677-8d47-b05810ee2163,1,user,"Tere! Ma arvan, et ma peaksin taotlema alaline elamisõigus, aga ma ei ole päris kindel, kuidas see toimib. Kas Politsei- ja Piirivalveamet seda menetleb?",output_Politsei-_ja_Piirivalveamet +214c8418-f19d-4677-8d47-b05810ee2163,2,assistant,"Tere! Jah, Politsei- ja Piirivalveamet kindlasti menetleb selle. Nad kontrollivad, et sa oled esitanud kõik vajalikud dokumendid ja et sa täidate elamisõiguse saamiseks nõuetekohased tingimused. Kui mingis punktis puudub midagi, sa saad teada. Kas sul on täna mõni konkreetne dokument, mille kohta sa arvad, et puudub midagi?",output_Politsei-_ja_Piirivalveamet +a2f6acdd-dd9e-420b-ae28-e5b0c2270b90,1,user,Tere! Ma kaotanud oma isikut tõendava dokumendi Jõhvis. Kus ma saaksin politsei juurest abi küsida?,output_Politsei-_ja_Piirivalveamet +a2f6acdd-dd9e-420b-ae28-e5b0c2270b90,2,assistant,"Tere! Jõhvi politseijaoskond asub aadressil Rahu 38, 41588 Jõhvi. Nad on avatud esmaspäevast reedeni kell 9.00 kuni 17.00.",output_Politsei-_ja_Piirivalveamet +b4758db7-649a-433c-bcc8-74aa60eff211,1,user,"Tere! Ma kaotanud oma rahakoti Jõhvis. Kas te teate, kust ma saan politseisse pöörduda?",output_Politsei-_ja_Piirivalveamet +b4758db7-649a-433c-bcc8-74aa60eff211,2,assistant,"Tere! Jõhvi politseijaoskond asub Rahu 38, 41588 Jõhvi. Nad on avatud esmaspäevast reedeni kell 9.00 kuni 17.00.",output_Politsei-_ja_Piirivalveamet +8175660e-00fd-4f87-86fb-08d78fb65c27,1,user,Tere! Ma kaotasin oma isikukaarti. Kust ma saan Jõhvi politseijaoskonna aadressi?,output_Politsei-_ja_Piirivalveamet +8175660e-00fd-4f87-86fb-08d78fb65c27,2,assistant,"Tere! Jõhvi politseijaoskond asub aadressil Rahu 38, 41588 Jõhvi.",output_Politsei-_ja_Piirivalveamet +3979c0af-d2cf-410a-b1ea-ea922b5fcc9c,1,user,"Tere! Ma kuulsin, et 112 on hädaabinumber. Aga millal täpselt ma peaksin sellele helistama?",output_Politsei-_ja_Piirivalveamet +3979c0af-d2cf-410a-b1ea-ea922b5fcc9c,2,assistant,"Tere! Sa peaksid 112-le helistama siis, kui sa vajad politsei kiiret abi ja tahad teatada korrarikkumisest, kuriteost või liiklusõnnetusest. See on number, millele helistada, kui on vaja kiiret sekkumist.",output_Politsei-_ja_Piirivalveamet +2de4b7c5-6816-4fde-9f6f-f43c6d04f4c1,1,user,"Tere! Ma just lugesin, et 112 on hädaabinumber. Aga millal täpselt ma sellele helistama peaksin?",output_Politsei-_ja_Piirivalveamet +2de4b7c5-6816-4fde-9f6f-f43c6d04f4c1,2,assistant,"Tere! 112-le helistad, kui vajad politsei kiiret abi ja soovid teatada korrarikkumisest, kuriteost või liiklusõnnetusest. See ongi näiteks, kui näed kedagi, kes on mures või vajan abi pärast liiklusõnnetust.",output_Politsei-_ja_Piirivalveamet +73eee664-f864-43a9-94ee-29e5a5b7d967,1,user,Tere! Ma just lugesin midagi 112 kohta. Millal ma peaksin sellele numbrile helistama?,output_Politsei-_ja_Piirivalveamet +73eee664-f864-43a9-94ee-29e5a5b7d967,2,assistant,"Tere! Sa peaks helistama 112 numbrile, kui sa vajad politsei kiiret abi ja soovid teatada korrarikkumisest, kuriteost või liiklusõnnetusest. See on hädaabinumber, nii et kasuta seda olukordades, kus kiiret reageerimist on vaja.",output_Politsei-_ja_Piirivalveamet +f9e8a496-d62c-4959-8a11-b8e797339dbb,1,user,Tere! Ma tahan elamisloakaarti taotleda. Kuidas ma riigilõivu maksta saan?,output_Politsei-_ja_Piirivalveamet +f9e8a496-d62c-4959-8a11-b8e797339dbb,2,assistant,"Tere! Riigilõivu tasumiseks on mitu võimalust. Saaja on Rahandusministeerium ja viitenumber on 2900082443. Pangad, kus riigilõivu maksta saad, on SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor Bank (EE701700017001577198) ja LHV Pank (EE777700771003813400).",output_Politsei-_ja_Piirivalveamet +243d1c01-a0f4-438b-99ae-765c7cf07179,1,user,Tere! Ma olen täiskasvanu ja tahan elamisloakaardi taotlemisega alustada. Kuidas ma riigilõivu maksta saan?,output_Politsei-_ja_Piirivalveamet +243d1c01-a0f4-438b-99ae-765c7cf07179,2,assistant,"Tere! Elamisloakaardi taotlemise riigilõivu maksmiseks on mitu võimalust. Saaja on Rahandusministeerium, ja viitenumber on 2900082443. Pangakontod on erinevad: SEB EE891010220034796011, Swedbank EE932200221023778606, Luminor EE701700017001577198 ja LHV Pank EE777700771003813400.",output_Politsei-_ja_Piirivalveamet +2231431f-09d1-440c-aa8b-b2996ef71fc4,1,user,Tere! Ma plaanin elamisloakaardi taotlemist. Kuidas ma riigilõivu maksta saan?,output_Politsei-_ja_Piirivalveamet +2231431f-09d1-440c-aa8b-b2996ef71fc4,2,assistant,"Tere! Elamisloakaardi taotlemisel riigilõivu tasumiseks on mitmeid võimalusi. Saaja on Rahandusministeerium. Viitenumber on 2900082443. Pangad, kuhu saad maksta, on SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor Bank (EE701700017001577198) ja LHV Pank (EE777700771003813400).",output_Politsei-_ja_Piirivalveamet +30e523fe-0804-4b0f-8588-1d7e644244a5,1,user,Tere! Ma tahan oma relvaloa kehtivust pikendada. Kas ma pean selleks laskekatset tegema?,output_Politsei-_ja_Piirivalveamet +30e523fe-0804-4b0f-8588-1d7e644244a5,2,assistant,"Tere! See oleneb relvaloa otstarbest. Kui relvaloa eesmärk on ainult enese ja vara kaitse, siis jah, pead täiendava laskekatse sooritama. Kui aga on märgitud jahipidamine või sport, siis laskekatset ei pea tegema.",output_Politsei-_ja_Piirivalveamet +57e0d5ac-2289-4cce-899f-649b0f985e43,1,user,Tere! Ma lähen relvaloa pikendamist taotlema. Kas ma pean kindlasti selle täiendava laskekatse sooritama?,output_Politsei-_ja_Piirivalveamet +57e0d5ac-2289-4cce-899f-649b0f985e43,2,assistant,"Tere! See oleneb relva otstarbetest. Kui relvaloa märgitud otstarbeks on enese ja vara kaitse, siis jah, pead täiendava laskekatse sooritama. Aga kui sinna on märgitud jahipidamine või sport, siis seda ei pea tegema.",output_Politsei-_ja_Piirivalveamet +85d4a914-6f11-495a-9726-3a0109f7ae26,1,user,"Tere! Ma olen kuulnud, et relvaloa kehtivust on võimalik pikendada. Mis siis täpsemalt on sellega seotud, kui ma tahan seda enda puhul teha?",output_Politsei-_ja_Piirivalveamet +85d4a914-6f11-495a-9726-3a0109f7ae26,2,assistant,"Tere! Relvaloa kehtivuse pikendamisel on oluline, miks sa relvaloa üldse omad. Kui relva otstarbeks on märgitud jahipidamine või sport, siis täiendavat laskekatset ei pea tegema. Kui aga on märgitud ainult enese ja vara kaitseks, siis pead selleks täiendava laskekatse sooritama.",output_Politsei-_ja_Piirivalveamet +7a8633d9-9fcd-4b40-89db-5b84a772feae,1,user,"Tere! Ma kuulsin, et e-residentsuse saamine on Venemaa ja Valgevene kodanikele muutunud keerulisemaks. Mis juhtus?",output_Politsei-_ja_Piirivalveamet +7a8633d9-9fcd-4b40-89db-5b84a772feae,2,assistant,Tere! See on õige. Alates 11. märtsist 2022 on esmakordselt Venemaa ja Valgevene kodanikest taotlejad jäetud e-residentsuse programmiga liitumise võimalusest ilma. Politsei- ja Piirivalveamet peatas selle võimaluse vastavalt peadirektori korraldusele.,output_Politsei-_ja_Piirivalveamet +bb4c01c9-f028-4818-aa0d-e08f4a13c262,1,user,"Tere! Kuulsin, et e-residensi saamine on muutunud keerulisemaks Venemaa ja Valgevene kodanikele. Kas see on tõsi?",output_Politsei-_ja_Piirivalveamet +bb4c01c9-f028-4818-aa0d-e08f4a13c262,2,assistant,"Jah, see on tõsi. Esmakordsed Venemaa ja Valgevene kodanikud ei saa enam Eesti e-residensi digitaalset isikutunnistust taotleda. See piirang jõustus 11. märtsist 2022.",output_Politsei-_ja_Piirivalveamet +a6a3f3c0-ae0e-4719-b3dd-9b00d1341668,1,user,"Tere! Olen kuulnud, et e-residensi saamine on Venemaa ja Valgevene kodanikele muutunud keerulisemaks. Kas see on tõsi?",output_Politsei-_ja_Piirivalveamet +a6a3f3c0-ae0e-4719-b3dd-9b00d1341668,2,assistant,"Jah, see on tõsi. Alates 11. märtsist 2022 on esmakordselt Venemaa ja Valgevene kodanikele e-residensi taotlusi enam rahuldatud ei saa. See on seotud Ukraina sõjaga ja on osa rahvusvahelistest sanktsioonidest.",output_Politsei-_ja_Piirivalveamet +2529d4d3-71ec-4d0a-840b-8abf747fb87a,1,user,"Tere! Kas sa võiksite mulle öelda, mitu Ukraina kodanikku on Eestisse alates 27. veebruarist saabunud?",output_Politsei-_ja_Piirivalveamet +2529d4d3-71ec-4d0a-840b-8abf747fb87a,2,assistant,Muidugi! Eestisse on alates 27. veebruarist saabunud umbes 23 886 Ukraina kodanikku ja nende pereliiget.,output_Politsei-_ja_Piirivalveamet +05cebc46-d4fc-4446-8c53-398cff194628,1,user,"Tere! Kas sa saadada mulle infot selle kohta, kui palju Ukraina kodanikke on Eestisse saabunud alates 27. veebruarist?",output_Politsei-_ja_Piirivalveamet +05cebc46-d4fc-4446-8c53-398cff194628,2,assistant,"Tere! Jah, ma saan. Alates 27. veebruarist on Eestisse saabunud 23 886 Ukraina kodanikku ja nende pereliiget. Kas sulle huvitab veel midagi?",output_Politsei-_ja_Piirivalveamet +b8951912-ce6c-4e09-9a2c-e29220e58785,1,user,Tere! Kuidas palju Ukraina kodanikke on nüüd Eestisse saabunud alates 27. veebruarist?,output_Politsei-_ja_Piirivalveamet +b8951912-ce6c-4e09-9a2c-e29220e58785,2,assistant,Tere! Alates 27. veebruarist on Eestisse saabunud kokku 23 886 Ukraina kodanikku ja nende pereliiget.,output_Politsei-_ja_Piirivalveamet +b2d7d307-85ca-44c3-8f0b-7c447fc0b186,1,user,"Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on lapsevanematele mingit ohutusraamatut välja andnud. Kas see on tõsi?",output_Politsei-_ja_Piirivalveamet +b2d7d307-85ca-44c3-8f0b-7c447fc0b186,2,assistant,"Jah, see on tõsi! Politsei- ja Piirivalveamet ongi valmistanud ohutusraamatu just 1. klassi minevate laste vanematele. See on mõeldud eelkõige lapsevanemate toetamiseks lapse turvalisuse tagamisel.",output_Politsei-_ja_Piirivalveamet +3ae196e6-d538-4227-9686-77c8d7bb1668,1,user,"Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on lapsevanematele mingit ohutusraamatut valmistanud. Mis see raamat on ja kellele see mõeldud?",output_Politsei-_ja_Piirivalveamet +3ae196e6-d538-4227-9686-77c8d7bb1668,2,assistant,"Tere! Jah, meil ongi olemas Ohutusraamat lapsevanematele, mis on mõeldud eelkõige 1. klassi minevate laste vanematele. See ongi materjal, mis toetab lapsevanemaid lapse elu kaitsmisel ja turvalise kasvukeskkonna loomisel.",output_Politsei-_ja_Piirivalveamet +892d287a-97bc-44e7-82a5-b3af564beca6,1,user,"Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on lapsevanematele ohutusraamatu välja andnud. Mis see raamat sisaldab?",output_Politsei-_ja_Piirivalveamet +892d287a-97bc-44e7-82a5-b3af564beca6,2,assistant,"Tere! Jah, see on tõsi. See ohutusraamat on mõeldud eelkõige 1. klassi minevate laste vanematele. See ongi Politsei- ja Piirivalveameti poolt toetav materjal, mis käsitleb lapse turvalise kasvukeskkonna loomist ja lapse ohutust erinevates elualdades. Seega, tegelikult sisaldab see informatsiooni lapse turvalisuse kohta koolitee alguses.",output_Politsei-_ja_Piirivalveamet +74efeb92-6a1d-4cee-9bc4-ed2df6821911,1,user,"Tere! Ma arvan, et politsei on mõnikord avalikule koosoledule sekkuda. Miks nad seda teevad, ja millistel juhtudel?",output_Politsei-_ja_Piirivalveamet +74efeb92-6a1d-4cee-9bc4-ed2df6821911,2,assistant,"Tere! Jah, see on tõsi. Politsei saab avalisele koosoledule sekkuda, kui arvab, et on olukord, kus koosoleku pidamine ohustab inimelu või seaduslikkust. Näiteks, kui on alust arvata, et kohas, kus koosolek toimub, on olukord, kus saab tõesti kiiresti süütu tekitada. Mida täpsemalt sa küsid?",output_Politsei-_ja_Piirivalveamet +a662e466-7469-4637-b1ac-3f1179c76867,1,user,"Tere! Kas ma saan aru, et politsei võtab kohalolekul vastu rolli koosolekute korraldamisel? Kas see on mingi kohustus?",output_Politsei-_ja_Piirivalveamet +a662e466-7469-4637-b1ac-3f1179c76867,2,assistant,"Tere! Jah, seda saab juhtuda. Politsei võib kohustada koosoleku korraldajat täitma lisaseid kohustusi, kui on alust arvata, et määratud korrapidaja ei suuda koosoleku korraldajat piisavalt abistada. See on siis tänu suurendatud korrapidajate arvu vajavusele. Kas sa mõtled rohkem sellele, mis juhtuks, kui korrapidaja ei suuda head abi pakkuda?",output_Politsei-_ja_Piirivalveamet +d8ca32d7-f498-4838-ad39-3e2e5576880b,1,user,"Tere! Ma arvan, et politsei saab konverentsidel abi pakkuda? Kas on olukord, kus neil on vaja konverentsil lisaks korraldajale veel üht inimest?",output_Politsei-_ja_Piirivalveamet +d8ca32d7-f498-4838-ad39-3e2e5576880b,2,assistant,"Tere! Jah, seda võib juhtuda. Politsei võib suurendada konverentsil korrapidajate arvu, kui arvab, et määratud korrapidaja ei suuda korraldajat piisavalt aidata. See on oluline, kui arvab, et on võimalik kõrgendatud ohu tekkimine või juba tegu on kõrgendatud ohuga. Kas sa mõtled midagi konkreetset? Näiteks kuidas see tä hakkab toimima?",output_Politsei-_ja_Piirivalveamet +5ca0da74-efe8-4ac3-9702-869199c0cafb,1,user,"Tere! Ma imestan, kuidas Politsei- ja Piirivalveamet kasutab minu isikuandmeid, kui ma teieni pöördun. Saaksin selle kohta rohkem teada?",output_Politsei-_ja_Piirivalveamet +5ca0da74-efe8-4ac3-9702-869199c0cafb,2,assistant,"Tere! Jah, muidugi. Me kasutame teie isikuandmeid pöördumise lahendamiseks ja teile vastamiseks. Kui me peame pöördumise lahendamiseks küsimusi esitama teistele asutustele, siis edastame neile ainult nii palju andmeid, mis on vastuse koostamiseks absolutne vaja. Me püüame minimeerida avaldatava info hulka. Kas teil on selle kohta veel küsimusi?",output_Politsei-_ja_Piirivalveamet +a0516c2c-cff8-4ca5-b99c-f817fcf6f6fd,1,user,"Tere! Ma olen huvitatud teadma, kuidas Politsei- ja Piirivalveamet pöördumistele vastates minu isikuandmeid kasutab?",output_Politsei-_ja_Piirivalveamet +a0516c2c-cff8-4ca5-b99c-f817fcf6f6fd,2,assistant,"Tere! Pöördumiste lahendamiseks ja teile vastamiseks kasutame teie isikuandmeid. Kui peame teistele asutustele või isikutele järelepärimisi esitama, edastame neile ainult nii palju andmeid, kui on vajalik vastuse koostamiseks. Me ei avalda neid andmeid rohkem kui on tarvis.",output_Politsei-_ja_Piirivalveamet +f695f3a2-4994-43a2-8c65-f06009da069d,1,user,"Tere! Ma olen kuulnud, et Politsei- ja Piirivalveamet kasutab minu isikuandmeid, kui ma saan teiega ühendust. Kas see on nii ja millal?",output_Politsei-_ja_Piirivalveamet +f695f3a2-4994-43a2-8c65-f06009da069d,2,assistant,"Tere! Jah, see on õige. Me kasutame isikuandmeid pöördumiste lahendamiseks ja teile vastamiseks. Kui peame pöördumist lahendades küsimusi esitama teistele asutustele, edastame neile ainult need andmed, mis on vastuse koostamiseks tõesti vajalikud. See aitab meil privaatsust kaitsta.",output_Politsei-_ja_Piirivalveamet +37f3bcf7-bc22-4b92-9f12-1113841fb826,1,user,"Tere! Ma tahan lapsele passi taotleda, aga ei tea, kuidas riigilõivu maksta. Kust ma selle maksta saan?",output_Politsei-_ja_Piirivalveamet +37f3bcf7-bc22-4b92-9f12-1113841fb826,2,assistant,"Tere! Riigilõivu saad tasuda mitme panga kaudu. Saaja on Rahandusministeerium. Kontonumbrid on: SEB EE891010220034796011, Swedbank EE932200221023778606, Luminor Bank EE701700017001577198 ja LHV Pank EE777700771003813400.",output_Politsei-_ja_Piirivalveamet +758138ce-e86c-42fe-b3f5-5ed9bf210e5a,1,user,"Tere! Mul on küsimus. Ma pean taotlema passi lapsele, kes on välismaalane. Kuidas ma riigilõivu maksin?",output_Politsei-_ja_Piirivalveamet +758138ce-e86c-42fe-b3f5-5ed9bf210e5a,2,assistant,"Tere! Riigilõivu tasumiseks pead kasutama Rahandusministeeriumi kontot. Saaja on Rahandusministeerium, viitenumber on 2900082443. Pangaülekandeks on saadaval erinevad kontod, näiteks SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor (EE701700017001577198), LHV Pank (EE777700771003813400).",output_Politsei-_ja_Piirivalveamet +6b57dfdb-da5f-417b-91cf-f887b7fd4b91,1,user,Tere! Ma lähen Eestisse ja tahan lapsele passi taotleda. Kuidas ma riigilõivu maksta saan?,output_Politsei-_ja_Piirivalveamet +6b57dfdb-da5f-417b-91cf-f887b7fd4b91,2,assistant,"Tere! Passi taotlemise riigilõivu maksmiseks on mitmeid võimalusi. Saaja on Rahandusministeerium ja viitenumber on 2900082443. Pangad, kust saad maksta, on SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor Bank (EE701700017001577198), LHV Pank (EE777700771003813400).",output_Politsei-_ja_Piirivalveamet +b8ed6a74-9a28-4df0-b3e2-63aecd711995,1,user,"Tere! Ma arvan, et minu ID-tarkvara on vanamatu. Kuidas ma saan selle uuemaks tegema?",output_ID.ee +b8ed6a74-9a28-4df0-b3e2-63aecd711995,2,assistant,"Tere! Päev! Võib olla, et on aeg selle uuemaks tegema. ID-tarkvara versiooninumber annab teada, kui uus tarkvara on. Mida suurem number, seda uuem. Kuidas sa siis selle uuemaks tegema soovid? Sa saad selle lehele laadida: [ID-tarkvara leht] (lahtiselt ID.ee lehelt).",output_ID.ee +ff2dff11-6c23-4d60-827b-ad6d2c86c06e,1,user,Tere! Kas teeksite mulle infot ID-tarkvara uusima versiooni kohta?,output_ID.ee +ff2dff11-6c23-4d60-827b-ad6d2c86c06e,2,assistant,"Tere! Jah, küllap. ID-tarkvara uusimad versioonid on ID.ee lehel saadaval. Meie süte automaatselt teie arvuti operatsioonisüsteemi ja pakub sobiva paketi. On lihtne, peate vaid tarkvara alla lahtima ja paigaldama.",output_ID.ee +be774435-2abd-4cd7-82b0-1c5013a666d8,1,user,Tere! Kas te saate rääkida ID-tarkvara värskendamisest? Mul on küsimusi selle kohta.,output_ID.ee +be774435-2abd-4cd7-82b0-1c5013a666d8,2,assistant,"Tere! Muidugi, ma saan. ID-tarkvara värskendamine on väga lihtne. Meie veebilehel on automaatselt kindluses oleva tarkvara uusim versioon. Kas te olete Windows, macOS või Linux kasutaja? See aitab öelda, millist versiooni peaksite alla laadima.",output_ID.ee +e80208ec-1710-4b59-8574-977a75a08ede,1,user,Tere! Ma saan probleeme e-teenustesse sisse logimisel. Mida ma peaks tegema?,output_ID.ee +e80208ec-1710-4b59-8574-977a75a08ede,2,assistant,"Tere! Puhkan teada, et e-teenustesse sisse logimisel tekib probleem. Ma aitaksin sulle probleemi lahendada. Alustame kõige lihtsamatest sammudest. Kas sa kasutad Windowsi, macOSi või Linuxi arvutit? See aitab mulle paremini aidata.",output_ID.ee +f4b56adf-8b75-46e2-91ea-58a4cd1a5f4f,1,user,Tere! Ma saan probleeme e-teenuste sisenemisega. Mis ma teha saan?,output_ID.ee +f4b56adf-8b75-46e2-91ea-58a4cd1a5f4f,2,assistant,"Tere! Ma saan aru, et sul on raskusi e-teenustega. Palju maasõnki! Alustame kõige lihtsamatest asjadest. Kas su arvatakse, et probleem on seotud ID-kaardiga või rohkem üldisemalt veebisõidumisel?",output_ID.ee +11fa356f-0d0f-4d74-9f6f-17649a886164,1,user,"Tere! Ma püüan mobiil-IDga dokumenti allkirjastada, aga saan veateate. Mis ma teha saan?",output_ID.ee +11fa356f-0d0f-4d74-9f6f-17649a886164,2,assistant,"Tere! Palun, andke teada, mis veateadet just saate? See aitab meil probleemi paremini mõistata. Kui teate veateate numbrit või tekstisummarit, siis palun jagage seda koos teiega tehtud toiminguga.",output_ID.ee +3f05f9e0-447d-48f3-bcce-9650ddedeae8,1,user,Tere! Ma saan probleeme mobiil-IDga allkirjastamisel. Mis võiks olla põhjus?,output_ID.ee +3f05f9e0-447d-48f3-bcce-9650ddedeae8,2,assistant,"Tere! Võib olla mitmeid asja. Sageli on probleem ajutine ja laheneb ise. Palju olgem vaadates veateadet – see sisalda alati vajaliku info probleemi lahendamiseks. Esmalt luge veateade täies ulatuses. Kas sa tead, mis veateadet sa nägid?",output_ID.ee +b3725b7f-5550-4396-bcec-e71ccbd71ae3,1,user,Tere! Kas ID-kaart on mõni uus dokument Eestis või on see vana?,output_ID.ee +b3725b7f-5550-4396-bcec-e71ccbd71ae3,2,assistant,"Tere! ID-kaart on Eesti Vabariigis esmane isikut tõendav dokument. See on suhteliselt uus, sest selle kasutamine on seadusest tuleneva kohustus.",output_ID.ee +804f5763-0433-43ac-aa65-ae9d61c3ea9c,1,user,"Tere! Sa ütled, et siin on infot ID-tarkvara kohta. Kõigepealt, mis see ID-tarkvara üldse on?",output_ID.ee +804f5763-0433-43ac-aa65-ae9d61c3ea9c,2,assistant,"Tere! ID-tarkvara on kogum, mis aitab identiteeti tõestada ja digitaalselt dokumente allkirjastada. Siin on osa sellest: DigiDoc4, RIA e-allkirja valideerimisteenus SiVa ning RIA DigiDoc mobiilirakendus. Kõigepealt aga, kas sa tead, mis on identiteedi tõendamine?",output_ID.ee +48e22bad-72e9-4e0a-809d-674910225b9a,1,user,"Tere! Sa võid mulle teada, milliseid andmeid DigiDoc4 rakendus kasutab ja kes neile andmeid edastab?",output_ID.ee +48e22bad-72e9-4e0a-809d-674910225b9a,2,assistant,"Tere! Loomulikult. DigiDoc4 rakendus kasutab mitmesuguseid andmeid, sealhulja sinu isikukoodi, telefoni numbrit ja aadressi. Need andmed edastatakse erinevate kolleegidele, näiteks Riigi Infosüsteemi Ametile (RIA), SK ID Solutionsi ja ka muudele partneritele, kes osalvad e-allkirjastamisprotsessis. Küll aga on oluline meeles pidada, et RIA kasutab neid andmeid ainult e-allkirjastamiseks ja andmete kontrollimiseks.",output_ID.ee +79e77168-8b14-438f-8942-b98eb131c1c1,1,user,Tere! Kas ma saan uue ID-kaardi? Ja kust saan selle taotleda?,output_ID.ee +79e77168-8b14-438f-8942-b98eb131c1c1,2,assistant,"Tere! Jah, uut ID-kaarti saab kindlasti taotleda. Peaasi, et see on kõikidele Eesti kodanikele kohustuslik isikut tõendav dokument. ID-kaardi saab taotleda ainult Politsei- ja Piirivalveametist (PPA). Saada lisa infot nende kodulehel: https://www.politsei.ee/et/juhend/id-kaardi-taotlemine-taeiskasvanule",output_ID.ee +bbdb890b-8caf-497a-ab30-810a77774862,1,user,Tere! Kas ma saan uue ID-kaardi tellida?,output_ID.ee +bbdb890b-8caf-497a-ab30-810a77774862,2,assistant,"Tere! Jah, seda on võimalik. Uue ID-kaardi saab taotleda Politsei- ja Piirivalveametist. See on kõikidele Eesti kodanikele kohustuslik isikutõendav dokument. Kui saad uue ID-kaardi, saad ka koos sellega koodiümbriku, millesse on trükieritud turvakoode - PIN ja PUK koodid. Küll aga, see on oluline, et teadaksid, et uue ID-kaardi saab tellida ka kauplustest alates jaanuari 2023. Kas sa oled pärituusama ID-kaardi tellimisel kindlamalt huvitatud?",output_ID.ee +d788341f-7b10-421f-90b7-d098cb76122c,1,user,"Tere! Ma arvan, et mul on probleeme oma ID-kaardi PIN ja PUK koodidega. Kas saaksite mind aidata?",output_ID.ee +d788341f-7b10-421f-90b7-d098cb76122c,2,assistant,"Tere! Muidugi, ma saan teid aidata. Kui te probleeme PIN ja PUK koodidega näete, on oluline need kohe vahetada. Soovitame seda teineteise vastu vahetada kohe - näiteks, kui valitud koodid on meeles, saate need muuta DigiDoc4 rakenduses. Kas te olete juba proovinud PIN koodi muuta DigiDoc4 kaudu?",output_ID.ee +6bac76d4-3260-4b01-af35-7bab8ea3111e,1,user,"Tere! Ma ei ole päris kindel, kuidas ID-kaardi PIN-koodi ja PUK-koodi vahetada. Kas on sellega vaja DigiDoc4 programmi?",output_ID.ee +6bac76d4-3260-4b01-af35-7bab8ea3111e,2,assistant,"Tere! Jah, DigiDoc4 on selle jaoks parim vahend. Soovitame kindlasti kohe PIN- ja PUK-koodid vahetada. Need on olulised, et sa neid meelepäraselt meenutada saaksid. Kas sa oled varem DigiDoc4 programmi kasutanud?",output_ID.ee +22cbdb7a-e412-4ff3-af49-786bf1289bc6,1,user,"Tere! Ma ei saa aru, miks mul peab PIN-koodi ja PUK-koodi vahetama. Kas see on oluline?",output_ID.ee +22cbdb7a-e412-4ff3-af49-786bf1289bc6,2,assistant,"Tere! Pigil, see on väga hea tava turvalisuse tagamiseks. Kui PIN- või PUK-koodid lähevad kadunud või on teiega otseselt mitte liigutav (näiteks, nad on lukustatud), on uus PIN ja PUK seadmine oluline. Kui kehtivad PIN-koodid on teiega selge selge, pole see eriti oluline, aga kui need on kadunud või lukustatud, siis tuleb neid kindlasti vahetada.",output_ID.ee +bcace457-55cd-4dd7-afad-e22a42864378,1,user,"Tere! Ma sain uue mobiil-ID SIM-kaardi. Mitte aru ei saa, mis need PIN-koodid on ja mida ma neilt peaks tegema.",output_ID.ee +bcace457-55cd-4dd7-afad-e22a42864378,2,assistant,"Tere! Tore, et sa oled uue mobiil-ID SIM-kaardi saanud. Need PIN-koodid on olulised, et sa võiksed mobiil-IDga seotud tehingud ja toimingud teha. Seal on kolm koodi: PIN1, PIN2 ja PUK. PIN1 ja PIN2 on vajalikud, et sisselogida ja tehinguid kinnitada. PUK on vajalik, kui need koodid lukku lähevad. Sa leiad need koodid SIM-kaardi ümbrise alt kraabitava pinna alt.",output_ID.ee +fd46c794-e668-4ddd-afe2-f81869c5fdc4,1,user,Tere! Ma saan uue mobiil-ID SIM-kaardi. Kas ma pean PIN-koodid täna kohe vahetama?,output_ID.ee +fd46c794-e668-4ddd-afe2-f81869c5fdc4,2,assistant,"Tere! Jah, väga soovitame PIN- ja PUK-koodid välja vahetada kohe. Need on kaasa tulnud SIM-kaardi ümbriselt –kraabitava pinna alt. Nende kinnipidasmine on lubamine, et sa soovid mobiil-ID funktsiooni kasutada. Muidugi on kaasa tulnud kolm koodi: isikut tuvastamiseks, digitaalallkirja andmiseks ja lukku läinud koodide taastamiseks.",output_ID.ee +43e3f554-bc12-4856-9225-f4dcc6f14557,1,user,Tere! Kas ma saan RIA DigiDoc äpi uue versiooni alla laadida? Mis tasu?,output_ID.ee +43e3f554-bc12-4856-9225-f4dcc6f14557,2,assistant,"Tere! Jah, RIA DigiDoc mobiilirakenduse uusim versioon on alati saadaval App Store'ist (https://itunes.apple.com/ee/app/ria-digidoc/id1216104448?mt=8) ja Google Play store'ist (https://play.google.com/store/apps/details?id=ee.ria.DigiDoc). See on tasuta!",output_ID.ee +8dd4cc89-880e-4f46-951c-2532bd376531,1,user,"Tere! Kas te saate öelda, kus ma saan RIA DigiDoc mobiilirakenduse uusima versiooni alla laadida?",output_ID.ee +8dd4cc89-880e-4f46-951c-2532bd376531,2,assistant,Muidugi! Uusima versiooni saate alla laadida App Store'ist: https://itunes.apple.com/ee/app/ria-digidoc/id1216104448?mt=8 või Google Play Store'ist: https://play.google.com/store/apps/details?id=ee.ria.DigiDoc.,output_ID.ee +4d34ec04-e60e-458f-9a3f-b1a909172137,1,user,"Tere! Kas te saate öelda, kus ma saan RIA DigiDoc mobiilirakenduse uue versiooni alla laadida?",output_ID.ee +4d34ec04-e60e-458f-9a3f-b1a909172137,2,assistant,Muidugi! RIA DigiDoc mobiilirakendus on saadaval nii App Store'ist (https://itunes.apple.com/ee/app/ria-digidoc/id1216104448?mt=8) kui ka Google Play poodist (https://play.google.com/store/apps/details?id=ee.ria.DigiDoc).,output_ID.ee +e849b734-25a9-4922-8cbf-70c927f96e3a,1,user,Tere! Mul on probleeme ID-kaardiga. Kas teil on võimalik aidata?,output_ID.ee +e849b734-25a9-4922-8cbf-70c927f96e3a,2,assistant,"Tere! Muidugi aitame. Kõigepealt, palun kirjuta id.ee veebilehele ja „Kirjuta ID-abile“. Seal saad oma probleemist kirjalikult selgitada. See aitab meil sind paremini aidata.",output_ID.ee +a106fbec-4756-4fc7-a1c5-0700452a064a,1,user,"Tere! Ma saan probleeme ID-kaardiga, aga ei ole kindel, kuidas diagnoosi faili genereerida. Kust ma peaksin alustama?",output_ID.ee +a106fbec-4756-4fc7-a1c5-0700452a064a,2,assistant,"Tere! Üsna sagedasti juhtubki. Võime teiega sellega kindlasti aidata. Kõige mugavam on kirjutada ID-abile läbi id.ee veebis asuva lingi (https://www.id.ee/artikkel/teavita-meid-probleemist/). Kui tead, milline probleem tekkis, siis saad teada täpsemad juhised. Aga enne, kas tead, et teil on DigiDoc4 ja RIA DigiDoc rakendus ID-kaardi kasutamisele?",output_ID.ee +c6260c6e-98b9-491b-b08a-029bd73b8928,1,user,Tere! Mul on probleem Windowsiga ID tarkvara paigaldamisel. Ilmnes mingi tõrk. Kas te saate mind aidata?,output_ID.ee +c6260c6e-98b9-491b-b08a-029bd73b8928,2,assistant,Tere! Ma saan küll. Palun räägi rohkem sellest tõrkest. Kas sa saad mingi veaõht (koodiga) või veateate? Ja millise ID tarkvara sa paigaldate?,output_ID.ee +158c4f81-cf85-42c8-bb91-d96d81a68957,1,user,"Tere! Ma sain probleeme ID-tarkvara paigaldamisel. Kas teil on mõni idee, mis võiks olla põhjus?",output_ID.ee +158c4f81-cf85-42c8-bb91-d96d81a68957,2,assistant,"Tere! Ma saan aru, et teil on raskusi ID-tarkvara paigaldamisega. See on levinum probleem, kui Windows operatsioonisüsteem on tehtud uuendusi. Ilmselt Windowsi registris olevad vaimed vanad ID-tarkvara komponendid ei sobi uute versioonidega. Kas sa tead, mis vea kood oli?",output_ID.ee +5787d720-f062-49f2-a7de-73a03db5abb8,1,user,"Tere! Mul oli probleeme ID tarkvara paigaldamisega. Sai teateid, et paigaldus läks tõrkeid. Kuidas ma saan sellest aru ja mida peaks tegema?",output_ID.ee +5787d720-f062-49f2-a7de-73a03db5abb8,2,assistant,"Tere! Üsna sagedased probleemid. Selle peamiseks põhjuseks on see, et Windows registreis on vana ID tarkvara komponentide kirjeid, mis uue versiooni ei nõua. Vaevalt paigaldus lõpeb, täna on see probleem seotud Windowsi operatsioonisüsteemi uuendusega. Kas sulle kuvati mingisugune veateade? Kui võisa seda koodi või veateadet meelde tuua, oleks see väga hea.",output_ID.ee +25dfa3fa-c10a-42f8-867e-6dfeb723ba9e,1,user,"Tere! Ma pole päris kindel, mida tähendab ID-kaardi sertifitseerimine. Kas saaksid sulle teada, mida see täpselt on?",output_ID.ee +25dfa3fa-c10a-42f8-867e-6dfeb723ba9e,2,assistant,"Tere! Muidugi. ID-kaardi sertifitseerimine on protsess, millega ID-kaardi omanik tõestab oma identiteeti. See tähendab, et võid saada ID-kaardile juurdepääsu ja seda kasutada erinevates eesti avalikus veebis. Sertifitseerimisel kontrollitakse, et sa oled tegelikult see, kes sa väidate olevat. Hetkel on aktuaalne sertifitseerimispoliitika (https://www.id.ee/wp-content/uploads/2024/07/cp_esteid_v2.1.-.pdf), mille alustasime kasutama 16. augustil 2024. Kas sa soovid, et ma selle kohta rohkem täpsemat infot annaks?",output_ID.ee +3b90c826-9cdb-41bb-960b-64fee1b1f400,1,user,"Tere! Ma nägin, et ID.ee juures on sertifitseerimiskorra teatud muudatusi. Kas saaksite öelda, milline on uus sertifitseerimiskorra versioon praegu keesingil? Vaatan, et need muutuvad sageli...",output_ID.ee +3b90c826-9cdb-41bb-960b-64fee1b1f400,2,assistant,"Tere! Jah, on. Praegune sertifitseerimiskorra versioon on v2.1. See jõustus 16. augustil 2024. Olete õigesti tähendanud, et need muutused on suhteloolised. Kas teiega on juttu sellest, kuidas see muutmine mõjutab ID-kaardi omanikke? See on uusim versioon.",output_ID.ee +cfd3bede-7308-4805-bca9-6e51f88b7d19,1,user,Tere! Ma ei saa oma ID-kaardi PIN-koodi sisse sisestada. Mida peaks tegema?,output_ID.ee +cfd3bede-7308-4805-bca9-6e51f88b7d19,2,assistant,"Tere! Kui PIN-kood on kolm korda järjest valesti sisestatud, siis ID-kaart selle lukustab turvalisuse huvides. Palju ei parda, see on üsna levinum probleem. Sa saad PIN-koodi ise kiiresti lahti blokeerida – ei tarvitse PUK-koodi kasutada! Kas sul on PUK-koodi olemas, või sooviksime teada, kas sul seda pole?",output_ID.ee +861efae5-b8f0-402b-8a3d-88900b44431c,1,user,Tere! Ma ei saa oma ID-kaardi PIN-koodi lahti blokeerida. Mis ma peaks tegema? Kas see on kurb asi?,output_ID.ee +861efae5-b8f0-402b-8a3d-88900b44431c,2,assistant,"Tere! Ei ole kurb asi, seda saab lahti blokeerida. Kui sa kolm korda järjest valesti PIN-koodi sisestasid, siis ID-kaart seda turvalisuse huvides blokeeris. Sa saad oma PIN-koodi ise kiirelt ja mugavalt lahti blokeerida. Pühkige ID-kaardi PIN-koodi blokeerimise maha. Kuidas ma saan selle täna tegema?",output_ID.ee +c449b19a-b4e9-49a7-86ae-1c195334b311,1,user,"Tere! Ma olen ID-kaardi kasutamiseks DigiDoc4 programmi installinud, aga ei suuda seda oma arvutis leida. Kas teil on mõni nõu hulgimisel?",output_ID.ee +c449b19a-b4e9-49a7-86ae-1c195334b311,2,assistant,"Tere! Päev vastu! Jah, seda on lihtne leida. Kui sa oled ID-kaardi kasutamiseks DigiDoc4 programmi juba installinud, siis DigiDoc4 ikoon peaks arvuti töölaual olema. Kui seda ei ole, siis tee topeltklõps sellel, millel on 'D' tähe all nimega DigiDoc4. Kas sa leiad selle ikooni?",output_ID.ee +cea24990-7861-40b6-aa4b-0633b2d40d6d,1,user,"Tere! Ma ei suuda DigiDoc4 programmi oma arvutis leida, kui ma ID kaardi kasutada tahan. Kuidas ma seda tegema saan?",output_ID.ee +cea24990-7861-40b6-aa4b-0633b2d40d6d,2,assistant,"Tere! Helge, küta. DigiDoc4 programmi leidmiseks on erinevad viisad sõltuvalt teie arvuti operatsioonisüsteemist. Kas te kasutate Windowsit, macOS või Linuxi? Kui te Windowsit kasutate, siis programmi ikoon peaks automaatselt arvuti töölauale paigaldatud olema. Kas te seda näete?",output_ID.ee +c2e2f86f-c5f1-4939-b22c-cffaadb4e6c4,1,user,"Tere! Ma saan aru, et ma pean DigiDoc4-i oma arvutis leidma, aga ma ei suuda seda leida. Kas teil on mõni nõuadav?",output_ID.ee +c2e2f86f-c5f1-4939-b22c-cffaadb4e6c4,2,assistant,"Tere! Jah, küllap! Palju õnne, et kontrollisid. Üldiselt paigaldatakse DigiDoc4-i Windowsi arvutitesse automaatselt arvuti töölehele. Pärast paigaldust peaks töölehel olema DigiDoc4 ikoon. Kuidas see tööleht välja näeb? Kas sa näed seal midagi, kuhu sa võiksid hiirega topeltklõpsata?",output_ID.ee +abbe6a6e-5475-42db-9856-745cfc317a10,1,user,"Tere! Ma tean, et on mobiil-ID, aga kas see on nagu tavaline ID-kaart, aga telefoni sees?",output_ID.ee +abbe6a6e-5475-42db-9856-745cfc317a10,2,assistant,"Tere! Jah, täpselt nii! Mobiil-ID on SIM-kaardi põhjal toimiv digitaalne isikutunnistus, mis on samasugune kui tavaline ID-kaart (digi-ID, elamisloakaart) – see on riiklikult garanteeritud. See on lihtsalt kasutuses mobiilseadmega. Kas sulle on oluline teada, kuidas seda kasutada? Kas sa mõtled, kuidas taotleda või kuidas sellega tegelikult midagi teha? ",output_ID.ee +4ab54758-9362-45c1-bdc5-1b7a438871d2,1,user,"Tere! Ma ei ole päris kindel, mis Mobiil-ID on. Kas see on midagi sarnast ID-kaardile?",output_ID.ee +4ab54758-9362-45c1-bdc5-1b7a438871d2,2,assistant,"Tere! Jah, Mobiil-ID on täpselt nii nagu ID-kaart, aga see on digitaalne ja see on seotud sinu nutitelefoniga. See on SIM-kaardi põhjal toimiv digitaalne isikutunnistus. Politsei- ja Piirivalveamet väljastab selle sertifikaadid. Kas sa mõtled, mis see täpselt teeb?",output_ID.ee +b8718b25-89dd-4937-8b49-49fa06e1dff8,1,user,"Tere! Ma ei ole päris kindel, mis Mobiil-ID on. Kas see on uus ID-kaart nutitelefonile?",output_ID.ee +b8718b25-89dd-4937-8b49-49fa06e1dff8,2,assistant,"Tere! Jah, Mobiil-ID on täpselt selline – see on digitaalne isikutunnistus, mis on alati sinuga nutiseadmega kaasas. See põhineb SIM-kaardil ja on riikliku garantii all, nagu ka tavaline ID-kaart (digi-ID, elamisloakaart). See on väga sarnane, aga selle saab kasutada just nutiseadmest.",output_ID.ee +10abb77d-dd48-4812-8ac8-724a8707b4c0,1,user,"Tere! Ma arvan, et mobiil-ID on juurde tulnud. Aga kuidas see töötab? Kas ma pean mingit eraldi seadet kasutama?",output_ID.ee +10abb77d-dd48-4812-8ac8-724a8707b4c0,2,assistant,"Tere! Jah, mobiil-ID on juurde tulnud ja selle abil saad e-teenustesse sisse logida ilma ID-kaardita. Pead seda üldiselt eraldi seadetega kasutada ei pea - see töötab nii arvutis, tahvelarvutis kui ka telefonis. Kui sa seda kasutad, siis piisab sellest, et sa mäletaksid oma mobiil-ID PIN-koode. Kas sul on kunagi olnud ID-kaart või oskad sellega sisse logida?",output_ID.ee +d0d64824-f96f-4b51-a406-186918c9e150,1,user,"Tere! Ma ei ole päris kindel, kuidas mobiil-ID töötab. Kas ma pean mingi eraldi seadme hankima, et seda kasutada?",output_ID.ee +d0d64824-f96f-4b51-a406-186918c9e150,2,assistant,"Tere! Ei pea. Mobiil-ID töötab nii arvutis, tahvelarvutis kui ka mobiiltelefonis. Pea kõik suuremad e-teenused, mis pakuvad ID-kaardiga sisenemise võimalust, toetavad seda. Kui sa tead oma mobiil-ID PIN-koode, siis saad neid kasutada!",output_ID.ee +9bd0d484-709c-43b6-9d71-428e31141ed0,1,user,"Tere! Kas te saate öelda, mis RIA DigiDoc on?",output_ID.ee +9bd0d484-709c-43b6-9d71-428e31141ed0,2,assistant,"Tere! RIA DigiDoc on mobiilirakendus, mis on mõeldud ID-kaardi, Smart-ID või mobiil-ID abil dokumente digitaalselt allkirjastama. See on nagu ID-tarkvara, aga nutitelefonis!",output_ID.ee +e12353b2-197f-4755-96c9-8a30cb6151f3,1,user,"Tere! Kas te saate rääkida sellest RIA DigiDoc mobiilirakendusest, mida ID.ee teeb?",output_ID.ee +e12353b2-197f-4755-96c9-8a30cb6151f3,2,assistant,"Muidugi! RIA DigiDoc on mobiilirakendus, mis on mõeldud ID-kaardi, Smart-ID ja mobiil-ID abil dokumentide digitaalse allkirjastamisele. See on tasuta saadaval App Store'ist ja Google Play poest. Kas te olete huvitatud teadma, mida sellega saab teha?",output_ID.ee +f43f4ad1-1b15-433d-94d1-513c3feb0cd5,1,user,"Tere! Ma arvan, et minu ID-kaart on kadunud. Kas ma pean midagi tegema selle kaitse tagamiseks?",output_ID.ee +f43f4ad1-1b15-433d-94d1-513c3feb0cd5,2,assistant,"Tere! Jah, väga hea, et mõtled sellele. Kui sa arvad, et ID-kaart on kadunud või varastatud, on oluline kohe sertifikaadid peatada. See aitab varguse korral sinu identiteeti kaitsmata jätta. Kas sa saaksid kasvõi lehel http://www.id.ee vaadata, mis seotud sertifikaate pead peatama?",output_ID.ee +89a0d3f6-ca15-4d2b-8ee8-a3dd88c10cf4,1,user,Tere! Ma jätsin oma ID-kaardi kaduma. Kas ma pean ka sellega seotud sertifikaadid peatama? Ja kuidas seda teen?,output_ID.ee +89a0d3f6-ca15-4d2b-8ee8-a3dd88c10cf4,2,assistant,"Tere! Jah, kindlasti! Kui olete kindel, et teie ID-kaart on kadunud või varastatud, siis peate sellega seotud sertifikaadid peatama, et varguse korral oleks teie identiteet paremini kaitstud. Seda saab teha telefoni teel või Politsei- ja Piirivalveameti teeninduses. Mida tegelikult peame tegema, on nii, et sertifikaat muutub mittesobivaks. Kas teiega oleks mugav telefoni teel küsida, kuidas see toimib?",output_ID.ee +2c649cf1-79f8-465d-b1aa-ea7f4296e0f2,1,user,"Tere! Ma arvan, et minu ID-kaart on kadunud. Mis ma peaksin tegema, et mu andmeid kaitsta?",output_ID.ee +2c649cf1-79f8-465d-b1aa-ea7f4296e0f2,2,assistant,"Tere! See on murettekitav. Kui olete kindel, et ID-kaart on kadunud või varastatud, peaksite koheselt sellega seotud sertifikaadid peatama. See on väga tähtis, et varguse korral peaksite oma identiteeti maksimaalselt kaitsma. Kas teiega on selge, mida sertifikaatide peatamine tähendab? Peatamine tähendab, et sertifikaat ei ole enam kehtiv. Kui teiega on selge, siis võime järjadel selle peatamisega tegeleda.",output_ID.ee +cd66b1b7-2be9-4236-bc44-2759559ecdd2,1,user,"Tere! Ma arvan, et Riigiportaali eesti.ee on teadaolev asi, aga ma ei ole kindel, kuidas seesama eesti.ee kasutada. Kas sa saaksid mulle aidata?",output_ID.ee +cd66b1b7-2be9-4236-bc44-2759559ecdd2,2,assistant,"Muidugi! Riigiportaali eesti.ee on üsna lahe asi. See on üks keskoht, kus saad kõik oma riigiga seotud teave ühes kohas. Kas sa oled mõelnud, mida selle kaudu teha saaksid? Näiteks, kas sul on küpsus testi tulemused, mis sa tahaksid kontrollida?",output_ID.ee +e08b28cf-e181-4691-bda2-cbc3edff0ab5,1,user,"Tere! Kas saaksite öelda, mida Riigiportaalis eesti.ee saab teha?",output_ID.ee +e08b28cf-e181-4691-bda2-cbc3edff0ab5,2,assistant,"Muidugi! Riigiportaalis eesti.ee on väga palju erinevaid asju. See on keskoht, kus saab hallata oma riigiga seotud infot. Näiteks saab kontrollida digiretsepte, haridusinformaat, vaadata oma elukohaandmeid ja palju muud. Põhimõtteliselt on see keskoht, kust saab juurde kõikvõimalikku infot, mis sinu elukaasale kuuluvaid asju puudutab. Kas saaksite öelda, millest te konkreetselt huvitatud olete?",output_ID.ee +a1330e02-2eba-44b2-90e7-2096a014cdc9,1,user,"Tere! Kas saaksite mulle öelda, mis on Riigiportaal eesti.ee?",output_ID.ee +a1330e02-2eba-44b2-90e7-2096a014cdc9,2,assistant,"Muidugi! Riigiportaal eesti.ee on Eesti uus digiregionsümbol. See on üks keskoht, kus saate hallata kõiki oma riigiga seotud infot ühes kohas. See on nagu ühtne keskkaabel, mis koondab palju erinevaid teenuseid ja infot.",output_ID.ee +12509224-15ba-4dc8-a944-a430bf246a4c,1,user,Tere! Mis on Web eID?,output_ID.ee +12509224-15ba-4dc8-a944-a430bf246a4c,2,assistant,"Tere! Web eID on uus süsteem, mis võimaldab teid veebis turvaliselt identifitseerida ja oma dokumendid allkirjastada. See tähendab, et saate kasutada oma ID-kaarti või digi-kaarti veebis, ilma seda kaardi enda eemardamata. See on uus arhitektuur selleks, et tagada parem turvalisus.",output_ID.ee +74583fd7-3339-4329-b30a-537f8dfeaa68,1,user,Tere! Mul on probleeme ID-kaardi PIN-koodiga. Kas ma saan uue PIN-koodi taotleda?,output_ID.ee +74583fd7-3339-4329-b30a-537f8dfeaa68,2,assistant,"Tere! Jah, seda saab. Uue PIN-koodi ümbriku taotlemiseks pead sa pöörduma Politsei- ja Piirivalveameti teenindusse või Eesti Vabariigi välisesindusse. Palun võta kaasa isikut tõendava dokumendi! Kas sa tead, kus on lähim Politsei- ja Piirivalveameti teenindus?",output_ID.ee +326a810d-3e56-4f92-af63-04ef14637113,1,user,"Tere! Ma saan aru, et ID-kaardi PIN-koodid võivad kaduda. Kuidas saan uue PIN-koodi taotleda?",output_ID.ee +326a810d-3e56-4f92-af63-04ef14637113,2,assistant,"Tere! Jah, see on võimalik. Uue PIN-koodi taotlemiseks pead pöörduma Politsei- ja Piirivalveameti teenindusse või Eesti Vabariigi välisesindusse. On oluline kaasa võtta isikut tõendav dokument. Kas sa tead, kus on lähim Politsei- ja Piirivalveameti asukoht?",output_ID.ee +daff3920-21c5-4e8b-a0da-2346e2cad62f,1,user,"Tere! Ma arvan, et on probleeme ID-kaardi kasutamisega. Kuidas saan abi?",output_ID.ee +daff3920-21c5-4e8b-a0da-2346e2cad62f,2,assistant,"Tere! Päev vastu. Kui sul on probleeme ID-kaardi või mobiil-ID-ga, on ID.ee veebist hea abi leida. Palju informatsiooni ja vastuseid seal leiad: id.ee. Sa võid ka ID-abikeskusega ühendust võtta läbi nende veebiformi, et saada personaalsemat abi.",output_ID.ee +536dad76-6ca5-43be-a3a6-86e38f30524e,1,user,"Tere! Ma saan probleeme ID-kaardi kasutamisega. Kas teil on ID.ee veebis kohta, kus ma saan vastuseid? Kas see on midagi, mis on loodud just selleks? ",output_ID.ee +536dad76-6ca5-43be-a3a6-86e38f30524e,2,assistant,"Tere! Jah, ID.ee veebis on täiesti loodud suur hulk teavet ID-kaardi ja mobiil-ID kohta. Sa võid seda kasutada, kui sul on probleeme. Palun kasuta meie otsingut – see on kõige parem viis leida just seda, mida sa otsid. Kui sul on raskusi leplikust või oled märganud midagi ebaselge, palun anna teada!",output_ID.ee +f7b33175-1746-4784-9221-f1ed306bb313,1,user,"Tere! Ma arvan, et probleem on ID kaardiga. Ma ei saa seda kasutada. Kas teil on midagi, mis aitab?",output_ID.ee +f7b33175-1746-4784-9221-f1ed306bb313,2,assistant,"Tere! Jah, meil on ID kaardiga seotud küsimustele vastuseid. Palju täpsemalt, kui probleem on, seda parem on abi. Kas saate palun öelda, mis täpselt probleem on? Kas on viga lugemisega, või ei saa seda üldse kasutada?",output_ID.ee +7de9b696-e4ea-49f9-a2fc-043dead8141c,1,user,"Tere! Ma arvan, et mobiil-ID on hea asi, aga kuidas seda saab tellida? Ma ei tea, kust alustada.",output_ID.ee +7de9b696-e4ea-49f9-a2fc-043dead8141c,2,assistant,"Tere! Päris hea küsimus. Mobiil-ID tellimine on päris lihtne. Seda saate tellida oma mobiilsideoperaatori juures - Telia, Elisa või Tele2. Kõik kolm pakuvad seda serviisi. Palju lihtsam on, kui saad teada, millise operaatori pakub sulle kõige sobivam teenus!",output_ID.ee +a92bd603-7350-40ff-98e6-ebb4066f1d43,1,user,"Tere! Ma olen kuulnud Mobiil-ID kohta, aga ei ole kindel, mis see on ja kuidas see töötab. Kas saaksite seda veidi selgitada?",output_ID.ee +a92bd603-7350-40ff-98e6-ebb4066f1d43,2,assistant,"Muidugi! Mobiil-ID on elektrooniline isikutuvastussüsteem, mis võimaldab teil sisse logida erinevate e-teenustega teie mobiiltelefoniga. See tähendab, et te ei pea pidevalt kaasas hoida ID-kaarti. Kas te teadate, et Mobiil-ID on turvaline ja seda saab kasutada seadmeüleselt – nutitelefonist, tahvelarvutist või arvutist. Kas soovite rohkem teada selle kohta?",output_ID.ee +bf0dcb05-b033-4502-a7dd-6248e872a70d,1,user,"Tere! Ma mõtlesin, mis see Mobiil-ID on? Kas see on uus ID-kaart?",output_ID.ee +bf0dcb05-b033-4502-a7dd-6248e872a70d,2,assistant,"Tere! Mobiil-ID on juurdekasv ID-kaardi kasutamisele. See on mobiiltelefoniga kaasasolev vebinimiliselt kindel identifikaator. See annab teile juurdejuurdepääsu e-teenustele, ilma et peaksite ID-kaarti kaasas kandma. Kas te mõtlete, et see on samasugune kui ID-kaart? See on juurdekasv!",output_ID.ee +ccc272f6-81d4-4041-8868-2f0e6480bc56,1,user,"Tere! Ma arvan, et ma saan aru, et me räägime ID-kaardi sertifikaatidest. Aga kuidas need sertifikaadid üldse töötavad? Miks on need vajalikud?",output_ID.ee +ccc272f6-81d4-4041-8868-2f0e6480bc56,2,assistant,"Tere! Jah, täiesti! Sertifikaadid on olulised ID-kaardi, elamisloakaardi, digi-ID ja diplomaatilise isikutunnistuse kasutamisel e-teenustes. Need on nagu digitaalne võti, mis võimaldab teil kindlustada oma identiteeti veebis. Need kinnitavad, et te olete selle dokumendi omanik.",output_ID.ee +9a463957-4e62-4b6e-9e86-a114d526c448,1,user,Tere! Kas mul on vaja sertifikaate ID-kaardi jaoks? Ma ei ole kindel.,output_ID.ee +9a463957-4e62-4b6e-9e86-a114d526c448,2,assistant,"Tere! Jah, ID-kaardi kasutamiseks on vajalikud sertifikaadid. Need garanteerivad teie identiteedi elektroonilises keskkonnas. Need annavad võimaluse sisse logida e-teenustesse ja digitaalseid allkirju anduda. Kas te olete varem sertifikaate kasutanud?",output_ID.ee +41787cd6-c51d-49e9-bdb5-35409197cdba,1,user,"Tere! Ma arvan, et digiallkirjastamine on üsna oluline, aga ei suuda päris aru saada, kuidas seda enda e-teenuses realiseerida. Kas saate öelda, milliseid võimalusi on olemas?",output_ID.ee +41787cd6-c51d-49e9-bdb5-35409197cdba,2,assistant,"Tere! Muidugi. Digiallkirjastamise realiseerimiseks on mitu võimalikku lahendust, mis sõtuvad valitsusektori ja erasektori lahendustega. Valitsusektori asutused saavad kasutada Riigi allkirjastamisteenust (https://www.ria.ee/riigi-infosusteem/elektrooniline-identiteet-ja-usaldusteenused/digiallkirja-serverteenused) . See pakub ID-kaardi, mobiil-ID, Smart-ID ja e-Templiga digiallkirjastamise tuge. Samuti on olemas mitmed teed, mida saate kasutada erasektoris. Kas teiega oleks huvitav ka uurida erasektori lahendusi?",output_ID.ee +e5e47028-6555-4bb0-9cd8-69f7d2051462,1,user,"Tere! Ma arvan, et olen kuuldud digiallkirjadest, aga ma ei ole kindel, mida need täpsemalt tähendavad ja kuidas need minu e-teenuses kasutamiseks soetada?",output_ID.ee +e5e47028-6555-4bb0-9cd8-69f7d2051462,2,assistant,"Tere! Palju maas! Digiallkirjad on oluline osa e-teenuste turvalisusest. Neil on võti sellele, et dokumendid ja tehingud on autentse ja usaldusväärse, ning neid ei saa muuta ilma, et teist ei teatsidaks. Paljuski seega on need olulised selleks, et mu e-teenuses toimuvatest asjadest oleks kindel, et on autentse. Arvestades seda, saame rääkida selle kohta rohkem, aga kõigepealt saame aru, kuidas neid üldjuhul saame teostada. Kõige tavalisemad viisid on ID-kaardiga, mobiil-ID või Smart-ID. Kas sa tahaksid teada ühe neist põhjalikumalt?",output_ID.ee +b3d36237-153a-40e7-a9f0-c4b275cdc34a,1,user,"Tere! Ma arvan, et tean, et ID.ee on olemas, aga ma ei ole päris kindel, kuidas digiallkirjastamist oma veebisüsteemi juurde lisada. Kas teil on näiteks lihtne viis, kuidas seda saaksin teha?",output_ID.ee +b3d36237-153a-40e7-a9f0-c4b275cdc34a,2,assistant,"Tere! Päev! Jah, digiallkirjastamist saab lisada oma veebisüsteemi mitmel viisil. Alustuseks on Riigi allkirjastamisteenus (SiGa) valitsusektori jaoks väga hea võimalus. See on aga tarkvara, mida peab enda serverile paigaldama. Kas teete veebirakendusi väljapoole valitsust?",output_ID.ee +28cd7dd4-3ff1-4052-add6-89ab650c9117,1,user,"Tere! Ma sooviks teada, kuidas digiallkirja anda ID.ee rakenduses DigiDoc4 kaudu. Kas see on väga keerukas?",output_ID.ee +28cd7dd4-3ff1-4052-add6-89ab650c9117,2,assistant,"Tere! Ei ole keerukas! Digiallkirja andmine DigiDoc4-s on üsna lihtne. Alguses pead valima, millega sa allkirja annad – kas ID-kaardiga, mobiil-IDga või Smart-IDga. Kas sul on neist ükski neist olemas?",output_ID.ee +e36289bb-ccfd-41dd-b697-920fa8dd6e0b,1,user,Tere! Ma soovin digiallkirja anda ID.ee rakenduses. Kuidas ma sellega alustan?,output_ID.ee +e36289bb-ccfd-41dd-b697-920fa8dd6e0b,2,assistant,"Tere! Digiallkirja andmine on päris lihtne. Alustada saate kahel viisil: kas ID-kaardi või mobiil-ID abil. Kõigepealt pead tegema kindlaks, millise meetodi te kasutate. Kas teil on ID-kaart või mobiil-ID?",output_ID.ee +78902e01-5b66-417a-bb0d-418d662190ac,1,user,"Tere! Ma mõtlesin, et saan nüüd digi-ID kaardi? Kas see on ID-kaardi asendaja?",output_ID.ee +78902e01-5b66-417a-bb0d-418d662190ac,2,assistant,"Tere! Jah, digi-ID on mõeldud ID-kaardi asendajaks elektroonilises keskkonnas. See on digitaalne dokument, millega saab isikut tõendada ja allkirja anda. See on oluline, et olla turvalisem ja mugavam elektroonilistes teenustes.",output_ID.ee +60392241-d3b3-40c3-af0e-1c556ca4abd4,1,user,Tere! Ma kuulasin rääkimist eIDAS-ist. Kas te võtaksite need mõned e-tehingud käsitlevaid küsimusi vastu? ,output_ID.ee +60392241-d3b3-40c3-af0e-1c556ca4abd4,2,assistant,"Tere! Muidugi, võtan need küsimused vastu. eIDAS ongi Euroopa Liidu määrus, mis on loodud selleks, et lihtsustada piiriülest e-teenuste kasutamist. Kas sa tead, milles see lihtsustamine just seisab? ",output_ID.ee +567ebbec-2216-4fa5-829d-8c47b9f4a42d,1,user,"Tere! Ma ei ole päris kindel, mis on eIDAS. Kas saate mulle sellest natuke selgeks teha? See on midagi, mis seotakse ID.ee?",output_ID.ee +567ebbec-2216-4fa5-829d-8c47b9f4a42d,2,assistant,"Tere! Jah, eIDAS on Euroopa Liidu määrus, mis seotakse ID.ee-ga. Lihtsasti öeldud, see on seadus, mis näeb ette, kuidas digiteenuseid ja e-tehinguid turvaliselt identifitseerida ja kinnitada. See aitab inimestel digimaailmas ohksele saada. See määrus jõudis jõule 23. juulil 2014. aastal.",output_ID.ee +7ba5d75f-07f7-44c9-b7c2-79e5afa60fbc,1,user,"Tere! Ma saan nüüd ID-kaardi ja Digi-ID, siis saan ka koodiümbriku? Ja mida see koodiümbrik on?",output_ID.ee +7ba5d75f-07f7-44c9-b7c2-79e5afa60fbc,2,assistant,"Tere! Jah, kui teil on ID-kaart või Digi-ID, siis saate ka koodiümbriku. See on kindlik koodiümbrik, mis sisaldab PIN- ja PUK-koodi. Need koodid on vajalikud, et saaksite ID-kaardi ja Digi-ID elektrooniliselt kasutada. See on nagu turvafirma sild, mis võimaldab kaardi turvaliselt kasutada.",output_ID.ee +ea8b6c2a-df3f-4164-b58c-71ff7912f9a7,1,user,"Tere! Ma sain uue ID-kaardi. Mis on see koodiümbrik, mille kaasa tuli? Kas ma pean seda aktiveerima?",output_ID.ee +ea8b6c2a-df3f-4164-b58c-71ff7912f9a7,2,assistant,"Tere! Päevad! Uue ID-kaardi puhul sa saad kaasa koodiümbriku, mille sees on PIN1, PIN2 ja PUK koodid. Seda ei pea eraldi aktiveerima, see töötab koheselt.",output_ID.ee +ed327007-efe3-4f50-b272-bf8dc6f3d4c3,1,user,"Tere! Ma arvan, et ID-tarkvara on päris oluline. Kas te saate mulle öelda, mis tarkvaraversiooni toeperioodi pikkus tähendab?",output_ID.ee +ed327007-efe3-4f50-b272-bf8dc6f3d4c3,2,assistant,"Tere! Jah, seda on oluline mõistata. Tarkvaraversiooni toeperioodi lõpp tähendab, et ID kasutajatugi enam seda versiooni teadmistebaasis enam ei uuenda. Lisaks sellele ei testita enam selles versioonis viidatud vigade olemasolu, ja tehnilisi lahenduse muudatusettepanekuid ei vastu võeta.",output_ID.ee +b8588622-0f0c-4536-be14-21431d8e1231,1,user,"Tere! Ma arvan, et ID-tarkvara on minu jaoks väga oluline. Aga mis see ‘toeperioodi pikkus’ tähendab? Ja miks see on nii tähtis?",output_ID.ee +b8588622-0f0c-4536-be14-21431d8e1231,2,assistant,"Tere! Päev vastu on ja ma saan aru, et see on uus. ‘Toeperioodi pikkus’ ID-tarkvara kontekstis tähendab seda, kui kaua ID-tarkvaraversioon on aktiivne ja et selle versiooni kohta pakutakse toetust. Kui versioon on ‘elatsükli lõppenud’, enam ei ole selle versiooni kohta ID kasutajatugi (teadmistebaasis), ei testita sellega seotud vigade leidimist ja vastu ei võeta tegelikult mitte mitte mitte midagi - ei uut tehnilist lahendust, ei funktsiooni muudatuste taotlusi. Peamine põhjus on turvajärjekorra. Vajalik on alati uusima versiooni kasutada. Kas sulle on plaanis uuendamine?",output_ID.ee +3f7fd49f-78fa-49c4-9b60-2e688fb660b6,1,user,"Tere! Ma ei ole päris kindel, mida tarkvaraversiooni toeperiood tähendab ID-tarkvaras. Kas sa saaksid seda selgitada?",output_ID.ee +3f7fd49f-78fa-49c4-9b60-2e688fb660b6,2,assistant,"Muidugi! Tarkvaraversiooni toeperiood ID-tarkvaras tähendab, et ID kasutajatugi enam selle versiooni kohta teavet teadmistebaasis ei uuenda. See on vôi siis, kui tarkvara versioon enam ei testi probleemide puhul, ja sellele ei vastueta muutusi. Kas see on selge?",output_ID.ee +be066a9c-68df-4be7-9018-a9f85dd35037,1,user,"Tere! Ma arvan, et mul on probleeme, kuidas sisse logida erinevate riiklike e-teenustesse. Kas ma pean iga kord uuesti sisse logima?",output_ID.ee +be066a9c-68df-4be7-9018-a9f85dd35037,2,assistant,"Tere! Päriselt, see on levinud küsimus. Jah, varem oli nii, et iga kord pead sisse logima. Aga nüüd on võimalik kasutada ühte sisselogimist, mis aitab sind säästida. See on võimalik läbi Riigi autentimisteenuse.",output_ID.ee +19c63307-10f7-4340-830e-056716ff0688,1,user,"Tere! Ma ei ole päris kindel, kuidas riiklikes e-teenustes toimib autentimine. Kas ma pean iga kord, kui ma mõnda teenust saan, uuesti ennast sisse logima?",output_ID.ee +19c63307-10f7-4340-830e-056716ff0688,2,assistant,"Tere! Jah, päriselt on nii. Riiklikes e-teenustes pead alati end autentima. See tähendab, et pead tõendama, et sa oled see, kes väidad olevat. Autentimiseks on saadaval ID-kaart, Mobiil-ID, Smart-ID ja ka Euroopa Liidu riikide autentimisvahendid. Küll aga toimib see keskse turvalise kanali, Riigi autentimisteenuse kaudu. Kas sa mõtled, millise autentimisvahendiga sa tavaliselt seda teed?",output_ID.ee +ac2d8478-e033-45fe-9c9c-bf42affbc207,1,user,"Tere! Ma ei ole päris kindel, kuidas riiklikes e-teenustes autentimine töötab. Kas ma pean iga kord, kui ma mõnda e-teenust kasutan, ise uuesti sisse logima?",output_ID.ee +ac2d8478-e033-45fe-9c9c-bf42affbc207,2,assistant,"Tere! See on väga hea küsimus. Põhjaosalt jah, igas e-teenuses pead peaaegu alati uuesti sisse logima. Küll aga on olemas võimalus, et kui sa oled juba ühe e-teenuse kaudu sisse logitud, siis saad sama sisselogimiskättelisuse ka teistes e-teenustes kasutada. Seda ei pea iga kord uuesti tegema.",output_ID.ee +11b61e8c-aa8c-4437-b572-184fd867d53b,1,user,Tere! Kas ma saan dokumendid RIA DigiDoc mobiilirakenduses digiallkirjastada?,output_ID.ee +11b61e8c-aa8c-4437-b572-184fd867d53b,2,assistant,"Kuigi, jah! Digiallkirjastamine RIA DigiDoc mobiilirakenduses on üsna lihtne. Kas sa kasutad ID-kaarti, mobiil-ID või Smart-ID? Alustame kõige lihtsamaga - kas sa kasutad ID-kaarti? ",output_ID.ee +abcbf8e5-114d-466a-9383-39bc1c0810f4,1,user,Tere! Ma pole kunagi digiallkirjastanud midagi mobiiliga. Kas see on üldse lihtne?,output_ID.ee +abcbf8e5-114d-466a-9383-39bc1c0810f4,2,assistant,"Tere! Jah, digiallkirjastamine RIA DigiDoc mobiilirakendusega on väga lihtne. See on mõeldud nii ID-kaardi, mobiil-ID kui ka Smart-ID kasutajatele. Pärast lohistamist saab sa sellega lihtsalt dokumente allkirjastada. Kas sul on ID-kaart, mobiil-ID või Smart-ID?",output_ID.ee +29363421-7329-4264-b488-824e89cd645b,1,user,Tere! Ma soovin dokumente digiallkirjastada mobiilirakendusega DigiDoc. Kuidas see kõigepeigem toimub?,output_ID.ee +29363421-7329-4264-b488-824e89cd645b,2,assistant,"Tere! Päevapeale. Digiallkirjastamine DigiDoc mobiilirakenduses on üsna lihtne. Põhiline on see, et seda saab teha ID-kaardi, mobiil-ID või Smart-ID abil. Kõigepealt tuleb avada rakendus ja sealt menüüst valida 'Allkirjasta'. Kas sa kasutad ID-kaarti, mobiil-ID või Smart-ID-d?",output_ID.ee +85009eff-aa6c-4c3e-b013-dfb914d60596,1,user,"Tere! Ma usun, et RIA DigiDoc mobiilirakendus peaks olema kõig jaoks kasutatav. Kas te saate rääkida sellest, kuidas selle ligipääsetavus on?",output_ID.ee +85009eff-aa6c-4c3e-b013-dfb914d60596,2,assistant,"Tere! Jah, seda küsitaksegi. RIA DigiDoc mobiilirakendus on ehitatud WCAG 2.1 AA suuniste järgi, et see oleks kasutatav erinevate puudete inimestele. Kuid tegelikult ei ole see täielikult ligipääsetav. Mida tähendab see tekitamas olevas teates?",output_ID.ee +8a9e0df4-469b-4bec-9632-29de97769c08,1,user,"Tere! Ma arvan, et kasutasin RIA DigiDoc'i mobiilirakendust. Kas teil on teada, kas see on hästi ligipääsetav? Või on mõnes peast kohas probleemid?",output_ID.ee +8a9e0df4-469b-4bec-9632-29de97769c08,2,assistant,"Tere! Jah, RIA DigiDoc mobiilirakendus on mõeldud olema ligipääsetav. Kuid seegi, et see on täielikult hea, on keeruline. Teatetes on kirjas, et osa funktsioonidest ei vasta täielikult WCAG 2.1 AA suunistele. Palju on parameetreid, millele on vaja tähelepanu pöörata. Näiteks on osa väljade kontrastsus väga väike. Kas te seda rakendust just kasutasite ja tekkis teil midagi?",output_ID.ee +2728853c-5475-44a2-a75e-36db99a96e13,1,user,"Tere! Ma saan aru, et RIA DigiDoc on mobiilirakendus. Kas see on täielikult ligipääsetav kõigile kasutajatele, kes seda kasutavad?",output_ID.ee +2728853c-5475-44a2-a75e-36db99a96e13,2,assistant,"Tere! Jah, RIA DigiDoc on ehitatud vastavalt WCAG 2.1 AA suunetele, mis tähendab, et seda on mõeldud kasutamiseks ka inimestele, kellel on nägemisvõimete puudega või muud liiklusvõimete probleemid. Küll aga on teatud piirkonnad, kus ligipääsetavus pole täielik. Meie teates on detailsemalt kirjas, milles need puudumised on.",output_ID.ee +818ddff5-40d7-418d-816e-cb940df8b0f7,1,user,Tere! Ma ei ole pärijadega tuttav elektrooniliste allkirjadega. Mis see eIDAS asi üldse on ja miks see oluline on?,output_ID.ee +818ddff5-40d7-418d-816e-cb940df8b0f7,2,assistant,"Tere! EIDAS on Euroopa Liidus kehtiv määrus, mis on loodud, et elektrooniliste teenuste kasutamine oleks lihtsam ja ühtlasem kogu Euroopas. Põhidepõhimõte on see, et erinevate riikide allkirjad oleksid üksteisel põhjal võrdsed. Kasutajad saavad siis ka üle piiri elektroonilisi teenuseid kasutada, näiteks sularaha laenu taotlemisel. Kas sulle see idee üldse mõistab?",output_ID.ee +26606987-1914-4362-81a2-187b41f0a8c8,1,user,"Tere! Ma pole päris kindel, mis need elektroonilised allkirjad on ja miks need nii olulised on Euroopas. Kas sa võiksid seda lihtsamalt seletada?",output_ID.ee +26606987-1914-4362-81a2-187b41f0a8c8,2,assistant,"Tere! Muidugi. Elektroonilised allkirjad on nagu tavaline allkiri, aga see on tehtud arvutis. Need on olulised, sest need aitavad meil e-tehinguteselt kindlust saada, et tegelikult on see just sina, kes selle allkirjastanud oled. Euroopas on selleks vajalikud reeglid, et nad oleksid kõik maailmas ühesugused.",output_ID.ee +f7a27532-fa90-4db0-b603-879bb59c6454,1,user,"Tere! Ma arvasin, et ID-kaardiga saab reisida, aga ma pole kindel. Kus ma saan teada, kuhu just saan ID-kaardiga reisida?",output_ID.ee +f7a27532-fa90-4db0-b603-879bb59c6454,2,assistant,"Tere! Vastus on, et ID-kaardiga saab reisida Euroopa Liidu (EL) ja Euroopa Majanduspiirkonna (EMP) liikmesriikidesse. Aga kõige täpsema info saad ReisiTargalt veebilehel: https://reisitargalt.vm.ee/kontrolli-reisidokumente/. Seal näeb, millistesse riikidesse ID-kaardiga reisida on lubatud.",output_ID.ee +08821dc8-00f8-4f7f-b904-b37c6655058e,1,user,"Tere! Ma arvan, et ma saan ID-kaardiga ilma passita Euroopasse reisida? Kas see on võimalik?",output_ID.ee +08821dc8-00f8-4f7f-b904-b37c6655058e,2,assistant,"Tere! Jah, ID-kaardiga saate reisida Euroopa Liitu (EL) ja Euroopa Majanduspiirkonda (EMP) kuuluvad riigid ilma passi kohta. See on võimalik, aga oluline on meeles pidada, et ID-kaart on vajalik reisidokumendina ja EI sobi paljude teiste dokumentidega, nagu näiteks elamisloa kaart või digi-ID.",output_ID.ee +90b0348d-2d30-43bd-b862-fe1d22b8febd,1,user,Tere! Kas ma saan ID-kaardiga reisida Euroopa Liitu? Kas mul peab olema pass? ,output_ID.ee +90b0348d-2d30-43bd-b862-fe1d22b8febd,2,assistant,"Tere! Jah, ID-kaardiga saad reisida Euroopa Liitu ja Euroopa Majanduspiirkonna liikmesriikidesse ilma passi kohta. Pass on aga alati vajalik dokument, sest ID-kaart on ainult reisidokumendina lubatud. Kas sa mõtled täpseid riikseid reegleid? ",output_ID.ee +07371b4d-54d4-4efd-8d1e-1245e06cb78d,1,user,"Tere! Ma arvan, et digitaalsed dokumendid on nagu tavalised dokumendid, aga elektroonilised. Kas seda täiesti õigesti arvan?",output_ID.ee +07371b4d-54d4-4efd-8d1e-1245e06cb78d,2,assistant,"Tere! Üsna täiesti õigesti! Digitaalsed dokumendid on tegelikult tavalised dokumendid – nagu ID-kaart, elukirja või passi – aga need on kujutatud digitaalselt, elektroniliselt. See tähendab, et neid saab hoida ja kasutada numbriliselt, mitte ainult paberil. Kas sa tahad teada rohkem nende erinevatest tüüpides või sellest, kuidas neid kasutada?",output_ID.ee +940f5a74-6c8a-4bb4-b891-820ea77a2f6f,1,user,"Tere! Ma arvan, et digitaalsed dokumendid on kirsileht... mida see tähendab ja miks need on olulised?",output_ID.ee +940f5a74-6c8a-4bb4-b891-820ea77a2f6f,2,assistant,"Tere! Kirsileht on hea võrdlus. Digitaalsed dokumendid on tegelikult elektroonilised tõendid, mis kinnitavad teie identiteeti, näiteks ID-kaart, digi-ID, või e-residentide digi-ID. Nende eesmärk on aidata teiega identifitseerida internetis ja elektroonilistes teenustes. Kas sa arvad, et need on olulised, ja miks?",output_ID.ee +1635fcfb-3759-4e5c-9911-dedefd5d18dd,1,user,"Tere! Kas sa oskad öelda, milleks mobiil-ID on?",output_ID.ee +1635fcfb-3759-4e5c-9911-dedefd5d18dd,2,assistant,"Tere! Mobiil-ID on digitaalne identifikaator, mida saab kasutada erinevates e-teenustes, näiteks pankades, riigilikus ja muudes võrketeenustes. See võimaldab sul e-teenustes sisse logida ühtse identifikaatori abil, mitte iga kord erineva kasutajanime ja salasõna.",output_ID.ee +71f46244-295f-4b4c-95b0-6d53103742e8,1,user,Tere! Kas sa oskad rääguda mobiil-ID-st? Ma pole sellest midagi kuulnud.,output_ID.ee +71f46244-295f-4b4c-95b0-6d53103742e8,2,assistant,"Tere! Jah, ma oskan. Mobiil-ID on digitaalne identifikaator, mis võimaldab sul kasutada oma nutitelefoniga e-teenuseid, nagu näiteks pangaülekanded või võrreldamatud kohvrid. See on nagu digitaalne võti! Kas sa tahad teada, kuidas see töötab?",output_ID.ee +591da53d-3d64-4486-b96c-ca6153b3ed70,1,user,Tere! Ma pole kunagi enne mobiil-ID'd kasutanud. Mis see üldse on ja kuidas seda kasutada?,output_ID.ee +591da53d-3d64-4486-b96c-ca6153b3ed70,2,assistant,"Tere! Mobili-ID on digitaalne identifikaator, mis võimaldab sul turvaliselt sisse logida erinevate e-teenustesse, näiteks e-posti, pankadesse ja ametiasutuste veebilehtudele. See on nagu digitaalne isikutunnus. Seda saab kasutada ka dokumendi allkirjastamiseks. Alguses on see pisemaks saamiseks eelistatud meetod, mis sobib enamusele inimestele. Kas sa tead, kas sul on juba mobiil-ID?",output_ID.ee +31f16542-9d8e-413b-aa0b-51aab616d0b4,1,user,Tere! Ma pole kunagi faili krüpteerinud. Kas DigiDoc4 abil saab faili turvaliseks teha?,output_ID.ee +31f16542-9d8e-413b-aa0b-51aab616d0b4,2,assistant,"Tere! Jah, DigiDoc4 võimaldab just seda. Faili krüpteerimine on oluline, et hoida faili sisu eemale võõrastelt pilgutelt. See on nagu omaette karp, mis on lukustatud. Kas sulle on teada, mis krüpteerimine täpselt on?",output_ID.ee +59a983d1-4faa-4ff3-a340-7c89d658bd27,1,user,"Tere! Ma arvan, et DigiDoc4 on uus programm, millega saab failid turvaliseks teha. Kas saaksite palun rääkida, kuidas see täpselt toimib?",output_ID.ee +59a983d1-4faa-4ff3-a340-7c89d658bd27,2,assistant,"Tere! Päev vastu ja jah, DigiDoc4 on just selline programm. See aitab krüpteerida dokumente, et neid ei pääseta võõraste käed. Põhiseadme on see, et need failid saavad avada ainult need, kellele selleks luba antud on. Kas teadaksite, kust alustada?",output_ID.ee +20ce9398-88a2-484a-8348-e21a13df83ac,1,user,"Tere! Ma saan aru, et DigiDoc4 rakendusega saab faile krüpteerida. Aga kuidas see tegelikult toimib? Ükskord ei ole mul sellega kunagi olnud kogemust.",output_ID.ee +20ce9398-88a2-484a-8348-e21a13df83ac,2,assistant,"Tere! Päevad! Kui teil on mõni fail, mida soetate turvalisemalt edastada, siis DigiDoc4 pakub võimalust neid krüpteerida. See tähendab, et faile saab muuta nii, et neid saab avada ainult need, kellel on selleks luba. Ükskord ei ole mul sellega kunagi olnud kogemust.",output_ID.ee +df5cdbcc-3538-4532-8209-f8716dae06b9,1,user,"Tere! Ma ei ole väga tuttav e-hääletamisega. Kas te võite mulle öelda, mis see täpselt on?",output_ID.ee +df5cdbcc-3538-4532-8209-f8716dae06b9,2,assistant,"Muidugi! E-hääletamine on olukord, kus sa saad hääleda valimistel interneti kaudu, ilma kodust lahkumata. See on väga erandlik asi, et Eesti on esimene riik maailmas, kus see üleriigiliselt valimistel kasutusele võeti! See on Interneti kaudu toimiv hääletamine, mis erineb traditsioonilisest hääletasest.",output_ID.ee +0d264b2c-8939-4f00-b894-efbee054eb8c,1,user,"Tere! Ma arvan, et e-hääletamine on tore asi, aga ma ei ole päris kindel, kuidas see töötab. Kas võite mulle öelda, kuidas seda täpselt tehakse?",output_ID.ee +0d264b2c-8939-4f00-b894-efbee054eb8c,2,assistant,"Tere! Päevake! Jah, e-hääletamine on päris uudne asi. Põhiseisus on see, et e-hääletamine tähendab, et saate hääletada interneti kaudu, ilma kodust lahkumata. Seega, kui teil on ID-kaart või mobiil-ID, on valmis! Algab valimispäeva esmaspäev hommikul kell 9 ja lõpeb laupäeval kell 20. Kas teiega jagatakse täpses põhjalikus juhendites, kuidas seda teha?",output_ID.ee +894c31a7-f13c-4874-bd27-c01da128446f,1,user,"Tere! Ma arvan, et ma tean, et digiallkirjastatud failid on ühel kujul, aga kas .bdoc ja .asice on ühesellegi sarnaselt? Kas need on täpselt sama asi?",output_ID.ee +894c31a7-f13c-4874-bd27-c01da128446f,2,assistant,"Tere! Jah, see on väga hea küsimus. Peamijärjekorda on see, et mõlemad on digitaalsed allkirjad, aga nad on tehniliselt erinevad. .bdoc oli varem kasutatud formaat, kuid .asice on praegune standard. Pidev muutmine on selleks, et sobiks rahvusvaheliselt paremini. Kas sul on mõni konkreetne küsimus, millele sa vastust vajad?",output_ID.ee +8e9afb9d-9b2f-40d2-a487-733afd00b8aa,1,user,"Tere! Ma arvan, et digiallkirjad on nagu tavaline kiri, aga digitaalselt. Kas .bdoc ja .asice faile saab mõlemad käsitleda samamoodi?",output_ID.ee +8e9afb9d-9b2f-40d2-a487-733afd00b8aa,2,assistant,"Tere! Tegelikult on see pisuvõrd küsimus. Need faile saab üldjuhul käsitleda samamoodi, aga see, kuidas need digiallkirjutega liidetakse, on oluline erinevus. .bdoc faile kasutati varem veel, nüüdseks on .asice domineeriv formaat. Kas sa tahaksid rohkem teada, kuidas need faile erinevad?",output_ID.ee +6c99cb36-e3e6-4998-a284-dc72b3c1bd2c,1,user,"Tere! Ma saan aru, et digiallkirjastamine on tähtis. Aga mis see .bdoc ja .asice vahetus täpselt tähendab? Ma näen ka mõlemat laiendit.",output_ID.ee +6c99cb36-e3e6-4998-a284-dc72b3c1bd2c,2,assistant,"Tere! Väga hea küsimus. Peen, et täheldasid neid erinevaid laiendeid. Peen, et digiallkirjad on Eestis väga üldlevinud. .bdoc ja .asice on kaks erinevat allkirjaformaati, mida kasutatakse digiallkirjastatud dokumentides. Aluperes kasutati Eestis .bdoc-formaati, aga nüüd on eelistatud .asice.",output_ID.ee +12bb9c92-cd2a-4251-b3c8-4cf3a0c44b4d,1,user,"Tere! Ma saan aru, et teiega on seotud DigiDoc konteinerid. Kas mul on mõni uus teadaolev formaat, mis on ID tarkvaras toetatud?",output_ID.ee +12bb9c92-cd2a-4251-b3c8-4cf3a0c44b4d,2,assistant,"Tere! Jah, seda on. DigiDoc konteinerid on ID tarkvaras toetatud erinevates versioonides, aga on oluline teada, et on olemas ka BDOC formaat. See loodi selleks, et asendada Eesti-spetsiifiline DDOC formaat. Kas sulle huvitaks, et teada saada, kuidas see erineb DDOC'st?",output_ID.ee +df0ed1fa-4c97-48fb-a31a-fd0c9a340731,1,user,"Tere! Ma saan aru, et teete asju DigiDociga. Kas teil on infot, milliseid failivorme DigiDoc konteinerid toetavad, eri tarkvaraversioonides?",output_ID.ee +df0ed1fa-4c97-48fb-a31a-fd0c9a340731,2,assistant,"Tere! Jah, seda küsimegi. ID-tarkvaras toetatud DigiDoc konteineri formaadid sõltuvad tarkvaraversioonist. Peamised on olnud DDOC, BDOC ja ka mõned vanemad versioonid. Kuidas ma saan sind paremini aidata? Kas sa oled huvitatud mõne konkreetse versiooni kohta?",output_ID.ee +2a887d49-ad0e-499b-9afb-51b889aaf7d6,1,user,"Tere! Ma arvan, et ma saan aru, et DigiDoc on seotud dokumentide digitaalse allkirjusega. Kuidas see aga konkreetselt PDF-idega või teiste failidega hakkab?",output_ID.ee +2a887d49-ad0e-499b-9afb-51b889aaf7d6,2,assistant,"Tere! Jah, õigesti. DigiDoc on oluline osa ID-tarkvara, mis käsitleb digitaalset allkirjastamist. See on seotud dokumentide vormingutega. Tegelikult on DigiDoc konteiner formaatidega seotud, mis pakuvad erinevaid digitaalseid allkirju. Praegune oluline vorming on BDOC, mille eesmärk on olla rahvusvahema standarditele vastav. Kas sa tahad teada rohkem BDOC kohta?",output_ID.ee +6e0df5cb-0e81-46d2-84e4-38cd1b477de3,1,user,"Tere! Mis on uus DigiDoc tarkvaras? Saaksin teada, milliseid versioone te saate toetada?",output_ID.ee +6e0df5cb-0e81-46d2-84e4-38cd1b477de3,2,assistant,"Tere! Päevääre! DigiDoc tarkvaras on mitmeid uusi versioone ja uuendusi. Viimasesse versiooni (v. 19.7.0.1837) toetame macOS 10.11 ja hiljem. See versioon on uusim ja pakub parima jõudluse ja turvalisuse. Varem toetame ka versioone 18.3 kuni 18.10. Kas sa tahaksid teada, milliseid muudatusi selles versioonis on tehtud?",output_ID.ee +d76d11f6-d4f2-407c-94a3-8a1ea71b1de5,1,user,"Tere! Palun, kas saaksid anda mulle ülevaate sellest DigiDoc4 tarkvarast? Kas tead, milliseid operatsioonisüsteeme see toetab?",output_ID.ee +d76d11f6-d4f2-407c-94a3-8a1ea71b1de5,2,assistant,"Tere! Muidugi. DigiDoc4 on ID-kaartide digitaalne allkirjastusprogramm. See on uus versioon vana DigiDoc3 programmi, mis on mõeldud ID-kaartidega töötamisele. Praegu toetab see Windowsi (uimastest versioonid), macOS (uimastest versioonid) ning Ubuntu'i (uimastest versioonid). Kas saaksid öelda, millist operatsioonisüsteemi sa kasutad, et ma saaksin sulle paremini nõu anda?",output_ID.ee +f71a186c-aefa-4932-b0c0-eaca77035e3d,1,user,"Tere! Ma ei saa aru, kuidas minu ID-kaardi lugeja korralikult töötada. Kas teil on mõningaid nõuandeid?",output_ID.ee +f71a186c-aefa-4932-b0c0-eaca77035e3d,2,assistant,"Tere! Ma saan aru. Alustame kõige põhilisega. Enne kui su ID-kaartega mingi probleemi ilmneb, tasub veenduda, et sinu arvutisse on ID-tarkvara paigaldatud. Sa saad selle alla laadida siin: https://www.id.ee/artikkel/paigalda-id-tarkvara. Kas oled seda teinud?",output_ID.ee +ca644354-6a56-45d2-ab3d-2729656d4075,1,user,"Tere! Ma saan probleeme ID-kaardi kasutamisega. Kas sulle teada on, kuidas kindlustada, et ID-kaart ja lugeja töötavad korralikult?",output_ID.ee +ca644354-6a56-45d2-ab3d-2729656d4075,2,assistant,"Tere! Ma saan aru, et sul on probleeme. Alguses on oluline, et sa veedad hetkel vääris aega ja saad lugema selle artikli: https://www.id.ee/artikkel/paigalda-id-tarkvara. Seega veedad hetkel vääris aega ja saad lugema selle artikli: https://www.id.ee/artikkel/paigalda-id-tarkvara. Siis on oluline, et sinul oleks arvutisse ID-tarkvara paigaldatud, ja vajadusel on ID-kaardi kasutamiseks seadistatud veebibrauserid.",output_ID.ee +78e4288b-4d48-40b0-add7-24ae9444cb79,1,user,"Tere! Kas te saate mulle öelda, mis on Smart-ID ja miks seda peaks kasutama?",output_ID.ee +78e4288b-4d48-40b0-add7-24ae9444cb79,2,assistant,"Tere! Muidugi. Smart-ID on tasuta nutirakendus, millega saate sisse logida erinevates e-teenustes, kinnitada tehinguid ja allkirjastada dokumende. See on nagu ID-kaart, digi-ID või mobiil-ID, aga see on mugavam kasutada telefoni kaudu. Kas sa tahaksid rohkem teada, kuidas see töötab?",output_ID.ee +cdbd47aa-959f-412a-997e-67546d6c55ee,1,user,"Tere! Ma arvan, et Smart-ID tundub päris hea asi. Kuidas sellega saab e-teenuste juurde sisse logida?",output_ID.ee +cdbd47aa-959f-412a-997e-67546d6c55ee,2,assistant,"Tere! Päris hea mõte, et huvitatud olete! Smart-ID on nutirakendus, millega saate e-teenustesse sisse logida ja tehinguid kinnitada. See on nagu ID-kaart või mobiil-ID, aga seda saab kasutada ka telefoni kaudu. Peaasi, et selle jaoks on konto olemas.",output_ID.ee +aaf9eefc-5901-4179-86f3-9a736eb08b39,1,user,"Tere! Ma mõtlen, et Smart-ID peaks olema hea, aga kuidas seda kasutada? Kas see on sama, mis ID-kaart?",output_ID.ee +aaf9eefc-5901-4179-86f3-9a736eb08b39,2,assistant,"Tere! Päevas! Jah, Smart-ID pakub palju samu võimalusi nagu ID-kaart ja mobiil-ID. Sa saad sellega e-teenustesse sisse logida, pangaülekandeid kinnitada ja isegi dokumente allkirjastada. See on kindlasti mugav alternatiiv. Kas sulle on teada, millise e-teenusega sa seda kasutada soovid?",output_ID.ee +2845d57b-57e3-492f-9a75-9e3ff883d02c,1,user,Tere! Ma nägin internetis kohta MyID portaali. Mis see on?,output_ID.ee +2845d57b-57e3-492f-9a75-9e3ff883d02c,2,assistant,"Tere! MyID portaal on SK ID Solutions AS (SK) poolt pakutav veebiportaal. See pakub teavet teie Mobiil-ID, ID-kaardi, Digi-ID ja Smart-ID kontode kohta, mis on SK poolt väljaõitud. See on nagu keskoht, kus saate näha, mida te sellede ID-dega teinud olete.",output_ID.ee +4b7099f9-8ffb-4539-a480-54b4f39d041d,1,user,Tere! Ma nägin seda MyID portaali. Mis see üldse on? Kas see on midagi uut?,output_ID.ee +4b7099f9-8ffb-4539-a480-54b4f39d041d,2,assistant,"Tere! MyID portaal on uus platvorm, mida pakub SK ID Solutions AS (SK), mobiil-ID, ID-kaardi, Digi-ID ja Smart-ID kontode kohta. See on koht, kus saad teavet nende kontode kohta ja sellest, mida neis tehtud on. Kas sa tahad teada rohkem, kuidas seda kasutada või mis teeb selle eriliseks? ",output_ID.ee +77b9e64e-4da5-4060-bbe5-743cc342d9cf,1,user,"Tere! Ma ei ole päevalt aru saanud, mis MyID portaal on ja mida ta pakub. Kas saate mulle sellest pisut rääkida?",output_ID.ee +77b9e64e-4da5-4060-bbe5-743cc342d9cf,2,assistant,"Muidugi! MyID portaal on ID.ee poolt pakutav keskoht, kus saab infot teatud digitaalsete identifikaatorite kohta, nagu Mobiil-ID, ID-kaart, Digi-ID ja Smart-ID. Need on kõik sertifikaate ja tehinguid pidav digitaalne identifikaatorid, mis on väljaõnnestatud SK ID Solutions AS (SK) poolt. Kas teiega on oluline teada, mida need identifikaatorid täpselt teevad?",output_ID.ee +45ca318f-e859-4fb0-a5e7-98a8912e3cef,1,user,"Tere! Ma ei taju nii palju, mida need sertifikaadid tähtsuses on. Kas need on nagu minu ID-kaardi versioon internetis?",output_ID.ee +45ca318f-e859-4fb0-a5e7-98a8912e3cef,2,assistant,"Tere! Jah, seda võid nimetada nii. Sertifikaadid on nagu ID-kaardi digitaalne versioon. Nad aitavad sinu identiteeti veebis tõestada. Kui sul on ID-kaart, digi-ID, mobiil-ID või Smart-ID, siis sul on ka nendega seotud sertifikaadid. Need on olulised, et sa võiksid turvaliselt e-teenuseid kasutada.",output_ID.ee +3dc98b10-4b98-470d-a2f0-1386ffbfa8bf,1,user,"Tere! Ma saan aru, et ID-kaardiga seotud sertifikaadid on olulised. Aga kuidas see töötab tegelikult? Miks on need nii olulised? Kas ma saan sellega midagi teha, ilma et ma PIN-koodi tean?",output_ID.ee +3dc98b10-4b98-470d-a2f0-1386ffbfa8bf,2,assistant,"Tere! Päevas! Nii jah, sertifikaadid on nagu sinu digitaalne võti e-teenustesse. Need seovad sind ID-kaardiga (või teiega liidetud muude digitaalsete dokumentidega) ja lubavad sinule e-teenustes sisse logida ja allkirju anda. Kuigi alati vajavad need PIN-koodi (ja PUK-koodi), on see nii selleks, et vältida ebaseaduslikku kasutamist. Mida sa pead silmas, et ilma PIN-koodita ei saa e-teenustega midagi teha.",output_ID.ee +d481d0e7-8d67-4871-b199-728500821f55,1,user,"Tere! Ma ei ole päris kindel, mis need sertifikaadid on ja miks on need nii olulised. Kas saaksid seda veidi selgitada?",output_ID.ee +d481d0e7-8d67-4871-b199-728500821f55,2,assistant,"Muidugi! Ühest küljest on sertifikaadid nagu digitaalne isikutunnusjoon, mis seob sind sinu digitaalse isikutuvastusvahendiga, näiteks ID-kaardiga või mobiil-ID-ga. See tähendab, et kui sa kasutad e-teenust, kasutatakse seda sertifikaati, et kinnitada, et sa oled see, keda sa väidate olevat. On nagu digitaalne kiri, millega sa kinnitad oma identiteedi. Kuidas sa need tavaliselt kasutad?",output_ID.ee +6491ba23-3e9c-4078-b71f-e9648db315c0,1,user,"Tere! Ma saan probleeme ID-kaardi kasutamisega. Kas teil oleks võimalik öelda, kuidas veenduda, et mu kaardilugeja töökorras on?",output_ID.ee +6491ba23-3e9c-4078-b71f-e9648db315c0,2,assistant,"Tere! Päevas! On hea, et sa pöördud abi pidi. Jah, kindlasti! Esimese sammuna on see, et vajad jääd kontrollima, et ID-kaardi ja kaardilugeja on täielikult töökorras. Enne kaardi kasutamist peaks arvutis olema ID-tarkvara paigaldatud (https://www.id.ee/artikkel/paigalda-id-tarkvara). Kas sa oled kindel, et ID-tarkvara on paigaldatud ja ID-kaart on kaardilugejas?",output_ID.ee +419ee2de-71ca-4eeb-9635-acf7b81b892a,1,user,"Tere! Ma ei saa aru, miks mul ID-kaardiga probleemid on, kui ma internetis teenuste eest sisenen. Mis ma peaks uurima?",output_ID.ee +419ee2de-71ca-4eeb-9635-acf7b81b892a,2,assistant,"Tere! Olgu. See on üsna tavaline probleem. Esimese sammuks on kindlasti kontrollida, et ID-kaardi ja kaardilugeja on korralikult ühendatud ja töökorras. Kas olete kaardilugeja arvutiga USB-kaabelga ühendanud, ja kui olete, on kaabel korralikult arvutiga luku?",output_ID.ee +6fa032bf-4c6c-4a47-a07c-62d37b4c0f1d,1,user,"Tere! Ma saan aru, et ma saan ID-kaardi abil eesti e-teenuseid kasutada. Aga kust ma saan alustada? Mis on kõige olulisem?",output_ID.ee +6fa032bf-4c6c-4a47-a07c-62d37b4c0f1d,2,assistant,"Tere! Hea küsimus! Alustamine ID-kaardi abil eesti e-teenuste kasutamisega on suhteliselt lihtne. Kõige olulisem asi on sellega, et pead paigaldama ID-kaartide tarkvara oma arvutile. Seda saab alla laadida ja paigaldata ID-kaardi ametlikult veebilehe kaudu: [https://www.id.ee/artikkel/paigalda-id-tarkvara](https://www.id.ee/artikkel/paigalda-id-tarkvara). Kas sa kasutad Windowsi, macOSi, Linuxi või Midigate? See aitab mul soovitada kõige sobivama versiooni.",output_ID.ee +d4bc3ee2-3da3-4514-b861-f9c5643aeb98,1,user,"Tere! Ma ei ole päris kindel, kuidas oma ID-kaardi sertifikaate vaadata ja neid arvutis salveida. Kas saate mind selles osas aidata?",output_ID.ee +d4bc3ee2-3da3-4514-b861-f9c5643aeb98,2,assistant,"Muidugi! Üsna lihtne on see. Esmalt võid kaardilugeja arvutiga ühendada ja oma ID-kaardi sisse süstada. Seejäna avad DigiDoc4 programmi. Pärast seda ootad, kuni sinu ID-kaardi andmed sisse loetakse. Kas sul on kaardilugeja ja arvuti, mis on valmis kasutama?",output_ID.ee +98b0ecf0-bcf7-4e8e-9b80-e0a23e0830fe,1,user,"Tere! Ma mõtlesin alustada mobiil-ID kasutusega. Kas see on midagi, mis on uus või midagi, millele ma võin minna?",output_ID.ee +98b0ecf0-bcf7-4e8e-9b80-e0a23e0830fe,2,assistant,"Tere! Päris tore, et huvitatud olete! Mobiil-ID on kindlasti modernsem ja mugavam lahendus kui vana ID-kaart. See on riiklikult tunnustatud digitaalne isikutunnistus, millega saate juurida erinevaid e-teenuseid. Kas te kujulite, et teate, mis see on?",output_ID.ee +fcc496c2-3069-416a-8cb3-c2d0c31ae721,1,user,"Tere! Kas tean, et mobiil-ID on olemas, aga pole päris kindel, mida see tähendab ja kas see on minu jaoks hea?",output_ID.ee +fcc496c2-3069-416a-8cb3-c2d0c31ae721,2,assistant,"Tere! On tore, et huvi on! Mobiil-ID on tegelikult väga hea lahendus, kui vajate digitaalset isikutunnistust. See on tänapäeval mugavam ja turvameelt hoogu andev kui traditsiooniline ID-kaart. Sellega saate viibida eesti.ee-s, internetipankas, või osaleda e-valimistel. Kas teil on küsimusi selle kohta?",output_ID.ee +4bffcec9-2ef2-45a3-8a50-f09d4d4bd932,1,user,"Tere! Ma mõtlesin alustada mobiil-ID kasutamist, aga ei tea päris midagi selle kohta. Kas seda võtab vana ID-kaardist palju rohkem aega?",output_ID.ee +4bffcec9-2ef2-45a3-8a50-f09d4d4bd932,2,assistant,"Tere! Väga tore, et huvi on! Ei võta reaalselt nii palju aega, kui tead, kuidas see töötab. Mobiil-ID kasutamine on palju mugavam, sest ei pea kaarti kaasas kanda. Kui vana ID-kaardi puhul on tähtaeg kaarti kanda, siis mobiil-ID on alati kaasas telefoni sees. Kas sa tahaksid teada, milliseid seadmeid sellega kasutada?",output_ID.ee +998f9d73-c33e-4421-9a2e-a31df908fe9c,1,user,Tere! Ma pole kunagi varem ID tarkvara uuendanud. Miks on see nii oluline?,output_ID.ee +998f9d73-c33e-4421-9a2e-a31df908fe9c,2,assistant,"Tere! Palju maasõna! ID tarkvara uuendamine on väga oluline turva tõttu ja sellepärast, et see hakkaks paremini töötama. Ühest küljest tagab uuendamine, et sa oled kaitstud uusimaid turvariske vastu. Teisest küljest aitab uuendamine, et tarkvara oleks otstarbeks mugavam kasutada.",output_ID.ee +c4e9bc76-8ff5-41f9-a02c-2befc606d30e,1,user,"Tere! Ma ei ole päris kindel, miks minu ID tarkvara on uuendamine soovitatud. Mis see tähtsusetu on?",output_ID.ee +c4e9bc76-8ff5-41f9-a02c-2befc606d30e,2,assistant,"Tere! Kui sul on ID tarkvara uuendamine soovitatud, see on hea märk – see tähendab, et sinu seade on turvalisemaks ja tööefektiivsemaks ümber tehtud. Peamiselt on uuendused seotud turvariskide vähendamisega või selleks, et tarkvara töötaks paremini teiste programmidega. Kas sulle on teada, mis ID tarkvara on?",output_ID.ee +ea8fd93e-aa32-490e-94ec-dd8535bae715,1,user,"Tere! Ma arvan, et ID tarkvara on jäänud natuke vanaks. Miks on nii oluline, et seda uuendada?",output_ID.ee +ea8fd93e-aa32-490e-94ec-dd8535bae715,2,assistant,"Tere! On õige jutt. ID tarkvara peab perioodiliselt uuendama, sest see on turvalisuse ja selle tööefektiivsuse tagamine. Peaasi, et tarkvara oleks uusim versioon, et vältida kriitilisi turvariskeid. Kui tarkvara ei ole uusim, ei saa sellega kindlalt digitaalselt allkirjastada või kontrollida juba antud allkirjade kehtivust.",output_ID.ee +80c48ac2-ae03-43ac-b2bb-57de5f12b3a3,1,user,"Tere! Ma saan aru, et id.ee veebileht kogub külastajate kohta andmeid. Kas on võimalik teada, millist teavet täpselt kogutakse, kui ma veebilehele saja.",output_ID.ee +80c48ac2-ae03-43ac-b2bb-57de5f12b3a3,2,assistant,"Tere! Jah, seda on võimalik. Kui külastate id.ee veebilehte, kogume teie arvuti või arvutivõrgu internetiaadressi (IP-aadressi), lehekülje, mida külastasite, teie arvuti veebilehitseja ja operatsioonisüsteemi ning külastamise aja. Me ei seota IP-aadressi isikut tuvastava teabega, vaid kogume selle kohta, millist veebilehe osa külastate ja kui kaua seal viibite. See on täiestelt külastusstatistika jaoks.",output_ID.ee +9aecdadd-1d17-459e-a0b3-9eb97bc88e30,1,user,"Tere! Palun, kas teeksite veebilehel id.ee mingisugustid muutusi privaatsustingimustes?",output_ID.ee +9aecdadd-1d17-459e-a0b3-9eb97bc88e30,2,assistant,"Tere! Jah, id.ee privaatsustingimused on uuendatud. Peamiselt on täiendatud teavet selle kohta, kuidas veebilehele külastamisel kogutakse andmeid, ning kuidas kasutatakse küpsiseid. Kas sooviks teada rohkem?",output_ID.ee +3c5d2c8f-35a7-4ada-9930-7ba4f3253f18,1,user,"Tere! Ma ei saa ID-kaardiga veebilehtedel eesti.ee keskkonnas toimida, ja mul on raskusi. Mis ma peaks tegema?",output_ID.ee +3c5d2c8f-35a7-4ada-9930-7ba4f3253f18,2,assistant,"Tere! Üsna sagedasti tekib just see probleem. Palju õnne, et teadsid, et sinuga on asi. Peame siis veendutama, et veebilehitseja on ID-kaardi sertifikaatidega õigesti seadistatud. Alustame kõige lihtsamast – kontrolli, et veebilehitsejas on Web eID laiendus lubatud. Kas sa kasutad Chrome, Edge või Midigate?",output_ID.ee +353151a0-72d4-4bf2-a7f1-8dd02716d1ea,1,user,Tere! Ma ei saa ID-kaardiga e-teenustes midagi teha. Mis ma peaks tegema?,output_ID.ee +353151a0-72d4-4bf2-a7f1-8dd02716d1ea,2,assistant,"Tere! Üldiselt on see probleem paljudel juhtudel seotud sellega, et veebilehitseja ei ole õigesti seadistatud ID-kaardi sertifikaatide kasutamiseks. Palju oluline on see, et veebilehitsejas on Web eID laiendus lubatud. Kas olete proovinud Chrome'i seadeid vaadata? Kui te seda ei ole, siis veenduge, et olete samm 2 läbi viinud - vajutage Chrome paremas servas ikoonile ja vali ‘Lisad’. Kui seda ei ole, siis võtke kindlasti puudu, et lisada `ncibgoaomkmdpilpocfeponihegamlic` lisada Chrome'ile.",output_ID.ee +a214d7ed-1b33-423f-a9e2-aca37373f74d,1,user,Tere! Ma saan probleeme ID-kaardi kasutamisega veebis. Mida ma peaksin tegema?,output_ID.ee +a214d7ed-1b33-423f-a9e2-aca37373f74d,2,assistant,"Tere! Ma saan aru, et sul on raskusi ID-kaardi kasutamisega veebis. Palju õnne, see on levinud probleem. Lühidalt, kõigepealt kontrollige, et veebilehitsejas on lubatud ID-kaardi sertifikaatide kasutamine. Kas sul on mõeldud Chrome, Edge, Firefox või mõni teine brauser?",output_ID.ee +b26c8c30-8da8-4044-aae8-5fa60466f194,1,user,Tere! Mul on probleeme ID-kaardi kasutamisega. Mida ma võin ette võtta?,output_ID.ee +b26c8c30-8da8-4044-aae8-5fa60466f194,2,assistant,"Tere! Ma saan sind aidata. Esmapilgub võta mõned lihtsad küsimused, et paremini aru saada, mis toimub. Kas sa kasutad ID-kaarti esimest korda, või on probleem tekkinud varem? Ja millist ID-kaarti sa kasutad (nt. mille tootja ja mudel)? ",output_ID.ee +423d4ba9-479a-44ad-a50e-23ad5c8cc780,1,user,"Tere! Ma nägin ID.ee saidikus kohta Digidoc failivorminguid. Kas saaksite öelda, mis need BDOC ja CDOC kujulised failid täna Eestis üldse tähendavad?",output_ID.ee +423d4ba9-479a-44ad-a50e-23ad5c8cc780,2,assistant,"Tere! Muidugi. Põhjaosas Eestis kasutatakse praeguses enamus digitaalallkirjade puhul BDOC-faili, mis on varem tuntud kui Eesti-spetsiifiline LT-TM formaat. CDOC on seevastu laiendus, mille abil eristatakse krüpteeritud DigiDoc vormingus olevaid faile. Pea silmass sellele, et uus versioon kasutab automaatselt .asice faili asemel.",output_ID.ee +b7e2f63f-1b22-4bcd-ae2e-7f816b1fe0a1,1,user,"Tere! Ma arvan, et Digidoc-failid on minu tööle tegelikult väga olulised. Aga ma ei ole päris kindel kõigest, mis seal sees on. Kas saate mulle põgusalt öelda, milliseid eri formaate seal üldse on?",output_ID.ee +b7e2f63f-1b22-4bcd-ae2e-7f816b1fe0a1,2,assistant,"Tere! Päev! Muidugi, ma saan. Digidoc-failides on mitut erinevat formaati. Üsna paljudes ID-tarkvara versioonides toetatakse BDOC, CDOC ja ASICE formaate. Lisaks on olemas veel ENCDOC-XML, mis on krüpteeritud DigiDoc formaat. Kas te raatsite täpsemalt mõelda, millise formaadi kohta te soovite rohkem teada?",output_ID.ee +89d81e43-4aaa-492e-85db-71cca16e1b66,1,user,Tere! Kas te raames arvatakse kiipkaardi lugejad? Olen päritlustega probleeme.,output_ID.ee +89d81e43-4aaa-492e-85db-71cca16e1b66,2,assistant,"Tere! Jah, ID.ee kiipkaardilugejad on toetatud. Aga probleemid võivad olla mitmetel põhjustel. Mis konkreetset probleemi teil tekkis? Kas näiteks kaardilugeja ei tööta üldse, või on probleem andmete lugemisel?",output_ID.ee +90785fb5-c55b-40e5-831e-b06badadf8a2,1,user,Tere! Ma tahan ID-tarkvara Ubuntu peale paigaldada. Kuidas ma kõige paremini alustan?,output_ID.ee +90785fb5-c55b-40e5-831e-b06badadf8a2,2,assistant,"Tere! Väga hea küsimus. Ubuntu jaoks on kõige mugavam paigaldada ID-tarkvara automaatskripti abil. Alustame sellega: esmalt pead salvestama skripti oma arvuti tahvlkale. Siis avad Terminali ja navigeerid kataloogi, kuhu sa skripti salvestasid. Näiteks, kui sa seda 'Downloads' kataloogi, siis sisestad käskluse `cd /home/kasutajanimi/Downloads` (kasutajanimi asendades sinu kasutajanimega). Skripti käivitamiseks siis `sh install-open-eid.sh` ning parool sisestamisel lihtsalt vajuta edasi.",output_ID.ee +8c2773f5-ca24-4b25-93bb-21550529565f,1,user,"Tere! Ma saan aru, et Ubuntu tarkvara paigaldusega seoses ID-kaardi kasutamine on üsna oluline. Kuidas ma kõige paremini ID-tarkvara Ubuntu-sse saan paigaldada?",output_ID.ee +8c2773f5-ca24-4b25-93bb-21550529565f,2,assistant,"Tere! Jah, see on oluline. Parim viis on automaatskripti abil. Sa saad selle siin: https://installer.id.ee/media/install-scripts/install-open-eid.sh. Eesmärk on avada Terminal ja navigeerida faili kataloogi, kuhu see on salvestatud. Näiteks, kui sa salvestasid selle ‘Downloads’ kataloogi, siis käsk on ‘cd /home/kasutajanimi/Downloads’. Pärast seda, skripti käivitamine 'sh install-open-eid.sh' käskuga. Kas sa salvestusid selle faili kuhagi või ei ole sa sellele veel jõudnud?",output_ID.ee +24e31713-b01a-4462-8a71-ecac016ce2d5,1,user,"Tere! Ma nägin, et on digitembeldamine. Mis see üldse tähendab ja miks seda vaja on?",output_ID.ee +24e31713-b01a-4462-8a71-ecac016ce2d5,2,assistant,"Tere! Digitembeldamine tähendab e-templi abil digitaalallkirja andmist. See on nagu kinnitus, et dokument on pärit selle saatnud asutusest ning seda ei ole muudetud. See aitab kindlustada dokumendi autentseid. Kui sa seda enam ei tea, sa saad abi SI ID lahendustes (SK ID Solutions) tehingu teinud teenust.",output_ID.ee +3c5e9362-dfff-4fb7-9fcf-17bf8cba1b29,1,user,"Tere! Ma arvan, et ma saan aru, et digitembeldamine on midagi seotud digiallkirjadega. Kas sa võiksite selgitada, mis see täpne on ja miks seda on vaja? Kui palju tean? ",output_ID.ee +3c5e9362-dfff-4fb7-9fcf-17bf8cba1b29,2,assistant,"Tere! Jah, seda saab öelda nii. Digitembeldamine on seotud digiallkirjade andmisega juriidilistele isikutele. See tähendab, et SK ID Solutions AS pakub e-Templi teenust, mis annab kinnituse sellele, et elektrooniliselt saadetud dokument või muu info pärineb tõepoolest selle saatnud asutusest. See on nagu digitaalne kiri, millele lisatakse kinnitus, et see on pärit sellest, kes selle on saatnud. Kas sa tead, et e-Tempel põhineb kvalifitseeritud e-Templi sertifikaatidel?",output_ID.ee +f1014b38-a1a6-48fd-be50-f11341eb4cc4,1,user,"Tere! Ma saan DigiDoc4 käivitades veateadet, et sertifikaatide usaldusnimekirja uuendamine ebaõnnestub. Kas teate, mis selle vastu pean tegema?",output_ID.ee +f1014b38-a1a6-48fd-be50-f11341eb4cc4,2,assistant,"Tere! Jah, seda probleemi on mõeldud just sellele vea korral. See tähendab, et DigiDoc4 ei saa oma sertifikaate teiega usaldusväärselt kontrollida. Alustame kõigepealt sellest, et veenduksime, et sul on ID-tarkvara kõige uuem versioon. Võite seda vaadata ja vajadusel paigaldada siin: https://www.id.ee/artikkel/paigalda-id-tarkvara/",output_ID.ee +a80f38b2-c750-41e2-8ce4-f236c8515e66,1,user,"Tere! Ma sain DigiDoc4 käivitamisel vea, et sertifikaatide usaldusnimekirja uuendamine ebaõnnestus. Mis see tähendab ja mida ma sellega pean tegema?",output_ID.ee +a80f38b2-c750-41e2-8ce4-f236c8515e66,2,assistant,"Tere! See tähendab, et arvutis, millega DigiDoc4 töötab, ei ole uusima usaldusnimekirja versiooni. See on vajalik allkirjade kontrollimiseks. Selle probleemiga toime tulla, kõigepealt veenduge, et teil on arvutis kõige uuem ID-tarkvara paigaldatud. Saate seda hankida siin: https://www.id.ee/artikkel/paigalda-id-tarkvara/ . Kas te seda juba proovite?",output_ID.ee +5e4ec88b-3162-459b-b422-c541a698835e,1,user,"Tere! Ma pole päris kindel, mida täpselt tähendab ‘digitaalne allkiri’. Kas see on sama asjalik kui tavaline kiri allkiri?",output_ID.ee +5e4ec88b-3162-459b-b422-c541a698835e,2,assistant,"Tere! Hea küsimus! Jah, mõttes on see väga sarnane, aga digitaalse allkirja puhul saab sellega kindluse, et see ei ole lihtsalt võltsitav. Tavaline kiri on mõelnud kui kirjalik kinnitus, kuid digitaalse allkirjaga saab kindlustada, et see on seaduslikult kehtiv ja seda ei saa muuta pärast allkirjastamist. See vastab omakäelisele allkirjale juriidiliselt.",output_ID.ee +419f58c1-63d7-47b5-968a-1163a9f26b55,1,user,"Tere! Ma arvan, et digitaalse allkirjastamine on vaimujaur, aga kas teeksite sellest mõru, et nad on nüüd igasugused allkirjad digiallkirjad? Kas see tähendab, et iga dokumendi allkiri on samamoodi juriidiliselt võrdne omakäelise allkirjaga?",output_ID.ee +419f58c1-63d7-47b5-968a-1163a9f26b55,2,assistant,"Tere! Päris hea küsimus! See on päris oluline erinevus. Tegelikult tähendab see, et ‘digiallkiri’ (ehk digiallkirjastamine jm) Eestis viitab ainult sellele signeerimisviisile, mis on seaduslikult kehtiv ja juriidiliselt võrdne omakäelise allkirjaga. See tähendab, et allkirja andmise ajal on kindlalt kontrollitud kasutaja identiteet ja sertifikaadi väljaandja taust. See tagab, et keegi teine ei saaks allkirjastatud dokumenti muutma. Elektrooniline allkiri ongi laiem mõiste, mis hõlmab ka digiallkirju, aga need ei pruugi olla samuti nii kindlalt juriidiliselt väärindatud.",output_ID.ee +2c87353f-da82-4b8a-b01e-8327a68e554d,1,user,"Tere! Ma arvan, et digitaalsed allkirjad on kirsilehe otsas. Kas saaksid sa mulle selgitada, mis see täpselt tähendab ja kuidas nad erinevad tavalisest allkirjastamisest?",output_ID.ee +2c87353f-da82-4b8a-b01e-8327a68e554d,2,assistant,"Tere! Väga hea küsimus. Digitaalse allkirjastamise tähendus on see, et sa saad dokumenti paberivaba allkirjastada. Selleks ei ole vaja dokumendi välja trükkida. Selleks kasutatakse digitaalset isikutuvastusvahendit, nagu ID-kaart. Aga oluline on see, et kõik digitaalsed allkirjad ei ole seaduslikult kehtivad nagu omakäelised allkirjad. Mis täpselt teeb digiallkiri seaduslikult kehtivaks? See on, et kasutaja isik ja sertifikaadi andja taust on kontrollitud, ning allkirja andmise aega on kindlalt fikseeritud.",output_ID.ee +1349a528-6efa-4013-8add-37e8b204bc5b,1,user,"Tere! Ma sain uue ID-kaardi, aga ei oska PIN- ja PUK-koodi kasutada. Mis see kõigega kokku tegemist on?",output_ID.ee +1349a528-6efa-4013-8add-37e8b204bc5b,2,assistant,"Tere! Tore, et sa uue ID-kaardi said! PIN- ja PUK-koodid on need, mis sinu ID-kaardiga elektrooniliselt ID-d tõestada panevad. Nii, et ilma neist ei saa ID-kaarti kasutada. Üks asi, mis väga oluline on, on nende turvalisus. Peame need koodid omaette kaardist eemal hoida, et keegi neid ära ei võiks. Kas sa tead, kus need koodid on?",output_ID.ee +1f2d031f-7ed0-49bf-af33-9401704d4a1b,1,user,Tere! Ma sain uue ID-kaardi ja koodiümbriku. Kas ma pean kohe PIN- ja PUK-koodid vahetama?,output_ID.ee +1f2d031f-7ed0-49bf-af33-9401704d4a1b,2,assistant,"Tere! Jah, väga hea, et sa sai uue ID-kaardi ja koodiümbriku! Soovitav on PIN- ja PUK-koodid kohe vahetada. See tagab parema turvalisuse. Koodiümbrikus on uusad koodid, ja on hea, kui sa neid kohe uusima kaardi külge kinnitatakse.",output_ID.ee +d34e29a5-7c8d-4346-90c6-f6eca574c661,1,user,Tere! Mul on uus ID-kaart ja koodiümbrik. Kas ma pean kohe PIN- ja PUK-koodid vahetama?,output_ID.ee +d34e29a5-7c8d-4346-90c6-f6eca574c661,2,assistant,"Tere! Jah, on väga oluline PIN- ja PUK-koodid kohe uuega vahetada. Kui sa neid uuega vahetamata jätad, ei saa ID-kaarti elektrooniliselt kasutada. Ja kui keegi saadab need koodid võõrasse kätesse, on ka sinu elektrooniline identiteet ohus. Kas sa oskad öelda, miks on see nii?",output_ID.ee +935afa73-fd4b-4bf7-98ec-79973c362097,1,user,"Tere! Ma saan aru, et peaks ID-kaardi tarkvara macOS arvutis paigaldama. Kutsutakse seda ID-tarkvara? Ja kuidas ma saan selle alla laadida?",output_ID.ee +935afa73-fd4b-4bf7-98ec-79973c362097,2,assistant,"Tere! Jah, see on täpsasti ID-tarkvara. See koosneb mitmest komponendist. Sa saad selle alla laadida DigiDoc4 rakendusest, mille lingi leiad ID.ee veebilehelt: [https://www.id.ee/artikkel/digidoc4-raakendus]. See on ühedest kolmest komponendist, mida pead macOS arvutile paigaldama.",output_ID.ee +3adeb096-90c9-439c-a26c-e1de85c80695,1,user,"Tere! Mul on küsimus. Kui saan teada, et minu ID-kaart on kadunud, mida peaks tegema?",output_ID.ee +3adeb096-90c9-439c-a26c-e1de85c80695,2,assistant,"Tere! See on murettekitav. Palun mõelge kõigepealt läbi, kas olete seda äsja kasutanud ja kas olete seda tavapärasest veidi erinevasse kohta jätnud, näiteks kontorilauale. Kui te kindel olete, et see on varguses, siis paneme kohe tegutama.",output_ID.ee +f017c668-3ae3-4f85-b73a-b198d34ae0ec,1,user,"Tere! Ma arvasin, et minu ID-kaart on kadunud. Mida ma peaks tegema?",output_ID.ee +f017c668-3ae3-4f85-b73a-b198d34ae0ec,2,assistant,"Tere! See on murettekitav. Enne, siis mõtle kokku, kas olete seda äsja kasutanud ja kas seda võisite just hiljemalt, näiteks kontorilauale jätta. Kui te olete kindel, et kaardi varguses, siis on oluline teguda kohe. Mida edasi sõltub sellest, kui kindel olete kaardi varguses.",output_ID.ee +2ca38de6-e1a8-4184-bbd3-af87dfa96b6b,1,user,"Tere! Ma arvan, et saan nüüd oma ID-kaardi kasutada internetis, aga ma ei ole kindel, kuidas kontrollida, et minu sertifikaadid on kehtivad. Kas on midagi oluline, mida pean teadmas?",output_ID.ee +2ca38de6-e1a8-4184-bbd3-af87dfa96b6b,2,assistant,"Tere! Päevas! Jah, oma ID-kaardi sertifikaadid on olulised, et seda kasutada. Sa saad sertifikaatide kehtivuse kontrollida DigiDoc4 rakenduses. See on lihtne: avad DigiDoc4, vali menüüst „Minu eID”, ja siis kontrollid sertifikaadi staatust. Palju lihtsam, kui arvatakse!",output_ID.ee +71b82447-22c8-4517-a67a-5ff711540562,1,user,Tere! Kas mul on DigiDoc4 rakenduses mingisugused piirangud digiallkirjade kohta? Ja kuidas see ajatempel asja ajab?,output_ID.ee +71b82447-22c8-4517-a67a-5ff711540562,2,assistant,"Tere! Jah, piirangud on olemas. Piirang puudutab isiklikuks tarbeks digiallkirjastamist, mis toimuvad läbi DigiDoc4 rakenduse ja RIA DigiDoc mobiilirakenduse. Kuus on ette nähtud tasuta digiallkirju, aga see arv on muutunud ja seda piirangut ravivad ID-kaardi, Digi-ID või Mobiil-ID abil tehtavad allkirjad. Peamaseks on see, et piirang puudutab ainult neid digiallkirju, mis tehtakse just selle rakenduse kaudu, mitte näiteks e-teenustes, nagu internetipankades.",output_ID.ee +24d473db-8fca-4414-bd57-edb2211ac70b,1,user,"Tere! Ma nägin, et DigiDoc4 rakendus on muutunud. Kas ma saan endiselt dokumente digiallkirjastada sellega?",output_ID.ee +24d473db-8fca-4414-bd57-edb2211ac70b,2,assistant,"Tere! Jah, seda saate. Küll aga on piirangud sellel, kui palju digiallkirju te saate kuus teha. Alates DigiDoc tarkvara loomisest on teatud arv tasuta allkirju kuus. See on muutunud paari aasta jooksul suuremaks, kuid see jääb endiselt piiratud.",output_ID.ee +ee053ac9-a25c-4791-887a-c60e4ebedb3d,1,user,"Tere! Ma arvan, et DigiDoc4 on uus tarkvara. Kas saaksite mulle öelda, mida see teeb ja kuidas seda kasutada?",output_ID.ee +ee053ac9-a25c-4791-887a-c60e4ebedb3d,2,assistant,"Tere! Jah, DigiDoc4 on ID-kaardi ja Smart-ID omanikele ettenähtud tarkvara digiallkirjade andmisel. Peamisi selleks on kasutada digiallkirju dokumentidele. Selle abil saate luua usaldusväärseid digiallkirju. Peamised erinevad on ajatempel ja kehtivuskinnitusteenus, mis kinnitavad digiallkirja autentseks ja ajakohaks.",output_ID.ee +f8a374be-6d55-47b1-ba4f-0c52e739dee1,1,user,"Tere! Ma arvan, et ID-kaart on väärt uurida. Kas sa võiksite mulle öelda, mis see üldse on ja milleks see on?",output_ID.ee +f8a374be-6d55-47b1-ba4f-0c52e739dee1,2,assistant,"Muidugi! ID-kaart on Eesti kodaniku ja Eestis püsivalt elava Euroopa Liidu kodaniku jaoks kohustuslik isikut tõendav dokument. See on digitaalne dokument, mis on ka võbilauaõigus, st seda saab kasutada reisidel passi asemel. Kas sa tahad rohkem teada selle kasutusvõimaluste kohta?",output_ID.ee +6f68d059-5586-4c35-8bc1-19b109f224c1,1,user,Tere! Kas ma saan ID-kaardi kasutamiseks oma arvutis ID-tarkvara paigaldata?,output_ID.ee +6f68d059-5586-4c35-8bc1-19b109f224c1,2,assistant,"Tere! Jah, seda saab. ID-tarkvara on võimalik paigaldada Windowsi, macOS ja Ubuntu operatsioonisüsteemidel. Kuid on oluline teada, et see ei toeta vanemaid operatsioonisüsteeme. Mis operatsioonisüsteemi sa kasutad?",output_ID.ee +15e075d7-ad68-4f89-aa77-8775d414441d,1,user,"Tere! Ma arvan, et ma pean ID-kaardi kasutama oma arvutis. Mis ma peaksin tegema?",output_ID.ee +15e075d7-ad68-4f89-aa77-8775d414441d,2,assistant,"Tere! Hea küsimus. Kui soovite ID-kaardi oma arvutis kasutada, siis kõigepealt pead ID-tarkvara paigaldama. See on väga oluline, et ID-kaart arvutis töötaks. Kas teil on praegu mingi operatsioonisüsteem arvutis? Näiteks Windows, macOS või Linux?",output_ID.ee +579e322f-f758-4121-8690-7fd6972e2149,1,user,"Tere! Ma arvan, et mul on ID-kaart ja ma tahan sellega arvutis midagi tegema. Mis ma pean tegema, et see tööle hakkaks?",output_ID.ee +579e322f-f758-4121-8690-7fd6972e2149,2,assistant,"Tere! Üsna hea küsimus! Et saaksid ID-kaardi arvutis kasutama, pead paigaldama ID-tarkvara. See tarkvara on sinu arvuti jaoks vajalik, et ID-kaardiga elektroonilisi toiminguid teha. Kas sa kasutad Windowsi, macOS või Linuxi operatsioonisüsteemi?",output_ID.ee +432967ac-e8c0-413e-aa5c-260ad2f24162,1,user,"Tere! Ma arvan, et ma saan nüüd Mobiil-ID'd, aga pole päris kindel. Mis see üldse on ja kuidas seda kasutada?",output_ID.ee +432967ac-e8c0-413e-aa5c-260ad2f24162,2,assistant,"Tere! Mobiil-ID on digitaalne identifikaator, mis võimaldab tegeleda mitmete võrguteenustega elektrooniliselt, näiteks sularahata maksed või e-krediit. See on nagu digitaalne isikutunnistus. Seda kasutamiseks vajad Mobiil-ID sertifikaadi, mis on kehtiv 5 aastat. Seda saab kontrollida oma mobiilsideoperaatori iseteeninduses.",output_ID.ee +0c9712a2-aab0-4d5a-a316-56336cf8497c,1,user,"Tere! Mul on küsimus Mobiil-ID kohta. Kas ma saan teada, kui kaua need sertifikaadid kehtivad?",output_ID.ee +0c9712a2-aab0-4d5a-a316-56336cf8497c,2,assistant,"Tere! Jah, Mobiil-ID sertifikaadid kehtivad 5 aastat. Kuidas ma saan teada, kas teie enda sertifikaat on aktiivne?",output_ID.ee +70543cc0-04b4-4a95-a9bd-2de4273cb5ca,1,user,"Tere! Ma arvan, et Mobiil-ID on minu jaoks oluline. Kuidas saan aru, kas minu Mobiil-ID on ikkagi kehtiv?",output_ID.ee +70543cc0-04b4-4a95-a9bd-2de4273cb5ca,2,assistant,"Tere! Noh, seda saab kindlasti kontrollida. Sul on võimalik seda kontrollida oma mobiilsideoperaatori iseteeninduses. Telia mobiil-ID leiate siin: https://www.telia.ee/era/lisateenused/mobiil-id, Elisa mobiil-ID siin: https://www.elisa.ee/et/eraklient/apid-ja-lisateenused/lisateenused/mobiil-id, ja Tele2 mobiil-ID siin: https://tele2.ee/teenused/mugavusteenused/mobiil-id. Kas sa tead, milline operaator sulle Mobiil-ID andis?",output_ID.ee diff --git a/experiments/base_model_training/experiments/bert/confusion_matrix.png b/experiments/base_model_training/experiments/bert/confusion_matrix.png new file mode 100644 index 00000000..b66b6511 Binary files /dev/null and b/experiments/base_model_training/experiments/bert/confusion_matrix.png differ diff --git a/experiments/base_model_training/experiments/bert/predictions.txt b/experiments/base_model_training/experiments/bert/predictions.txt new file mode 100644 index 00000000..f923f9b2 --- /dev/null +++ b/experiments/base_model_training/experiments/bert/predictions.txt @@ -0,0 +1,228 @@ +user: Tere! Ma ostsin äsja uued jalanõud ja ma ei saa päris alguses aru, mida kõik need sildid tähendavad. Mida ma peaksin seal märkima? +assistant: Tere! Jalanõudel peaksid leidma märkused pealsete, voodri ja talla valmistamisel kasutatud materjalid. Samuti on oluline, et jalatsitel oleks märgistatud nende otstarve – kas need sobivad tänaval kandmiseks või siseruumides kasutamiseks. 0 0 +user: Tere! Ma tahaksin teada, kuidas ma saan lapsele passi taotleda. Mis on esimene asi, mida ma pean tegema? +assistant: Tere! Esiteks, pead broneerima aja politsei teeninduses. Sa leiad teenindused ja saad aja broneerida siin: https://broneering.politsei.ee/. Või saad dokumendi kaupluses kätte, aga sel juhul pead arvestama lisatasuga. 1 1 +user: Tere! Ma sain DigiDoc4 käivitamisel vea, et sertifikaatide usaldusnimekirja uuendamine ebaõnnestus. Mis see tähendab ja mida ma sellega pean tegema? +assistant: Tere! See tähendab, et arvutis, millega DigiDoc4 töötab, ei ole uusima usaldusnimekirja versiooni. See on vajalik allkirjade kontrollimiseks. Selle probleemiga toime tulla, kõigepealt veenduge, et teil on arvutis kõige uuem ID-tarkvara paigaldatud. Saate seda hankida siin: https://www.id.ee/artikkel/paigalda-id-tarkvara/ . Kas te seda juba proovite? 2 2 +user: Tere! Kas te saate rääkida ID-tarkvara värskendamisest? Mul on küsimusi selle kohta. +assistant: Tere! Muidugi, ma saan. ID-tarkvara värskendamine on väga lihtne. Meie veebilehel on automaatselt kindluses oleva tarkvara uusim versioon. Kas te olete Windows, macOS või Linux kasutaja? See aitab öelda, millist versiooni peaksite alla laadima. 2 2 +user: Tere! Mulle on vaja lennureisija õigustest rohkem teada. Mis on olulisim asi, mida ma peaksin meeles pidama? +assistant: Tere! Olulisim asi on see, et sul on õigus saada abi lennujaamas, olenemata sinu erivajadusest. See tähendab, et saad abi registreerimisel, pagasiregistreerimisel ning isegi transpordiga väravani jõudmiseks. On oluline teatada abivajadusest teenusepakkujale 48 tundi enne lennu väljumisaega. 1 0 +user: Tere! Olen kuulnud midagi ühtse laadija kohta. Mis see tähendab ja miks on see oluline? +assistant: Tere! Ühtne laadija tähendab, et paljud seadmed hakkavad kasutama USB-C laadimispesa, mis on juba üha levinum. See on oluline, kuna see standardiseerib laadimist, muudab laadijate ostmist kergemaks ja toetab kiirlaadimise tehnoloogiat. 0 0 +user: Tere! Mul on palju lugemist, aga ma tahan teada, mida tähendab "tarbija käendusleping"? Mis see üldse on? +assistant: Tere! Tarbija käendusleping on olukord, kus sa kohustud sõbra või sugulase laenu eest tasuma, kui ta ise seda ei suuda. See annab laenuandjale lisakindluse, et laen tagastatakse. Olenemata sellest, kas tegemist on väikelaenu, järelmaksu, tarbimislaenu või eluasemelaenu puhul. Kas soovid, et selgitan täpsemalt sellega seotud nüansse? 1 0 +user: Tere! Ma olen kuulnud Tarbijakaitse Ametist, aga ei tea täpselt, mis tarbija õigused on. Mis on kõige olulisem asi, mida tarbija peaks teadma? +assistant: Tere! Oluline on teada, et sul on õigus nõuda ja saada kaup või teenus, mis vastab nõuetele ja on ohutu sinu elule, tervisele ja varale. Samuti on sul õigus saada infot, et teha teadlik valik. See tähendab, et müüjalt peaks alati küsima vajalikku teavet toote või teenuse kohta. 0 0 +user: Tere! Mis õigused on mul kui lennureisija? +assistant: Tere! Lennureisijana on sul mitmeid õigusi. Näiteks, kui lend hiljub või tühistatakse, sul on õigus teavitamist saada ja hüvitist nõuda, samuti saad abi saada lennujaamas, kui sul on erivajadusi. On ka õigusi seoses pagasi transpordi ja transpordivahendi kahjustuse puhul. Soovid, et ma puutuksin mõnele neist detailsemalt lahti? 1 0 +user: Tere! Ma kuulsin midagi hoonete energiatõhususest ja Tarbijakaitse ja Tehnilise Järelevalve Ametist. Mis see tähendab täpselt? +assistant: Tere! Hoonete energiatõhusus tähendab seda, et uutele ja oluliselt rekonstrueeritavatele hoonetele on kehtestatud miinimumnõuded selle kohta, kui palju energiat nad tarbivad. Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib, et neid nõudeid järgitaks. Näiteks, uuelt hoonelt ootatakse, et see oleks energiatõhusam kui varem. Kas sa tahaksid teada, kuidas need nõuded täpsemalt kehtivad? 0 0 +user: Tere! Ma lugesin TTJA veebilehel sideturu kohta ja natuke segadus. Mis need hulgiturgude teenused täpsemalt on, mida seal loetletud on? Näiteks kõne lõpetamise hulgiteenus telefonivõrgus - mis see tähendab? +assistant: Tere! Hea, et küsid. Need hulgiturgude teenused on sellised teenused, mida operaatorid pakuvad teistele operaatoritele, mitte otse lõppkasutajale. Näiteks, kõne lõpetamise hulgiteenus telefonivõrgus tähendab, et üks operaator maksab teisele operaatorile selle eest, et tema klient saaks teha kõne teise operaatori klientidele. Sisuliselt on see hulgiturgude teenus, mis võimaldab erinevatel operaatoritel omavahel ühendust tagada. 0 0 +user: Tere! Ma olen huvitatud teadma, kuidas Politsei- ja Piirivalveamet pöördumistele vastates minu isikuandmeid kasutab? +assistant: Tere! Pöördumiste lahendamiseks ja teile vastamiseks kasutame teie isikuandmeid. Kui peame teistele asutustele või isikutele järelepärimisi esitama, edastame neile ainult nii palju andmeid, kui on vajalik vastuse koostamiseks. Me ei avalda neid andmeid rohkem kui on tarvis. 1 1 +user: Tere! Ma saan DigiDoc4 käivitades veateadet, et sertifikaatide usaldusnimekirja uuendamine ebaõnnestub. Kas teate, mis selle vastu pean tegema? +assistant: Tere! Jah, seda probleemi on mõeldud just sellele vea korral. See tähendab, et DigiDoc4 ei saa oma sertifikaate teiega usaldusväärselt kontrollida. Alustame kõigepealt sellest, et veenduksime, et sul on ID-tarkvara kõige uuem versioon. Võite seda vaadata ja vajadusel paigaldada siin: https://www.id.ee/artikkel/paigalda-id-tarkvara/ 2 2 +user: Tere! Ma sain just elamisloa ettevõtluseks suurinvestorina. Kust ma saan selle elamisloakaardi kätte? +assistant: Õnnitleme! Elamisloakaardi saad kätte isiklikult kas Eesti välisesinduses või Politsei- ja Piirivalveameti teeninduses. Tõenäoliselt märkisid sa seda juba oma tähtajalise elamisloa taotluses, kus said valida sobiva koha. 1 1 +user: Tere! Mul on küsimus elamisloa kohta. Mis juhtub, kui minu elamisluba täisealisele lapsele pereliikme juurde elama asumiseks lõppeb? +assistant: Tere! Kui sinu elamisluba lõppeb, saad Eestisse jääda veel kuni 270 päevaks. Sel perioodil on sul ka võimalus Eestis töötada. 1 1 +user: Tere! Ma kuulsin palju 5G kohta, aga ei saa päris hästi aru, mida see tähendab. Mis on need kolm peamist arengusuunda, mida ITU näeb 5G puhul? +assistant: Tere! ITU näeb 5G puhul kolme peamist arengusuunda: suurendatud andmesidekiirus (eMBB), tagatud ajakriitiline reaalajasüsteemide andmeside (URLLC) ja suurearvuline väikeseadmete kommunikatsioon (mMTC). See tähendab lihtsalt seda, et 5G lubab kiiremaid andmesidegaid, võimaldab reaalajas teenuseid ja toetab rohkemat arvukust seadmete ühenduse kohta. 2 0 +user: Tere! Ma tahaksin teada, kuidas ma saan Politsei- ja Piirivalveametilt teavet küsida. Mis on kõige lihtsam viis? +assistant: Tere! Pöördumised PPA poole saab saata e-posti teel või postiaadressil Pärnu mnt 139, 15060 Tallinn. Kui saata e-postiga faile, palun krüpteeri need sertifikaadiga „Politsei- ja Piirivalveamet (PPA) 70008747“ turvalisuse tagamiseks. 1 1 +user: Tere! Ma pole kunagi digiallkirjastanud midagi mobiiliga. Kas see on üldse lihtne? +assistant: Tere! Jah, digiallkirjastamine RIA DigiDoc mobiilirakendusega on väga lihtne. See on mõeldud nii ID-kaardi, mobiil-ID kui ka Smart-ID kasutajatele. Pärast lohistamist saab sa sellega lihtsalt dokumente allkirjastada. Kas sul on ID-kaart, mobiil-ID või Smart-ID? 2 2 +user: Tere! Ma kuulsin, et 112 on hädaabinumber. Aga millal täpselt ma peaksin sellele helistama? +assistant: Tere! Sa peaksid 112-le helistama siis, kui sa vajad politsei kiiret abi ja tahad teatada korrarikkumisest, kuriteost või liiklusõnnetusest. See on number, millele helistada, kui on vaja kiiret sekkumist. 1 1 +user: Tere! Mul on küsimus raudtee ohutuse kohta. Mida peaksin silmas pidama, kui lähen raudteeülesõidu juurde? +assistant: Tere! Oluline on peata kiirust, et vajaduse korral saaksid peatuda. Kontrolli liiklusmärke ja valgusfoore ning vaata ja kuula, et veenduda, et liikumine on ohutu. Enne üleminekut veendu alati, et lähenev raudteesõidukile anda teed. 0 0 +user: Tere! Ma imestan, kuidas Politsei- ja Piirivalveametiga (PPA) ühendust saada, kui mul on küsimus? +assistant: Tere! PPA registrikood on 70008747 ja aadress on Pärnu mnt 139, 15060 Tallinn. Sa saad meiega ühendust faksi teel, number on 612 3009. Üldiselt on riigiinfo telefon 1247 ka hea võimalus alustuseks. 1 1 +user: Tere! Kas te saate mulle öelda, mis on Smart-ID ja miks seda peaks kasutama? +assistant: Tere! Muidugi. Smart-ID on tasuta nutirakendus, millega saate sisse logida erinevates e-teenustes, kinnitada tehinguid ja allkirjastada dokumende. See on nagu ID-kaart, digi-ID või mobiil-ID, aga see on mugavam kasutada telefoni kaudu. Kas sa tahaksid rohkem teada, kuidas see töötab? 2 2 +user: Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Ametil on kaart, mis näitab sideteenuste kättesaadavust. Mis see kaart täpselt on ja milleks see mõeldud? +assistant: Tere! Jah, meil on sideteenuste kaart. See näitab, kas aadressil on võimalik interneti teenusega liituda. Kaart põhineb sideoperaatorite andmetel nende tavahinnakirjade alusel. Roheline punkt tähendab, et liitumisvõimalus on, punane aga mitte. 0 0 +user: Tere! Ma ei ole päris kindel, kuidas oma ID-kaardi sertifikaate vaadata ja neid arvutis salveida. Kas saate mind selles osas aidata? +assistant: Muidugi! Üsna lihtne on see. Esmalt võid kaardilugeja arvutiga ühendada ja oma ID-kaardi sisse süstada. Seejäna avad DigiDoc4 programmi. Pärast seda ootad, kuni sinu ID-kaardi andmed sisse loetakse. Kas sul on kaardilugeja ja arvuti, mis on valmis kasutama? 2 2 +user: Tere! Ma saan teada, et Narva arestimajas on mingid reeglid pakikandmise kohta. Kas ma saan sinna saates käimas käibele pakki viia kellelegi? +assistant: Tere! Kahjuks mitte. Arestialustele pakke üldiselt tuua ei saa. On teisi küsimusi selle kohta? 1 1 +user: Tere! Kuulsin, et ID-kaartidega on midagi juhtunud. Mis siis täpselt on juhtunud sertifikaatidega? +assistant: Tere! Jah, 2018. aasta 1. aprillist tunnistati turvanõrkusega ID-kaardi sertifikaadid kehtetuks. See tähendab, et kui sa ei uuendanud neid 31. märtsiks 2018, siis elektrooniliselt ID-kaarti enam ei saa kasutada. 2 1 +user: Tere! Ma pole päris kindel, mida täpselt tähendab ‘digitaalne allkiri’. Kas see on sama asjalik kui tavaline kiri allkiri? +assistant: Tere! Hea küsimus! Jah, mõttes on see väga sarnane, aga digitaalse allkirja puhul saab sellega kindluse, et see ei ole lihtsalt võltsitav. Tavaline kiri on mõelnud kui kirjalik kinnitus, kuid digitaalse allkirjaga saab kindlustada, et see on seaduslikult kehtiv ja seda ei saa muuta pärast allkirjastamist. See vastab omakäelisele allkirjale juriidiliselt. 0 2 +user: Tere! Ma uurisin Politsei ja Piirivalve ameti veebilehel infot lapse kodakondsuse kohta. Kuidas saan teada, millal saan lapse kodakondsustunnistuse? +assistant: Tere! Kui laps saab Eesti kodakondsuse, saadetakse kodakondsustunnistus teile posti teel. Täpne ajakavast sõltub menetluse kiirusest, aga tunniste tulek postiga on tavapärane viis dokumentide edastamine. 1 1 +user: Tere! Ma olen Tarbijakaitse ja Tehnilise Järelevalve Ameti uus praktikant. Räägime natuke raadiosageduste kasutamisest. Mis on EFIS ja milleks seda kasutada saab? +assistant: Tere! EFIS on Frequency Information System – Euroopa Raadiosageduste Informatsiooni Süsteem. See andmebaas võimaldab näha, kuidas konkreetne riik sagedusala (9kHz kuni 275 GHz) kasutab ja võrrelda seda teiste Euroopa riikidega. Samuti leiad sealt infot erinevates riikides kehtestatud nõuetele raadioseadmete kasutamisele ja sageduste jaotustele elektroonilise side teenuste osutamiseks. 0 0 +user: Tere! Ma olen kuulnud CE-märgist, aga ma ei tea päris täpselt, mis see tähendab. Saaksin natuke selgitust? +assistant: Tere! Muidugi. CE-märgist kinnitab tootja või tema volitatud esindaja tootele. See tähendab, et tootja kinnitab, et toode vastab ELi ühtlustatud nõuetele. Mis konkreetsemalt sind CE-märgise kohta huvitab? 2 0 +user: Tere! Mul on küsimus lennureisijate õiguste kohta. Mis juhtub, kui mu lend hiljub? +assistant: Tere! Kui sinu lend hiljub, on sul õigus hüvitisele, aga see sõltub hilinemise põhjustust. Kui lend hiljub kolm tundi või rohkem, ja hilinemine on vedaja süül, saad hüvitist. Kui aga hilinemine on erakorraliste asjaolude tõttu (mis on vedaja kontrolli alt väljas), hüvitist ei maksta. Täpsemalt on hüvitise summa olenev lennu pikkusest. 0 0 +user: Tere! Ostsin hiljaaegu veebist kaamera Saksamaalt. Mida ma peaksin teadma, kui sellega peaks midagi viga olema? +assistant: Tere! Saksamaalt ostetud kaamera puhul on oluline teada, et EL-i ja Euroopa Majanduspiirkonna riikides (Saksamaa on ka selles piirkonnas) on tarbijatele sarnane kaitsetase. Sul on õigus esitada kaebus defektide kohta vähemalt kahe aasta jooksul alates ostukuupäevast. Tähtis on, et sul oleks ostutšekk või maksekorralduse väljavõte tõendamaks ostu. 0 0 +user: Tere! Ma sain trahviteate kiiruskaamerast, aga ma ei saa aru, kas fotot saab ka saada? +assistant: Tere! Põhimõtteliselt jah, saad. Trahviteatega ei saadetata fotot automaatselt. Kuid mootorsõiduki eest vastutav isik saab taotleda fotokoopiat. Seda tuleb teha aga eraldi taotlusega, millele on lisatud digitaal- või tavaallkiri. 1 1 +user: Tere! Ma kuulsin, et piiri tugevdamiseks on rajatud lõiketraati. Kas see ei mõju metsloomadele? Palju nad sellega hädaldavad? +assistant: Tere! See on väga hea küsimus. Tegelikult on seda küsimust juba arutletud. Mure on küll sees, et lõiketraat võib loomade liikumist piirata. Kuid politsei on Keskkonnaametiga kooskõlastanud, kuidas olukorda lahendada, kui loomad juhtu takerduma. 0 1 +user: Tere! Ma arvan, et mobiil-ID on hea asi, aga kuidas seda saab tellida? Ma ei tea, kust alustada. +assistant: Tere! Päris hea küsimus. Mobiil-ID tellimine on päris lihtne. Seda saate tellida oma mobiilsideoperaatori juures - Telia, Elisa või Tele2. Kõik kolm pakuvad seda serviisi. Palju lihtsam on, kui saad teada, millise operaatori pakub sulle kõige sobivam teenus! 2 2 +user: Tere! Ma nägin midagi energiamärgistuse kohta, aga ei saa pähe. Mis see tähendab ja miks see oluline on? +assistant: Tere! Energiamärgistus näitab, kui palju energiat seade kulutab. See on oluline, sest see aitab teil valida energiasäästlikumaid tooteid, mis aitab vähendada teie energiakulusid ja ka keskkonna mõju. ELis on kehtestatud ühtsed nõuded mitmele tootegrupile, näiteks külmikutele, pesumasinatele ja televiisioritele. 0 0 +user: Tere! Ma pole kunagi varem ID tarkvara uuendanud. Miks on see nii oluline? +assistant: Tere! Palju maasõna! ID tarkvara uuendamine on väga oluline turva tõttu ja sellepärast, et see hakkaks paremini töötama. Ühest küljest tagab uuendamine, et sa oled kaitstud uusimaid turvariske vastu. Teisest küljest aitab uuendamine, et tarkvara oleks otstarbeks mugavam kasutada. 2 2 +user: Tere! Ma arvasin, et ID-kaardiga saab reisida, aga ma pole kindel. Kus ma saan teada, kuhu just saan ID-kaardiga reisida? +assistant: Tere! Vastus on, et ID-kaardiga saab reisida Euroopa Liidu (EL) ja Euroopa Majanduspiirkonna (EMP) liikmesriikidesse. Aga kõige täpsema info saad ReisiTargalt veebilehel: https://reisitargalt.vm.ee/kontrolli-reisidokumente/. Seal näeb, millistesse riikidesse ID-kaardiga reisida on lubatud. 1 2 +user: Tere! Ma soovin tööle minna Eestis ettevõtluse pidamiseks, aga mul pole hetkel Eestis elamisloa. Kuidas saan selle taotleda? +assistant: Tere! Palju õnne otsusega Eestis ettevõtlust alustada! Elamisluba ettevõtluseks saab taotleda, kui investeerid Eestis oma äriühingusse. Esiteks pead investeerima. Kas sa oled juba seda tegema planeerinud? Ja siis pead kohutama nõutud dokumentidele ja riigilõive. 1 1 +user: Tere! Mul on küsimus defektse toote kohta. Mis on minu õigused, kui kaubal on viga? +assistant: Tere! Kui tootel on viga, on Sul õigus nõuda müüjalt esmalt tasuta parandamist või selle asendamist uue kaubaga. Müüja valib reeglina ise, kas parandada või asendada, kuid saavad kokku leppida. On ka olukorda, kus müüjal ei ole võimalik kaupa parandada või asendada, parandamine ebaõnnestub, müüja ei kõrvalda puudust mõistliku aja jooksul või Sulle tekitatakse põhjendamatuid ebamugavusi. 0 0 +user: Tere! Mul on küsimus – kui ma olen jäänud kelmuse ohvriks, kuhu ma peaksin kõigepealt pöörduma? +assistant: Tere! Esimese asjana on väga oluline kohe ühendust võtta oma pangaga. Nad saavad sageli kiiresti reaalseid meetmeid võtma, et vältida lisakahjustusi. Swedbank, SEB, LHV ja Luminor kõik pakuvad selleks oma turvalisuse leheküljed, näiteks Swedbanki oma on siin: https://www.swedbank.ee/private/d2d/ebanking/secureBanking 0 1 +user: Tere! Ma kuulsin midagi CE-märgise kohta. Mis see tähendab ja kes sellele tootele kinnitada tohib? +assistant: Tere! CE-märgis näitab, et toode vastab EL-i ühtlustatud nõuetele. Selle kinnitada tohib ainult tootja või tema volitatud esindaja. See on nagu kinnitus, et toode on ohutu ja vastab nõuetele. 0 0 +user: Tere! Kas ma saan ID-kaardiga reisida Euroopa Liitu? Kas mul peab olema pass? +assistant: Tere! Jah, ID-kaardiga saad reisida Euroopa Liitu ja Euroopa Majanduspiirkonna liikmesriikidesse ilma passi kohta. Pass on aga alati vajalik dokument, sest ID-kaart on ainult reisidokumendina lubatud. Kas sa mõtled täpseid riikseid reegleid? 2 2 +user: Tere! Kas ma saan ID-kaardi kasutamiseks oma arvutis ID-tarkvara paigaldata? +assistant: Tere! Jah, seda saab. ID-tarkvara on võimalik paigaldada Windowsi, macOS ja Ubuntu operatsioonisüsteemidel. Kuid on oluline teada, et see ei toeta vanemaid operatsioonisüsteeme. Mis operatsioonisüsteemi sa kasutad? 2 2 +user: Tere! Ma kuulsin, et Tarbijakaitse Amet on teinud meeleseisundi raudteeohutuse kohta. Mis see tähendab? +assistant: Tere! Jah, meil ongi meeleseisund, mis tuletab meile kõik olulised punktid raudtee ohutuse kohta. Põhimõtteliselt tuletab see meid kõiki hoiatama, et raudteed on ohtlikud piirkonnad, kui eiratakse liikluseeskirju. 0 0 +user: Tere! Mul on küsimus TTJA otsuste kohta. Mis saab, kui ma ei nõustu TTJA otsusega? +assistant: Tere! Kui sa ei nõustu TTJA otsusega, on sul õigus selle vaidlustada. Sul on kaks võimalust: esitada vaie TTJA peadirektorile või esitada kaebus Tallinna Halduskohtule. 1 0 +user: Tere! Ma uurisin politseijaoskondi ja nägin Lääne-Harju Kesklinna politseijaoskonna nime. Kus see asub? +assistant: Tere! Lääne-Harju Kesklinna politseijaoskond asub Kolde puiestee 65, 10321 Tallinn. 1 1 +user: Tere! Mul on küsimus. Ma pean taotlema passi lapsele, kes on välismaalane. Kuidas ma riigilõivu maksin? +assistant: Tere! Riigilõivu tasumiseks pead kasutama Rahandusministeeriumi kontot. Saaja on Rahandusministeerium, viitenumber on 2900082443. Pangaülekandeks on saadaval erinevad kontod, näiteks SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor (EE701700017001577198), LHV Pank (EE777700771003813400). 1 1 +user: Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) jälgib gaasiseadmete ohutust. Kuidas see täpselt toimib? +assistant: Tere! TTJA peab silmas, et gaasiseadmed müügil ei tohiks ohustada inimeste tervist ja turvalisust. See tähendab, et nad peavad olema valmistatud ja paigaldatud nõuetekohaselt, regulaarselt hooldatud ning kasutama ettenähtud otstarbel. TTJA võib peatada ohtliku gaasiseadme levitamise ja kasutamise, kui tekib kahtlus ning on õigus nõuda selle turult kõrvaldamist, kui ekspertiis näitab, et seade ei vasta nõuetele. 0 0 +user: Tere! Ma tahaksin teada, kuidas Narva arestimajas külastajatele lubatud on asju toimetada. Kas saab sinna pakke saata? +assistant: Tere! Narva arestimajasse pakke ei saa toimetada. Ainult vahistatul on lubatud isiklikult toimetada asju. 1 1 +user: Tere! Ma tahaksin teada, mida ma peaksin silmas pidama, kui ma võtan laenu. Mis on vastutustundliku laenamise põhimõtted? +assistant: Tere! Vastutustundliku laenamise põhimõtted tähendavad, et nii laenuandjad kui ka tarbijad peavad olema ettevaatlikud. Enne laenu, järelmaksu või liisingu võtmist, võta pakkumisi paarilt-kolmelt eri firmalt ja küsi tingimuste võrdlemiseks koos pakkumisega teabelehte. See leht sisaldab kõiki olulisi laenutingimusi. 0 0 +user: Tere! Ma arvan, et ma peaksin taotlema alaline elamisõigus, aga ma ei ole päris kindel, kuidas see toimib. Kas Politsei- ja Piirivalveamet seda menetleb? +assistant: Tere! Jah, Politsei- ja Piirivalveamet kindlasti menetleb selle. Nad kontrollivad, et sa oled esitanud kõik vajalikud dokumendid ja et sa täidate elamisõiguse saamiseks nõuetekohased tingimused. Kui mingis punktis puudub midagi, sa saad teada. Kas sul on täna mõni konkreetne dokument, mille kohta sa arvad, et puudub midagi? 1 1 +user: Tere! Ma olen välismaal ja tahaksin teada, kuidas ma saan elamisloakaarti taotleda. On see võimalik? +assistant: Tere! Jah, on võimalik. Välismaal saab elamisloakaarti taotleda erandkorras, kui oled välismaalane, kes ei ole EL liikmesriigi kodanik, sul on kehtiv Eesti elamisluba või elamisõigus ning sinu välismaa elukoht on Eesti rahvastikuregistris registreeritud. 1 1 +user: Tere! Ma pole päris kindel, mis need elektroonilised allkirjad on ja miks need nii olulised on Euroopas. Kas sa võiksid seda lihtsamalt seletada? +assistant: Tere! Muidugi. Elektroonilised allkirjad on nagu tavaline allkiri, aga see on tehtud arvutis. Need on olulised, sest need aitavad meil e-tehinguteselt kindlust saada, et tegelikult on see just sina, kes selle allkirjastanud oled. Euroopas on selleks vajalikud reeglid, et nad oleksid kõik maailmas ühesugused. 1 2 +user: Tere! Ma olen huvitatud Eesti kodakondsuse taotlemisest. Kui ma esitan taotluse, mis siis juhtub järgmisena? +assistant: Tere! Pärast taotluse esitamist edastatakse dokumendid Vabariigi Valitsusele. Nad langetavad otsuse sinu kodakondsuse andmise kohta. 1 1 +user: Tere! Olen kuulnud seiklusparkidest, aga ei tea väga palju. Mis on seiklusparki mineku reeglid, mida peaksin teadma? +assistant: Tere! Seikluspark on tore koht aktiivseks puhkuseks. Enne rajale minekut on oluline tutvuda reeglite ja piirangutega. Näiteks, on oluline hinnata oma võimeid ja tervislikku seisundit, eriti kui kavatsete läbida raskemaid radu. Kas Sa oled huvitatud, et ma rääkiks täpsemalt raskusastmete kohta? 1 0 +user: Tere! Ma olen kuulnud, et Tarbijakaitseamet aitab probleemide puhul. Mis on esimene asi, mida ma peaksin tegema, kui mul on probleem mingi ostuga? +assistant: Tere! Absoluutselt, Tarbijakaitseamet on abiks. Kõigepealt peaksite kirjutama kaebusena kirja kauplejale. Kui kaebus ei lahenda probleemi, saate pöörduda tarbijavaidluste komisjoni poole. 0 0 +user: Tere! Ma kaotasin oma passi ja ma ei tea, kust ma saan uue passi tellida. Kas Põlva politseiosakond saab mulle aidata? +assistant: Tere! Jah, Põlva politseiosakond saab teid aidata. Parim viis passi taotlemiseks on PPA iseteenindus veebilehel: https://www.politsei.ee/et/iseteenindus. Võite ka tulla kohale. Põlva teenindus asub Võru 12, 63308 Põlva. 1 1 +user: Tere! Kas ID-kaart on mõni uus dokument Eestis või on see vana? +assistant: Tere! ID-kaart on Eesti Vabariigis esmane isikut tõendav dokument. See on suhteliselt uus, sest selle kasutamine on seadusest tuleneva kohustus. 1 2 +user: Tere! Ma tahan teada, mis on Eesti kodaniku passi tellimisel riigilõivu suurus? +assistant: Tere! Eesti kodaniku passi riigilõivu suurust saad teada Politsei- ja Piirivalveameti veebilehel. Link on https://www.politsei.ee/et/telli-dokument-selverisse. Täpset summat saad seal vaadata. 1 1 +user: Tere! Ma lugesin sellest artikkel kohta tarbijavaidluste komisjoni. Mis on selle komisjoni peamine ülesanne? +assistant: Tere! Tarbijavaidluste komisjoni peamine ülesanne on lahendada vaidlusi tarbija ja ettevõtte vahel, mis puudutavad tarbekaupade või -teenuste ostu-müüki. Nad aitavad leida lahendusi, kui tarbija on rahul teenusega või toodetega. 0 0 +user: Tere! Ma olen kuulnud Ida-Harju politseijaoskonnast. Kus see asub täpselt? +assistant: Tere! Ida-Harju politseijaoskond asub aadressil P. Pinna 4, 13615 Tallinn. 1 1 +user: Tere! Olen kuulnud, et on võimalik laenu maksepuhkust taotleda. Kuidas see üldiselt toimib? +assistant: Tere! Jah, on õige. Maksepuhkus võimaldab teil laenu tagasimaksmist lühikeseks ajaks peatada, kui teil on raskusi. Üldjuhul antakse see kuni kuueks kuuks, ja sel ajal ei maksa laenu põhiselt, vaid intressi tuleb ikkagi edasi tasuda. Oluline on märkida, et seda tuleks taotleda alles siis, kui sissetuleku vähenemine on probleem ja teised kulud kokku tõmmata ei aita. 0 0 +user: Tere! Ma kuulasin rääkimist eIDAS-ist. Kas te võtaksite need mõned e-tehingud käsitlevaid küsimusi vastu? +assistant: Tere! Muidugi, võtan need küsimused vastu. eIDAS ongi Euroopa Liidu määrus, mis on loodud selleks, et lihtsustada piiriülest e-teenuste kasutamist. Kas sa tead, milles see lihtsustamine just seisab? 2 2 +user: Tere! Ma olen välisriigi kodanik ja plaanin Eestis äri alustada. Kas mulle on vaja mingit erist elamisluba? +assistant: Tere! Jah, kui sa ei ole Euroopa Liidu riigi, Islandi, Liechtensteini, Norra või Šveitsi kodanik, siis Eestis ettevõtlusega tegelneda on vaja elamisluba ettevõtluseks. See on Politsei- ja Piirivalveameti väljastatav dokument. 1 1 +user: Tere! Kas tean, et mobiil-ID on olemas, aga pole päris kindel, mida see tähendab ja kas see on minu jaoks hea? +assistant: Tere! On tore, et huvi on! Mobiil-ID on tegelikult väga hea lahendus, kui vajate digitaalset isikutunnistust. See on tänapäeval mugavam ja turvameelt hoogu andev kui traditsiooniline ID-kaart. Sellega saate viibida eesti.ee-s, internetipankas, või osaleda e-valimistel. Kas teil on küsimusi selle kohta? 2 2 +user: Tere! Ma olen täiskasvanu ja tahan elamisloakaardi taotlemisega alustada. Kuidas ma riigilõivu maksta saan? +assistant: Tere! Elamisloakaardi taotlemise riigilõivu maksmiseks on mitu võimalust. Saaja on Rahandusministeerium, ja viitenumber on 2900082443. Pangakontod on erinevad: SEB EE891010220034796011, Swedbank EE932200221023778606, Luminor EE701700017001577198 ja LHV Pank EE777700771003813400. 1 1 +user: Tere! Kas te saate öelda, kus ma saan RIA DigiDoc mobiilirakenduse uue versiooni alla laadida? +assistant: Muidugi! RIA DigiDoc mobiilirakendus on saadaval nii App Store'ist (https://itunes.apple.com/ee/app/ria-digidoc/id1216104448?mt=8) kui ka Google Play poodist (https://play.google.com/store/apps/details?id=ee.ria.DigiDoc). 2 2 +user: Tere! Ma olen välisriigi kodanik ja plaanin Eestis äri alustada. Kas ma kindlasti vajan elamisluba selleks? +assistant: Tere! Vaelisriigi kodanikuna jah, enamikul juhtudel vajad. Kui sa ei ole Euroopa Liidu riikide, Islandi, Liechtensteini, Norra või Šveitsi kodanik, siis ettevõtluseks Eestis on vaja elamisluba. 1 1 +user: Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb numeratsiooniga. Mis see täpsemalt tähendab? +assistant: Tere! TTJA väljastab numbrilube sideteenuse osutajatele, ehk sideettevõtjatele, ning ka neile, kes pakuvad muud teenust side teenuse abil, näiteks sisuteenuseid. See tähendab, et nad määravad, kes tohib kasutada erinevaid numbrimudeleid, näiteks telefoninumbreid või mobiilnumbreid. 0 0 +user: Tere! Mul on küsimus TTJA otsuste kohta. Mis teha, kui ma ei nõustu TTJA tehtud otsusega? +assistant: Tere! Kui te ei nõustu TTJA tehtud otsusega, on teil õigus selle vaidlustada. Teil on kaks võimalust: esitada vaie TTJA peadirektorile või esitada kaebus Tallinna Halduskohtule. 1 0 +user: Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on lapsevanematele mingit ohutusraamatut välja andnud. Kas see on tõsi? +assistant: Jah, see on tõsi! Politsei- ja Piirivalveamet ongi valmistanud ohutusraamatu just 1. klassi minevate laste vanematele. See on mõeldud eelkõige lapsevanemate toetamiseks lapse turvalisuse tagamisel. 1 1 +user: Tere! Mul on küsimusi rahvusvahelise kaitse taotlemise kohta, eriti seoses olukorraga Ukrainas. Kuidas ma saan seda taotleda? +assistant: Tere! Rahvusvahelise kaitse taotlemiseks võite pöörduda nelja kohta: Tallinnas Pärnu mnt 139, Pärnus H. Tammsaare pst 61, Jõhvis Rahu tn 38 või Tartus Riia tee 132. Samuti on saadaval ankeedid Politsei- ja Piirivalveameti veebilehel. Aluseks on põhjendus, et te kardate tagakiusamist Ukrainas rassi, rahvuse, usu, ühiskondliku rühmitusse kuulumise või poliitiliste vaadete tõttu või et tagasipöördumine Ukrainasse kujutab endast tõsist ohtu. 1 1 +user: Tere! Ma arvan, et ma saan ID-kaardiga ilma passita Euroopasse reisida? Kas see on võimalik? +assistant: Tere! Jah, ID-kaardiga saate reisida Euroopa Liitu (EL) ja Euroopa Majanduspiirkonda (EMP) kuuluvad riigid ilma passi kohta. See on võimalik, aga oluline on meeles pidada, et ID-kaart on vajalik reisidokumendina ja EI sobi paljude teiste dokumentidega, nagu näiteks elamisloa kaart või digi-ID. 1 2 +user: Tere! Ma saan probleeme ID-kaardi kasutamisega. Kas sulle teada on, kuidas kindlustada, et ID-kaart ja lugeja töötavad korralikult? +assistant: Tere! Ma saan aru, et sul on probleeme. Alguses on oluline, et sa veedad hetkel vääris aega ja saad lugema selle artikli: https://www.id.ee/artikkel/paigalda-id-tarkvara. Seega veedad hetkel vääris aega ja saad lugema selle artikli: https://www.id.ee/artikkel/paigalda-id-tarkvara. Siis on oluline, et sinul oleks arvutisse ID-tarkvara paigaldatud, ja vajadusel on ID-kaardi kasutamiseks seadistatud veebibrauserid. 2 2 +user: Tere! Ma saan probleeme mobiil-IDga allkirjastamisel. Mis võiks olla põhjus? +assistant: Tere! Võib olla mitmeid asja. Sageli on probleem ajutine ja laheneb ise. Palju olgem vaadates veateadet – see sisalda alati vajaliku info probleemi lahendamiseks. Esmalt luge veateade täies ulatuses. Kas sa tead, mis veateadet sa nägid? 2 2 +user: Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on saanud raha piiriehituseks. Kas see on tõsi? +assistant: Jah, see on tõsi. Politsei- ja Piirivalveamet on saanud toetust piiriehituse rahastamiseks Euroopa Liidu Sisejulgeolekufondi välispiiride ja viisade rahastamisvahendist perioodil 2014-2020. 1 1 +user: Tere! Ma tahan ID-tarkvara Ubuntu peale paigaldada. Kuidas ma kõige paremini alustan? +assistant: Tere! Väga hea küsimus. Ubuntu jaoks on kõige mugavam paigaldada ID-tarkvara automaatskripti abil. Alustame sellega: esmalt pead salvestama skripti oma arvuti tahvlkale. Siis avad Terminali ja navigeerid kataloogi, kuhu sa skripti salvestasid. Näiteks, kui sa seda 'Downloads' kataloogi, siis sisestad käskluse `cd /home/kasutajanimi/Downloads` (kasutajanimi asendades sinu kasutajanimega). Skripti käivitamiseks siis `sh install-open-eid.sh` ning parool sisestamisel lihtsalt vajuta edasi. 2 2 +user: Tere! Kas ma saan Eesti kodakondsuse taotleda, kui ma olen eestkostetav? +assistant: Tere! Jah, võimalik on. Aga on mõned tingimused, mida pead täistõttu täitma. Peamine asi on see, et pead olema vähemalt 15-aastane. Kas te seda teate? 1 1 +user: Tere! Ma tahan lapsele ID-kaarti taotleda. Kuidas ma riigilõivu maksta saan? +assistant: Tere! ID-kaardi taotlemisel lapsele riigilõivu tasumisel on mitmeid võimalusi. Kui taotled iseteeninduses, saad riigilõivu tasuda otse sealsamas. Kui aga teed pangaülekande, siis saaja on Rahandusministeerium, viitenumber on 2900082443. Kontopõhjad on erinevate pankade jaoks erinevad: SEB EE891010220034796011, Swedbank EE932200221023778606, Luminor Bank EE701700017001577198, LHV Pank EE777700771003813400. 1 1 +user: Tere! Ma tean, et on mobiil-ID, aga kas see on nagu tavaline ID-kaart, aga telefoni sees? +assistant: Tere! Jah, täpselt nii! Mobiil-ID on SIM-kaardi põhjal toimiv digitaalne isikutunnistus, mis on samasugune kui tavaline ID-kaart (digi-ID, elamisloakaart) – see on riiklikult garanteeritud. See on lihtsalt kasutuses mobiilseadmega. Kas sulle on oluline teada, kuidas seda kasutada? Kas sa mõtled, kuidas taotleda või kuidas sellega tegelikult midagi teha? 2 2 +user: Tere! Olen kuulnud, et mõned tooted on ohtlikud. Kuidas ma saan teada, millised tooted on just praegu probleemiks? +assistant: Tere! EL-is on selleks kiirhoiatussüsteem Safety Gate. Saa tutvuda nädala ülevaatega nende kodulehel https://ec.europa.eu/consumers/consumers_safety/safety_products/rapex/alerts/?event=main.listNotifications&lng=en või kasuta otsingut, kui sul on huvi mingi kindla toote suhtes. 0 0 +user: Tere! Ma just sain teada, et meil on uus beebi. Mida me peame nüüd kodakondsuse osas tegema? +assistant: Õnne! Kui teie laps saab Eesti kodakondsuse, siis teile saadetakse posti teel kodakondsustunnistus. See on tähtis dokument! Meil on Politsei- ja Piirivalveamet vastutab selle väljastamise. 1 1 +user: Tere! Mul on küsimus WiFi-seadmete kasutamise kohta. Mis on need piirangud, kui ma näiteks tahaksin kodus selliseid seadmeid kasutada? +assistant: Tere! WiFi-seadmete kasutamisel on oluline teada, et erinevad sagedusribad on reguleeritud erinevate piirangutega. Näiteks 2,4 GHz sagedusel on lubatud võimsus kuni 100 mW ja selle kasutamine ei ole piiratud. Täpsem info on leitav määruse „Raadiosageduste kasutamise tingimused ja tehnilised nõuded sagedusloast vabastatud raadioseadmetele“ lisast 6. 0 0 +user: Tere! Ma olen kuulnud, et lennusõitjatel on teatud õigused. Mis need on? +assistant: Tere! Jah, lennusõitjatel on mitmeid õigusi. Näiteks, kui lennud hiljenevad või tühistatakse, on teil õigus teavitus saada, hüvitist nõuda ja abi saada tagasipöörduse korral. Lisaks on teil õigus saada abi lennujaamas kui teil on erivajadused. 0 0 +user: Tere! Ma ei ole päris kindel, mis on eIDAS. Kas saate mulle sellest natuke selgeks teha? See on midagi, mis seotakse ID.ee? +assistant: Tere! Jah, eIDAS on Euroopa Liidu määrus, mis seotakse ID.ee-ga. Lihtsasti öeldud, see on seadus, mis näeb ette, kuidas digiteenuseid ja e-tehinguid turvaliselt identifitseerida ja kinnitada. See aitab inimestel digimaailmas ohksele saada. See määrus jõudis jõule 23. juulil 2014. aastal. 1 2 +user: Tere! Ma olen kuulnud, et relvaloa kehtivust on võimalik pikendada. Mis siis täpsemalt on sellega seotud, kui ma tahan seda enda puhul teha? +assistant: Tere! Relvaloa kehtivuse pikendamisel on oluline, miks sa relvaloa üldse omad. Kui relva otstarbeks on märgitud jahipidamine või sport, siis täiendavat laskekatset ei pea tegema. Kui aga on märgitud ainult enese ja vara kaitseks, siis pead selleks täiendava laskekatse sooritama. 0 1 +user: Tere! Ma arvan, et mul on ID-kaart ja ma tahan sellega arvutis midagi tegema. Mis ma pean tegema, et see tööle hakkaks? +assistant: Tere! Üsna hea küsimus! Et saaksid ID-kaardi arvutis kasutama, pead paigaldama ID-tarkvara. See tarkvara on sinu arvuti jaoks vajalik, et ID-kaardiga elektroonilisi toiminguid teha. Kas sa kasutad Windowsi, macOS või Linuxi operatsioonisüsteemi? 2 2 +user: Tere! Ma olen kuulnud, et Politsei- ja Piirivalveamet kasutab minu isikuandmeid, kui ma saan teiega ühendust. Kas see on nii ja millal? +assistant: Tere! Jah, see on õige. Me kasutame isikuandmeid pöördumiste lahendamiseks ja teile vastamiseks. Kui peame pöördumist lahendades küsimusi esitama teistele asutustele, edastame neile ainult need andmed, mis on vastuse koostamiseks tõesti vajalikud. See aitab meil privaatsust kaitsta. 1 1 +user: Tere! Kas teeksite mulle infot ID-tarkvara uusima versiooni kohta? +assistant: Tere! Jah, küllap. ID-tarkvara uusimad versioonid on ID.ee lehel saadaval. Meie süte automaatselt teie arvuti operatsioonisüsteemi ja pakub sobiva paketi. On lihtne, peate vaid tarkvara alla lahtima ja paigaldama. 2 2 +user: Tere! Ma lugesin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb ka kauplejate komisjoni töoga. Kuidas see üldiselt toimib, kui mul on mingi probleem kauplusega? +assistant: Tere! Üldjuhul, kui sul on probleem, esitad avalduse TTJAs. Komisjon annab kauplejale aega oma seisukohti selgitada ja vastata sinu väidetele. See annab neile võimaluse probleemi lahendada enne, kui asi jõuab komisjoni istungini. 0 0 +user: Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) on tellinud mitmeid uuringuid. Kas sa saaksid rääkida neist? +assistant: Tere! Jah, TTJA on tellinud mitmeid uuringuid. Üks olulisi on uuring passiivse lairibataristu rajamise kohta maapiirkondadesse, mille tellis Majandus- ja Kommunikatsiooniministeerium. Lisaks on tehtud uuring Eesti elanike tarbijate õiguste teadlikkusest ja e-kaubanduse turust. 0 0 +user: Tere! Ma arvan, et tean, et ID.ee on olemas, aga ma ei ole päris kindel, kuidas digiallkirjastamist oma veebisüsteemi juurde lisada. Kas teil on näiteks lihtne viis, kuidas seda saaksin teha? +assistant: Tere! Päev! Jah, digiallkirjastamist saab lisada oma veebisüsteemi mitmel viisil. Alustuseks on Riigi allkirjastamisteenus (SiGa) valitsusektori jaoks väga hea võimalus. See on aga tarkvara, mida peab enda serverile paigaldama. Kas teete veebirakendusi väljapoole valitsust? 2 2 +user: Tere! Ma olen ajakirjanik ja tahaksin teada, kuidas Politsei- ja Piirivalveametiga pressilisti liikmeks saada. Mis on selleks vaja? +assistant: Tere! Liitumine on lihtne. Saate saata e-kirja aadressil, mis on kirjeldatud ametisõnumites. Kirjutage kirja enda nimi, väljaande nimi ja e-posti aadress. 1 1 +user: Tere! Mul on mõned küsimused selle kohta, mida ma saan teha, kui ma olen veebist midagi ostnud ja ma ei ole sellega rahul. Mis on minu õigused? +assistant: Tere! Kui oled midagi veebist ostnud, on sul õigus lepingust taganeda 14 päeva jooksul alates sellest, kui kaubad sinuni jõudsid. See on nn 14-päevane taganemisõigus. See kehtib ka teenuste puhul, kus taganemisõiguse tähtaeg algab lepingu sõlmimisest. 0 0 +user: Tere! Ma olen Tarbijakaitse ja Tehnilise Järelevalve Ameti uus töötaja. Üks minu ülesandete kohta on raadiosageduste kasutamine. Kust ma peaksin alustama, et aru saada, kuidas seda Eestis reguleeritakse? +assistant: Tere! Suurepärane! Alustada saaks Eesti raadiosagedusplaanist. See on meil avaldatud ja saadaval Riigiteataja veebilehel: https://www.riigiteataja.ee/akt/125012019006. See dokument annab kõige olulisema ülevaate sellest, kuidas Eestis raadiosagedusi kasutatakse ja mis on sellega seotud reeglid. 0 0 +user: Tere! Ma sain uue ID-kaardi ja koodiümbriku. Kas ma pean kohe PIN- ja PUK-koodid vahetama? +assistant: Tere! Jah, väga hea, et sa sai uue ID-kaardi ja koodiümbriku! Soovitav on PIN- ja PUK-koodid kohe vahetada. See tagab parema turvalisuse. Koodiümbrikus on uusad koodid, ja on hea, kui sa neid kohe uusima kaardi külge kinnitatakse. 2 2 +user: Tere! Ma kuulsin, et Rail Balticu ehitamine on keeruline projekt. Mis kõige suuremad väljakutsed sellega seoses on olnud? +assistant: Tere! Jah, Rail Balticu ehitamine on tegelikult üsna keeruline. Üks suurim väljakutse on see, et olemasoleva raudtee ümberehitus, näiteks kurvide sirgemaks tegemine või teise rööpapaari lisamine, on tehtud analüüside järgi kallim kui uue trassi ehitamine. Lisaks on oluline arvestada, et Tartu kaudu minev trass ei võimalda kasutada Euroopa ühendamise rahastu toetust samas mahus. 0 0 +user: Tere! Ma ei ole päris kindel, mis need sertifikaadid on ja miks on need nii olulised. Kas saaksid seda veidi selgitada? +assistant: Muidugi! Ühest küljest on sertifikaadid nagu digitaalne isikutunnusjoon, mis seob sind sinu digitaalse isikutuvastusvahendiga, näiteks ID-kaardiga või mobiil-ID-ga. See tähendab, et kui sa kasutad e-teenust, kasutatakse seda sertifikaati, et kinnitada, et sa oled see, keda sa väidate olevat. On nagu digitaalne kiri, millega sa kinnitad oma identiteedi. Kuidas sa need tavaliselt kasutad? 2 2 +user: Tere! Ma saan aru, et RIA DigiDoc on mobiilirakendus. Kas see on täielikult ligipääsetav kõigile kasutajatele, kes seda kasutavad? +assistant: Tere! Jah, RIA DigiDoc on ehitatud vastavalt WCAG 2.1 AA suunetele, mis tähendab, et seda on mõeldud kasutamiseks ka inimestele, kellel on nägemisvõimete puudega või muud liiklusvõimete probleemid. Küll aga on teatud piirkonnad, kus ligipääsetavus pole täielik. Meie teates on detailsemalt kirjas, milles need puudumised on. 0 2 +user: Tere! Ma plaanin elamisloakaardi taotlemist. Kuidas ma riigilõivu maksta saan? +assistant: Tere! Elamisloakaardi taotlemisel riigilõivu tasumiseks on mitmeid võimalusi. Saaja on Rahandusministeerium. Viitenumber on 2900082443. Pangad, kuhu saad maksta, on SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor Bank (EE701700017001577198) ja LHV Pank (EE777700771003813400). 1 1 +user: Tere! Ma olen ostmas kuldehit, aga ma ei tea, millele ma peaksin tähelepanu pöörama. Mida peaksin kontrollima? +assistant: Tere! Kuldehit ostes on oluline, et sellel oleksid proovi- ja nimemärgid. Proovimärgis näitab, millist metalli on kasutatud ja selle osa sulamis, näiteks 375, 585 või 916. See märk on tuhandeksüsteemis. Nimemärgis on tootja või maaletoja nimi. See peab olema registreeritud riiklikus registris. Kas sa teadsid seda? 1 0 +user: Tere! Ma ostsin hiljuti televiisori, aga see ei tööta korralikult. Kuidas ma peaksin selle kohta kaebust esitama? +assistant: Tere! Esiteks, otsige üles ostutšekk või mingi muu dokument, mis tõendab ostu. See on oluline. Seejärel pöörduge viivitamatult selle poodi, kust televiisori ostsite. Kaebuse esitamine peaks toimuma hiljemalt kahe kuu jooksul alates vea avastamisest. 0 0 +user: Tere! Ma plaanin elamisloakaarti taotleda. Kus ma teenindusse ma peaksin minema ja mida ma kaasa võtma peaksin? +assistant: Tere! Elamisloakaardi taotlemiseks saad valida sobiva teeninduse kohta Politsei- ja Piirivalveameti veebilehelt. Kindlasti kaasa isikut tõendav dokument. Sa saad aja broneerida veebilehel https://broneering.politsei.ee/ kui soovid. 1 1 +user: Tere! Ma mõtlesin, mis see Mobiil-ID on? Kas see on uus ID-kaart? +assistant: Tere! Mobiil-ID on juurdekasv ID-kaardi kasutamisele. See on mobiiltelefoniga kaasasolev vebinimiliselt kindel identifikaator. See annab teile juurdejuurdepääsu e-teenustele, ilma et peaksite ID-kaarti kaasas kandma. Kas te mõtlete, et see on samasugune kui ID-kaart? See on juurdekasv! 2 2 +user: Tere! Ma olen veidi segadus ID-kaardi taotlemisel. Kuidas liikumisvõimetule inimesele ID-kaart taotleda saab? Mis on kõige lihtsam viis? +assistant: Tere! Liikumisvõimetule inimesele on mitmeid võimalusi ID-kaardi taotlemiseks. Iseteeninduses saab taotleda, kui te olete täiskasvanu või alaealine laps, ja teil on ID-kaart, mobiil-ID või Smart-ID. Seal peate üles laadima foto ja maksma riigilõivu pangalingi kaudu. Kas teil on iseteenindusega kogemust? 2 1 +user: Tere! Ma tean, et võin taotleda alaline elamisoigust EL-ist pärit inimesena. Aga kuidas see poliisiasutus selle menetletakse? +assistant: Tere! Jah, seda saab teha. Politsei- ja Piirivalveamet kontrollib taotluse. Nad peaksid veenduma, et teil on kõik nõuetekohased dokumendid, ja et teil on täidetud kõik elamisõiguse saamiseks vajalikud tingimused. Küll aga ei pruugiks teil midagi lisaks kuuldagi, kui kõik on korras. Kas te soovite rohkem teada? 1 1 +user: Tere! Mul on probleem Windowsiga ID tarkvara paigaldamisel. Ilmnes mingi tõrk. Kas te saate mind aidata? +assistant: Tere! Ma saan küll. Palun räägi rohkem sellest tõrkest. Kas sa saad mingi veaõht (koodiga) või veateate? Ja millise ID tarkvara sa paigaldate? 2 2 +user: Tere! Ma tahan küsida Kuressaare arestimaja kohta. Kas sinna saab pakke tuua? +assistant: Tere! Kuressaare arestimajas ei saa arestalustele pakke tuua. Vahistatule aga lubatakse üks pakk kuus alates 01.08.2020. 1 1 +user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TJAA) tegelda koduohutusega. Mis see täpsemalt tähendab? +assistant: Tere! Nõmme, TJAA jälgib, et ehitis oleks ohutu kogu oma eluea jooksul. Põhimõte on, et omanik vastutab ehitise ohutuse eest. Korterelamus on oluline vaheline erinevus reaalosade ja kaasomandi vahel. Mis on sinu jaoks kõige olulisem selles? 0 0 +user: Tere! Ma olen kuulnud, et Tarbijakaitseamet kaitseb tarbija õigusi. Aga mida see täpsemalt tähendab? Millised on mu peamised õigused ostjana? +assistant: Tere! Jah, Tarbijakaitseamet on seal, et kaitsta sind tarbijana. Peamiselt sa saad nõuda kaupa või teenust, mis vastab nõuetele, on ohutu ja mida ei ole keelatud omada või kasutada. Samuti on sul õigus saada vajalikku teavet pakutavate kaupade ja teenuste kohta, et sa saaksid teadlikult valida. Ja kui su õigusi rikutakse, saad nõu ja abi Tarbijakaitseametilt! 0 0 +user: Tere! Ma arvan, et politsei on mõnikord avalikule koosoledule sekkuda. Miks nad seda teevad, ja millistel juhtudel? +assistant: Tere! Jah, see on tõsi. Politsei saab avalisele koosoledule sekkuda, kui arvab, et on olukord, kus koosoleku pidamine ohustab inimelu või seaduslikkust. Näiteks, kui on alust arvata, et kohas, kus koosolek toimub, on olukord, kus saab tõesti kiiresti süütu tekitada. Mida täpsemalt sa küsid? 1 1 +user: Tere! Ma nägin seda MyID portaali. Mis see üldse on? Kas see on midagi uut? +assistant: Tere! MyID portaal on uus platvorm, mida pakub SK ID Solutions AS (SK), mobiil-ID, ID-kaardi, Digi-ID ja Smart-ID kontode kohta. See on koht, kus saad teavet nende kontode kohta ja sellest, mida neis tehtud on. Kas sa tahad teada rohkem, kuidas seda kasutada või mis teeb selle eriliseks? 2 2 +user: Tere! Mul on küsimusi rõivaste ja jalatste märgistamise kohta. Mis on kõige olulisem, mida ma peaks ostes vaatama? +assistant: Tere! Oluline on vaadata, kas rõival või jalatseid on näidatud toote nimetus, suurus, materjalikoostis (eriti oluline, kui ei ole nahast või karusnahast) ja hooldusjuhised ehk hooldustähistus. Need on põhilised asjad, mis annavad sulle olulist teavet toote kohta. 0 0 diff --git a/experiments/base_model_training/experiments/roberta/confusion_matrix.png b/experiments/base_model_training/experiments/roberta/confusion_matrix.png new file mode 100644 index 00000000..012e1314 Binary files /dev/null and b/experiments/base_model_training/experiments/roberta/confusion_matrix.png differ diff --git a/experiments/base_model_training/experiments/roberta/predictions.txt b/experiments/base_model_training/experiments/roberta/predictions.txt new file mode 100644 index 00000000..8fb75187 --- /dev/null +++ b/experiments/base_model_training/experiments/roberta/predictions.txt @@ -0,0 +1,228 @@ +user: Tere! Ma ostsin äsja uued jalanõud ja ma ei saa päris alguses aru, mida kõik need sildid tähendavad. Mida ma peaksin seal märkima? +assistant: Tere! Jalanõudel peaksid leidma märkused pealsete, voodri ja talla valmistamisel kasutatud materjalid. Samuti on oluline, et jalatsitel oleks märgistatud nende otstarve – kas need sobivad tänaval kandmiseks või siseruumides kasutamiseks. 0 0 +user: Tere! Ma tahaksin teada, kuidas ma saan lapsele passi taotleda. Mis on esimene asi, mida ma pean tegema? +assistant: Tere! Esiteks, pead broneerima aja politsei teeninduses. Sa leiad teenindused ja saad aja broneerida siin: https://broneering.politsei.ee/. Või saad dokumendi kaupluses kätte, aga sel juhul pead arvestama lisatasuga. 1 1 +user: Tere! Ma sain DigiDoc4 käivitamisel vea, et sertifikaatide usaldusnimekirja uuendamine ebaõnnestus. Mis see tähendab ja mida ma sellega pean tegema? +assistant: Tere! See tähendab, et arvutis, millega DigiDoc4 töötab, ei ole uusima usaldusnimekirja versiooni. See on vajalik allkirjade kontrollimiseks. Selle probleemiga toime tulla, kõigepealt veenduge, et teil on arvutis kõige uuem ID-tarkvara paigaldatud. Saate seda hankida siin: https://www.id.ee/artikkel/paigalda-id-tarkvara/ . Kas te seda juba proovite? 2 2 +user: Tere! Kas te saate rääkida ID-tarkvara värskendamisest? Mul on küsimusi selle kohta. +assistant: Tere! Muidugi, ma saan. ID-tarkvara värskendamine on väga lihtne. Meie veebilehel on automaatselt kindluses oleva tarkvara uusim versioon. Kas te olete Windows, macOS või Linux kasutaja? See aitab öelda, millist versiooni peaksite alla laadima. 2 2 +user: Tere! Mulle on vaja lennureisija õigustest rohkem teada. Mis on olulisim asi, mida ma peaksin meeles pidama? +assistant: Tere! Olulisim asi on see, et sul on õigus saada abi lennujaamas, olenemata sinu erivajadusest. See tähendab, et saad abi registreerimisel, pagasiregistreerimisel ning isegi transpordiga väravani jõudmiseks. On oluline teatada abivajadusest teenusepakkujale 48 tundi enne lennu väljumisaega. 1 0 +user: Tere! Olen kuulnud midagi ühtse laadija kohta. Mis see tähendab ja miks on see oluline? +assistant: Tere! Ühtne laadija tähendab, et paljud seadmed hakkavad kasutama USB-C laadimispesa, mis on juba üha levinum. See on oluline, kuna see standardiseerib laadimist, muudab laadijate ostmist kergemaks ja toetab kiirlaadimise tehnoloogiat. 0 0 +user: Tere! Mul on palju lugemist, aga ma tahan teada, mida tähendab "tarbija käendusleping"? Mis see üldse on? +assistant: Tere! Tarbija käendusleping on olukord, kus sa kohustud sõbra või sugulase laenu eest tasuma, kui ta ise seda ei suuda. See annab laenuandjale lisakindluse, et laen tagastatakse. Olenemata sellest, kas tegemist on väikelaenu, järelmaksu, tarbimislaenu või eluasemelaenu puhul. Kas soovid, et selgitan täpsemalt sellega seotud nüansse? 0 0 +user: Tere! Ma olen kuulnud Tarbijakaitse Ametist, aga ei tea täpselt, mis tarbija õigused on. Mis on kõige olulisem asi, mida tarbija peaks teadma? +assistant: Tere! Oluline on teada, et sul on õigus nõuda ja saada kaup või teenus, mis vastab nõuetele ja on ohutu sinu elule, tervisele ja varale. Samuti on sul õigus saada infot, et teha teadlik valik. See tähendab, et müüjalt peaks alati küsima vajalikku teavet toote või teenuse kohta. 0 0 +user: Tere! Mis õigused on mul kui lennureisija? +assistant: Tere! Lennureisijana on sul mitmeid õigusi. Näiteks, kui lend hiljub või tühistatakse, sul on õigus teavitamist saada ja hüvitist nõuda, samuti saad abi saada lennujaamas, kui sul on erivajadusi. On ka õigusi seoses pagasi transpordi ja transpordivahendi kahjustuse puhul. Soovid, et ma puutuksin mõnele neist detailsemalt lahti? 0 0 +user: Tere! Ma kuulsin midagi hoonete energiatõhususest ja Tarbijakaitse ja Tehnilise Järelevalve Ametist. Mis see tähendab täpselt? +assistant: Tere! Hoonete energiatõhusus tähendab seda, et uutele ja oluliselt rekonstrueeritavatele hoonetele on kehtestatud miinimumnõuded selle kohta, kui palju energiat nad tarbivad. Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib, et neid nõudeid järgitaks. Näiteks, uuelt hoonelt ootatakse, et see oleks energiatõhusam kui varem. Kas sa tahaksid teada, kuidas need nõuded täpsemalt kehtivad? 0 0 +user: Tere! Ma lugesin TTJA veebilehel sideturu kohta ja natuke segadus. Mis need hulgiturgude teenused täpsemalt on, mida seal loetletud on? Näiteks kõne lõpetamise hulgiteenus telefonivõrgus - mis see tähendab? +assistant: Tere! Hea, et küsid. Need hulgiturgude teenused on sellised teenused, mida operaatorid pakuvad teistele operaatoritele, mitte otse lõppkasutajale. Näiteks, kõne lõpetamise hulgiteenus telefonivõrgus tähendab, et üks operaator maksab teisele operaatorile selle eest, et tema klient saaks teha kõne teise operaatori klientidele. Sisuliselt on see hulgiturgude teenus, mis võimaldab erinevatel operaatoritel omavahel ühendust tagada. 0 0 +user: Tere! Ma olen huvitatud teadma, kuidas Politsei- ja Piirivalveamet pöördumistele vastates minu isikuandmeid kasutab? +assistant: Tere! Pöördumiste lahendamiseks ja teile vastamiseks kasutame teie isikuandmeid. Kui peame teistele asutustele või isikutele järelepärimisi esitama, edastame neile ainult nii palju andmeid, kui on vajalik vastuse koostamiseks. Me ei avalda neid andmeid rohkem kui on tarvis. 1 1 +user: Tere! Ma saan DigiDoc4 käivitades veateadet, et sertifikaatide usaldusnimekirja uuendamine ebaõnnestub. Kas teate, mis selle vastu pean tegema? +assistant: Tere! Jah, seda probleemi on mõeldud just sellele vea korral. See tähendab, et DigiDoc4 ei saa oma sertifikaate teiega usaldusväärselt kontrollida. Alustame kõigepealt sellest, et veenduksime, et sul on ID-tarkvara kõige uuem versioon. Võite seda vaadata ja vajadusel paigaldada siin: https://www.id.ee/artikkel/paigalda-id-tarkvara/ 2 2 +user: Tere! Ma sain just elamisloa ettevõtluseks suurinvestorina. Kust ma saan selle elamisloakaardi kätte? +assistant: Õnnitleme! Elamisloakaardi saad kätte isiklikult kas Eesti välisesinduses või Politsei- ja Piirivalveameti teeninduses. Tõenäoliselt märkisid sa seda juba oma tähtajalise elamisloa taotluses, kus said valida sobiva koha. 1 1 +user: Tere! Mul on küsimus elamisloa kohta. Mis juhtub, kui minu elamisluba täisealisele lapsele pereliikme juurde elama asumiseks lõppeb? +assistant: Tere! Kui sinu elamisluba lõppeb, saad Eestisse jääda veel kuni 270 päevaks. Sel perioodil on sul ka võimalus Eestis töötada. 1 1 +user: Tere! Ma kuulsin palju 5G kohta, aga ei saa päris hästi aru, mida see tähendab. Mis on need kolm peamist arengusuunda, mida ITU näeb 5G puhul? +assistant: Tere! ITU näeb 5G puhul kolme peamist arengusuunda: suurendatud andmesidekiirus (eMBB), tagatud ajakriitiline reaalajasüsteemide andmeside (URLLC) ja suurearvuline väikeseadmete kommunikatsioon (mMTC). See tähendab lihtsalt seda, et 5G lubab kiiremaid andmesidegaid, võimaldab reaalajas teenuseid ja toetab rohkemat arvukust seadmete ühenduse kohta. 0 0 +user: Tere! Ma tahaksin teada, kuidas ma saan Politsei- ja Piirivalveametilt teavet küsida. Mis on kõige lihtsam viis? +assistant: Tere! Pöördumised PPA poole saab saata e-posti teel või postiaadressil Pärnu mnt 139, 15060 Tallinn. Kui saata e-postiga faile, palun krüpteeri need sertifikaadiga „Politsei- ja Piirivalveamet (PPA) 70008747“ turvalisuse tagamiseks. 1 1 +user: Tere! Ma pole kunagi digiallkirjastanud midagi mobiiliga. Kas see on üldse lihtne? +assistant: Tere! Jah, digiallkirjastamine RIA DigiDoc mobiilirakendusega on väga lihtne. See on mõeldud nii ID-kaardi, mobiil-ID kui ka Smart-ID kasutajatele. Pärast lohistamist saab sa sellega lihtsalt dokumente allkirjastada. Kas sul on ID-kaart, mobiil-ID või Smart-ID? 2 2 +user: Tere! Ma kuulsin, et 112 on hädaabinumber. Aga millal täpselt ma peaksin sellele helistama? +assistant: Tere! Sa peaksid 112-le helistama siis, kui sa vajad politsei kiiret abi ja tahad teatada korrarikkumisest, kuriteost või liiklusõnnetusest. See on number, millele helistada, kui on vaja kiiret sekkumist. 1 1 +user: Tere! Mul on küsimus raudtee ohutuse kohta. Mida peaksin silmas pidama, kui lähen raudteeülesõidu juurde? +assistant: Tere! Oluline on peata kiirust, et vajaduse korral saaksid peatuda. Kontrolli liiklusmärke ja valgusfoore ning vaata ja kuula, et veenduda, et liikumine on ohutu. Enne üleminekut veendu alati, et lähenev raudteesõidukile anda teed. 0 0 +user: Tere! Ma imestan, kuidas Politsei- ja Piirivalveametiga (PPA) ühendust saada, kui mul on küsimus? +assistant: Tere! PPA registrikood on 70008747 ja aadress on Pärnu mnt 139, 15060 Tallinn. Sa saad meiega ühendust faksi teel, number on 612 3009. Üldiselt on riigiinfo telefon 1247 ka hea võimalus alustuseks. 1 1 +user: Tere! Kas te saate mulle öelda, mis on Smart-ID ja miks seda peaks kasutama? +assistant: Tere! Muidugi. Smart-ID on tasuta nutirakendus, millega saate sisse logida erinevates e-teenustes, kinnitada tehinguid ja allkirjastada dokumende. See on nagu ID-kaart, digi-ID või mobiil-ID, aga see on mugavam kasutada telefoni kaudu. Kas sa tahaksid rohkem teada, kuidas see töötab? 2 2 +user: Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Ametil on kaart, mis näitab sideteenuste kättesaadavust. Mis see kaart täpselt on ja milleks see mõeldud? +assistant: Tere! Jah, meil on sideteenuste kaart. See näitab, kas aadressil on võimalik interneti teenusega liituda. Kaart põhineb sideoperaatorite andmetel nende tavahinnakirjade alusel. Roheline punkt tähendab, et liitumisvõimalus on, punane aga mitte. 0 0 +user: Tere! Ma ei ole päris kindel, kuidas oma ID-kaardi sertifikaate vaadata ja neid arvutis salveida. Kas saate mind selles osas aidata? +assistant: Muidugi! Üsna lihtne on see. Esmalt võid kaardilugeja arvutiga ühendada ja oma ID-kaardi sisse süstada. Seejäna avad DigiDoc4 programmi. Pärast seda ootad, kuni sinu ID-kaardi andmed sisse loetakse. Kas sul on kaardilugeja ja arvuti, mis on valmis kasutama? 2 2 +user: Tere! Ma saan teada, et Narva arestimajas on mingid reeglid pakikandmise kohta. Kas ma saan sinna saates käimas käibele pakki viia kellelegi? +assistant: Tere! Kahjuks mitte. Arestialustele pakke üldiselt tuua ei saa. On teisi küsimusi selle kohta? 1 1 +user: Tere! Kuulsin, et ID-kaartidega on midagi juhtunud. Mis siis täpselt on juhtunud sertifikaatidega? +assistant: Tere! Jah, 2018. aasta 1. aprillist tunnistati turvanõrkusega ID-kaardi sertifikaadid kehtetuks. See tähendab, et kui sa ei uuendanud neid 31. märtsiks 2018, siis elektrooniliselt ID-kaarti enam ei saa kasutada. 2 1 +user: Tere! Ma pole päris kindel, mida täpselt tähendab ‘digitaalne allkiri’. Kas see on sama asjalik kui tavaline kiri allkiri? +assistant: Tere! Hea küsimus! Jah, mõttes on see väga sarnane, aga digitaalse allkirja puhul saab sellega kindluse, et see ei ole lihtsalt võltsitav. Tavaline kiri on mõelnud kui kirjalik kinnitus, kuid digitaalse allkirjaga saab kindlustada, et see on seaduslikult kehtiv ja seda ei saa muuta pärast allkirjastamist. See vastab omakäelisele allkirjale juriidiliselt. 0 2 +user: Tere! Ma uurisin Politsei ja Piirivalve ameti veebilehel infot lapse kodakondsuse kohta. Kuidas saan teada, millal saan lapse kodakondsustunnistuse? +assistant: Tere! Kui laps saab Eesti kodakondsuse, saadetakse kodakondsustunnistus teile posti teel. Täpne ajakavast sõltub menetluse kiirusest, aga tunniste tulek postiga on tavapärane viis dokumentide edastamine. 1 1 +user: Tere! Ma olen Tarbijakaitse ja Tehnilise Järelevalve Ameti uus praktikant. Räägime natuke raadiosageduste kasutamisest. Mis on EFIS ja milleks seda kasutada saab? +assistant: Tere! EFIS on Frequency Information System – Euroopa Raadiosageduste Informatsiooni Süsteem. See andmebaas võimaldab näha, kuidas konkreetne riik sagedusala (9kHz kuni 275 GHz) kasutab ja võrrelda seda teiste Euroopa riikidega. Samuti leiad sealt infot erinevates riikides kehtestatud nõuetele raadioseadmete kasutamisele ja sageduste jaotustele elektroonilise side teenuste osutamiseks. 0 0 +user: Tere! Ma olen kuulnud CE-märgist, aga ma ei tea päris täpselt, mis see tähendab. Saaksin natuke selgitust? +assistant: Tere! Muidugi. CE-märgist kinnitab tootja või tema volitatud esindaja tootele. See tähendab, et tootja kinnitab, et toode vastab ELi ühtlustatud nõuetele. Mis konkreetsemalt sind CE-märgise kohta huvitab? 0 0 +user: Tere! Mul on küsimus lennureisijate õiguste kohta. Mis juhtub, kui mu lend hiljub? +assistant: Tere! Kui sinu lend hiljub, on sul õigus hüvitisele, aga see sõltub hilinemise põhjustust. Kui lend hiljub kolm tundi või rohkem, ja hilinemine on vedaja süül, saad hüvitist. Kui aga hilinemine on erakorraliste asjaolude tõttu (mis on vedaja kontrolli alt väljas), hüvitist ei maksta. Täpsemalt on hüvitise summa olenev lennu pikkusest. 0 0 +user: Tere! Ostsin hiljaaegu veebist kaamera Saksamaalt. Mida ma peaksin teadma, kui sellega peaks midagi viga olema? +assistant: Tere! Saksamaalt ostetud kaamera puhul on oluline teada, et EL-i ja Euroopa Majanduspiirkonna riikides (Saksamaa on ka selles piirkonnas) on tarbijatele sarnane kaitsetase. Sul on õigus esitada kaebus defektide kohta vähemalt kahe aasta jooksul alates ostukuupäevast. Tähtis on, et sul oleks ostutšekk või maksekorralduse väljavõte tõendamaks ostu. 0 0 +user: Tere! Ma sain trahviteate kiiruskaamerast, aga ma ei saa aru, kas fotot saab ka saada? +assistant: Tere! Põhimõtteliselt jah, saad. Trahviteatega ei saadetata fotot automaatselt. Kuid mootorsõiduki eest vastutav isik saab taotleda fotokoopiat. Seda tuleb teha aga eraldi taotlusega, millele on lisatud digitaal- või tavaallkiri. 0 1 +user: Tere! Ma kuulsin, et piiri tugevdamiseks on rajatud lõiketraati. Kas see ei mõju metsloomadele? Palju nad sellega hädaldavad? +assistant: Tere! See on väga hea küsimus. Tegelikult on seda küsimust juba arutletud. Mure on küll sees, et lõiketraat võib loomade liikumist piirata. Kuid politsei on Keskkonnaametiga kooskõlastanud, kuidas olukorda lahendada, kui loomad juhtu takerduma. 0 1 +user: Tere! Ma arvan, et mobiil-ID on hea asi, aga kuidas seda saab tellida? Ma ei tea, kust alustada. +assistant: Tere! Päris hea küsimus. Mobiil-ID tellimine on päris lihtne. Seda saate tellida oma mobiilsideoperaatori juures - Telia, Elisa või Tele2. Kõik kolm pakuvad seda serviisi. Palju lihtsam on, kui saad teada, millise operaatori pakub sulle kõige sobivam teenus! 1 2 +user: Tere! Ma nägin midagi energiamärgistuse kohta, aga ei saa pähe. Mis see tähendab ja miks see oluline on? +assistant: Tere! Energiamärgistus näitab, kui palju energiat seade kulutab. See on oluline, sest see aitab teil valida energiasäästlikumaid tooteid, mis aitab vähendada teie energiakulusid ja ka keskkonna mõju. ELis on kehtestatud ühtsed nõuded mitmele tootegrupile, näiteks külmikutele, pesumasinatele ja televiisioritele. 0 0 +user: Tere! Ma pole kunagi varem ID tarkvara uuendanud. Miks on see nii oluline? +assistant: Tere! Palju maasõna! ID tarkvara uuendamine on väga oluline turva tõttu ja sellepärast, et see hakkaks paremini töötama. Ühest küljest tagab uuendamine, et sa oled kaitstud uusimaid turvariske vastu. Teisest küljest aitab uuendamine, et tarkvara oleks otstarbeks mugavam kasutada. 2 2 +user: Tere! Ma arvasin, et ID-kaardiga saab reisida, aga ma pole kindel. Kus ma saan teada, kuhu just saan ID-kaardiga reisida? +assistant: Tere! Vastus on, et ID-kaardiga saab reisida Euroopa Liidu (EL) ja Euroopa Majanduspiirkonna (EMP) liikmesriikidesse. Aga kõige täpsema info saad ReisiTargalt veebilehel: https://reisitargalt.vm.ee/kontrolli-reisidokumente/. Seal näeb, millistesse riikidesse ID-kaardiga reisida on lubatud. 1 2 +user: Tere! Ma soovin tööle minna Eestis ettevõtluse pidamiseks, aga mul pole hetkel Eestis elamisloa. Kuidas saan selle taotleda? +assistant: Tere! Palju õnne otsusega Eestis ettevõtlust alustada! Elamisluba ettevõtluseks saab taotleda, kui investeerid Eestis oma äriühingusse. Esiteks pead investeerima. Kas sa oled juba seda tegema planeerinud? Ja siis pead kohutama nõutud dokumentidele ja riigilõive. 1 1 +user: Tere! Mul on küsimus defektse toote kohta. Mis on minu õigused, kui kaubal on viga? +assistant: Tere! Kui tootel on viga, on Sul õigus nõuda müüjalt esmalt tasuta parandamist või selle asendamist uue kaubaga. Müüja valib reeglina ise, kas parandada või asendada, kuid saavad kokku leppida. On ka olukorda, kus müüjal ei ole võimalik kaupa parandada või asendada, parandamine ebaõnnestub, müüja ei kõrvalda puudust mõistliku aja jooksul või Sulle tekitatakse põhjendamatuid ebamugavusi. 0 0 +user: Tere! Mul on küsimus – kui ma olen jäänud kelmuse ohvriks, kuhu ma peaksin kõigepealt pöörduma? +assistant: Tere! Esimese asjana on väga oluline kohe ühendust võtta oma pangaga. Nad saavad sageli kiiresti reaalseid meetmeid võtma, et vältida lisakahjustusi. Swedbank, SEB, LHV ja Luminor kõik pakuvad selleks oma turvalisuse leheküljed, näiteks Swedbanki oma on siin: https://www.swedbank.ee/private/d2d/ebanking/secureBanking 0 1 +user: Tere! Ma kuulsin midagi CE-märgise kohta. Mis see tähendab ja kes sellele tootele kinnitada tohib? +assistant: Tere! CE-märgis näitab, et toode vastab EL-i ühtlustatud nõuetele. Selle kinnitada tohib ainult tootja või tema volitatud esindaja. See on nagu kinnitus, et toode on ohutu ja vastab nõuetele. 0 0 +user: Tere! Kas ma saan ID-kaardiga reisida Euroopa Liitu? Kas mul peab olema pass? +assistant: Tere! Jah, ID-kaardiga saad reisida Euroopa Liitu ja Euroopa Majanduspiirkonna liikmesriikidesse ilma passi kohta. Pass on aga alati vajalik dokument, sest ID-kaart on ainult reisidokumendina lubatud. Kas sa mõtled täpseid riikseid reegleid? 1 2 +user: Tere! Kas ma saan ID-kaardi kasutamiseks oma arvutis ID-tarkvara paigaldata? +assistant: Tere! Jah, seda saab. ID-tarkvara on võimalik paigaldada Windowsi, macOS ja Ubuntu operatsioonisüsteemidel. Kuid on oluline teada, et see ei toeta vanemaid operatsioonisüsteeme. Mis operatsioonisüsteemi sa kasutad? 2 2 +user: Tere! Ma kuulsin, et Tarbijakaitse Amet on teinud meeleseisundi raudteeohutuse kohta. Mis see tähendab? +assistant: Tere! Jah, meil ongi meeleseisund, mis tuletab meile kõik olulised punktid raudtee ohutuse kohta. Põhimõtteliselt tuletab see meid kõiki hoiatama, et raudteed on ohtlikud piirkonnad, kui eiratakse liikluseeskirju. 0 0 +user: Tere! Mul on küsimus TTJA otsuste kohta. Mis saab, kui ma ei nõustu TTJA otsusega? +assistant: Tere! Kui sa ei nõustu TTJA otsusega, on sul õigus selle vaidlustada. Sul on kaks võimalust: esitada vaie TTJA peadirektorile või esitada kaebus Tallinna Halduskohtule. 0 0 +user: Tere! Ma uurisin politseijaoskondi ja nägin Lääne-Harju Kesklinna politseijaoskonna nime. Kus see asub? +assistant: Tere! Lääne-Harju Kesklinna politseijaoskond asub Kolde puiestee 65, 10321 Tallinn. 1 1 +user: Tere! Mul on küsimus. Ma pean taotlema passi lapsele, kes on välismaalane. Kuidas ma riigilõivu maksin? +assistant: Tere! Riigilõivu tasumiseks pead kasutama Rahandusministeeriumi kontot. Saaja on Rahandusministeerium, viitenumber on 2900082443. Pangaülekandeks on saadaval erinevad kontod, näiteks SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor (EE701700017001577198), LHV Pank (EE777700771003813400). 1 1 +user: Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) jälgib gaasiseadmete ohutust. Kuidas see täpselt toimib? +assistant: Tere! TTJA peab silmas, et gaasiseadmed müügil ei tohiks ohustada inimeste tervist ja turvalisust. See tähendab, et nad peavad olema valmistatud ja paigaldatud nõuetekohaselt, regulaarselt hooldatud ning kasutama ettenähtud otstarbel. TTJA võib peatada ohtliku gaasiseadme levitamise ja kasutamise, kui tekib kahtlus ning on õigus nõuda selle turult kõrvaldamist, kui ekspertiis näitab, et seade ei vasta nõuetele. 0 0 +user: Tere! Ma tahaksin teada, kuidas Narva arestimajas külastajatele lubatud on asju toimetada. Kas saab sinna pakke saata? +assistant: Tere! Narva arestimajasse pakke ei saa toimetada. Ainult vahistatul on lubatud isiklikult toimetada asju. 1 1 +user: Tere! Ma tahaksin teada, mida ma peaksin silmas pidama, kui ma võtan laenu. Mis on vastutustundliku laenamise põhimõtted? +assistant: Tere! Vastutustundliku laenamise põhimõtted tähendavad, et nii laenuandjad kui ka tarbijad peavad olema ettevaatlikud. Enne laenu, järelmaksu või liisingu võtmist, võta pakkumisi paarilt-kolmelt eri firmalt ja küsi tingimuste võrdlemiseks koos pakkumisega teabelehte. See leht sisaldab kõiki olulisi laenutingimusi. 0 0 +user: Tere! Ma arvan, et ma peaksin taotlema alaline elamisõigus, aga ma ei ole päris kindel, kuidas see toimib. Kas Politsei- ja Piirivalveamet seda menetleb? +assistant: Tere! Jah, Politsei- ja Piirivalveamet kindlasti menetleb selle. Nad kontrollivad, et sa oled esitanud kõik vajalikud dokumendid ja et sa täidate elamisõiguse saamiseks nõuetekohased tingimused. Kui mingis punktis puudub midagi, sa saad teada. Kas sul on täna mõni konkreetne dokument, mille kohta sa arvad, et puudub midagi? 1 1 +user: Tere! Ma olen välismaal ja tahaksin teada, kuidas ma saan elamisloakaarti taotleda. On see võimalik? +assistant: Tere! Jah, on võimalik. Välismaal saab elamisloakaarti taotleda erandkorras, kui oled välismaalane, kes ei ole EL liikmesriigi kodanik, sul on kehtiv Eesti elamisluba või elamisõigus ning sinu välismaa elukoht on Eesti rahvastikuregistris registreeritud. 1 1 +user: Tere! Ma pole päris kindel, mis need elektroonilised allkirjad on ja miks need nii olulised on Euroopas. Kas sa võiksid seda lihtsamalt seletada? +assistant: Tere! Muidugi. Elektroonilised allkirjad on nagu tavaline allkiri, aga see on tehtud arvutis. Need on olulised, sest need aitavad meil e-tehinguteselt kindlust saada, et tegelikult on see just sina, kes selle allkirjastanud oled. Euroopas on selleks vajalikud reeglid, et nad oleksid kõik maailmas ühesugused. 0 2 +user: Tere! Ma olen huvitatud Eesti kodakondsuse taotlemisest. Kui ma esitan taotluse, mis siis juhtub järgmisena? +assistant: Tere! Pärast taotluse esitamist edastatakse dokumendid Vabariigi Valitsusele. Nad langetavad otsuse sinu kodakondsuse andmise kohta. 1 1 +user: Tere! Olen kuulnud seiklusparkidest, aga ei tea väga palju. Mis on seiklusparki mineku reeglid, mida peaksin teadma? +assistant: Tere! Seikluspark on tore koht aktiivseks puhkuseks. Enne rajale minekut on oluline tutvuda reeglite ja piirangutega. Näiteks, on oluline hinnata oma võimeid ja tervislikku seisundit, eriti kui kavatsete läbida raskemaid radu. Kas Sa oled huvitatud, et ma rääkiks täpsemalt raskusastmete kohta? 1 0 +user: Tere! Ma olen kuulnud, et Tarbijakaitseamet aitab probleemide puhul. Mis on esimene asi, mida ma peaksin tegema, kui mul on probleem mingi ostuga? +assistant: Tere! Absoluutselt, Tarbijakaitseamet on abiks. Kõigepealt peaksite kirjutama kaebusena kirja kauplejale. Kui kaebus ei lahenda probleemi, saate pöörduda tarbijavaidluste komisjoni poole. 0 0 +user: Tere! Ma kaotasin oma passi ja ma ei tea, kust ma saan uue passi tellida. Kas Põlva politseiosakond saab mulle aidata? +assistant: Tere! Jah, Põlva politseiosakond saab teid aidata. Parim viis passi taotlemiseks on PPA iseteenindus veebilehel: https://www.politsei.ee/et/iseteenindus. Võite ka tulla kohale. Põlva teenindus asub Võru 12, 63308 Põlva. 1 1 +user: Tere! Kas ID-kaart on mõni uus dokument Eestis või on see vana? +assistant: Tere! ID-kaart on Eesti Vabariigis esmane isikut tõendav dokument. See on suhteliselt uus, sest selle kasutamine on seadusest tuleneva kohustus. 1 2 +user: Tere! Ma tahan teada, mis on Eesti kodaniku passi tellimisel riigilõivu suurus? +assistant: Tere! Eesti kodaniku passi riigilõivu suurust saad teada Politsei- ja Piirivalveameti veebilehel. Link on https://www.politsei.ee/et/telli-dokument-selverisse. Täpset summat saad seal vaadata. 1 1 +user: Tere! Ma lugesin sellest artikkel kohta tarbijavaidluste komisjoni. Mis on selle komisjoni peamine ülesanne? +assistant: Tere! Tarbijavaidluste komisjoni peamine ülesanne on lahendada vaidlusi tarbija ja ettevõtte vahel, mis puudutavad tarbekaupade või -teenuste ostu-müüki. Nad aitavad leida lahendusi, kui tarbija on rahul teenusega või toodetega. 0 0 +user: Tere! Ma olen kuulnud Ida-Harju politseijaoskonnast. Kus see asub täpselt? +assistant: Tere! Ida-Harju politseijaoskond asub aadressil P. Pinna 4, 13615 Tallinn. 1 1 +user: Tere! Olen kuulnud, et on võimalik laenu maksepuhkust taotleda. Kuidas see üldiselt toimib? +assistant: Tere! Jah, on õige. Maksepuhkus võimaldab teil laenu tagasimaksmist lühikeseks ajaks peatada, kui teil on raskusi. Üldjuhul antakse see kuni kuueks kuuks, ja sel ajal ei maksa laenu põhiselt, vaid intressi tuleb ikkagi edasi tasuda. Oluline on märkida, et seda tuleks taotleda alles siis, kui sissetuleku vähenemine on probleem ja teised kulud kokku tõmmata ei aita. 0 0 +user: Tere! Ma kuulasin rääkimist eIDAS-ist. Kas te võtaksite need mõned e-tehingud käsitlevaid küsimusi vastu? +assistant: Tere! Muidugi, võtan need küsimused vastu. eIDAS ongi Euroopa Liidu määrus, mis on loodud selleks, et lihtsustada piiriülest e-teenuste kasutamist. Kas sa tead, milles see lihtsustamine just seisab? 0 2 +user: Tere! Ma olen välisriigi kodanik ja plaanin Eestis äri alustada. Kas mulle on vaja mingit erist elamisluba? +assistant: Tere! Jah, kui sa ei ole Euroopa Liidu riigi, Islandi, Liechtensteini, Norra või Šveitsi kodanik, siis Eestis ettevõtlusega tegelneda on vaja elamisluba ettevõtluseks. See on Politsei- ja Piirivalveameti väljastatav dokument. 1 1 +user: Tere! Kas tean, et mobiil-ID on olemas, aga pole päris kindel, mida see tähendab ja kas see on minu jaoks hea? +assistant: Tere! On tore, et huvi on! Mobiil-ID on tegelikult väga hea lahendus, kui vajate digitaalset isikutunnistust. See on tänapäeval mugavam ja turvameelt hoogu andev kui traditsiooniline ID-kaart. Sellega saate viibida eesti.ee-s, internetipankas, või osaleda e-valimistel. Kas teil on küsimusi selle kohta? 2 2 +user: Tere! Ma olen täiskasvanu ja tahan elamisloakaardi taotlemisega alustada. Kuidas ma riigilõivu maksta saan? +assistant: Tere! Elamisloakaardi taotlemise riigilõivu maksmiseks on mitu võimalust. Saaja on Rahandusministeerium, ja viitenumber on 2900082443. Pangakontod on erinevad: SEB EE891010220034796011, Swedbank EE932200221023778606, Luminor EE701700017001577198 ja LHV Pank EE777700771003813400. 1 1 +user: Tere! Kas te saate öelda, kus ma saan RIA DigiDoc mobiilirakenduse uue versiooni alla laadida? +assistant: Muidugi! RIA DigiDoc mobiilirakendus on saadaval nii App Store'ist (https://itunes.apple.com/ee/app/ria-digidoc/id1216104448?mt=8) kui ka Google Play poodist (https://play.google.com/store/apps/details?id=ee.ria.DigiDoc). 2 2 +user: Tere! Ma olen välisriigi kodanik ja plaanin Eestis äri alustada. Kas ma kindlasti vajan elamisluba selleks? +assistant: Tere! Vaelisriigi kodanikuna jah, enamikul juhtudel vajad. Kui sa ei ole Euroopa Liidu riikide, Islandi, Liechtensteini, Norra või Šveitsi kodanik, siis ettevõtluseks Eestis on vaja elamisluba. 1 1 +user: Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb numeratsiooniga. Mis see täpsemalt tähendab? +assistant: Tere! TTJA väljastab numbrilube sideteenuse osutajatele, ehk sideettevõtjatele, ning ka neile, kes pakuvad muud teenust side teenuse abil, näiteks sisuteenuseid. See tähendab, et nad määravad, kes tohib kasutada erinevaid numbrimudeleid, näiteks telefoninumbreid või mobiilnumbreid. 0 0 +user: Tere! Mul on küsimus TTJA otsuste kohta. Mis teha, kui ma ei nõustu TTJA tehtud otsusega? +assistant: Tere! Kui te ei nõustu TTJA tehtud otsusega, on teil õigus selle vaidlustada. Teil on kaks võimalust: esitada vaie TTJA peadirektorile või esitada kaebus Tallinna Halduskohtule. 0 0 +user: Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on lapsevanematele mingit ohutusraamatut välja andnud. Kas see on tõsi? +assistant: Jah, see on tõsi! Politsei- ja Piirivalveamet ongi valmistanud ohutusraamatu just 1. klassi minevate laste vanematele. See on mõeldud eelkõige lapsevanemate toetamiseks lapse turvalisuse tagamisel. 1 1 +user: Tere! Mul on küsimusi rahvusvahelise kaitse taotlemise kohta, eriti seoses olukorraga Ukrainas. Kuidas ma saan seda taotleda? +assistant: Tere! Rahvusvahelise kaitse taotlemiseks võite pöörduda nelja kohta: Tallinnas Pärnu mnt 139, Pärnus H. Tammsaare pst 61, Jõhvis Rahu tn 38 või Tartus Riia tee 132. Samuti on saadaval ankeedid Politsei- ja Piirivalveameti veebilehel. Aluseks on põhjendus, et te kardate tagakiusamist Ukrainas rassi, rahvuse, usu, ühiskondliku rühmitusse kuulumise või poliitiliste vaadete tõttu või et tagasipöördumine Ukrainasse kujutab endast tõsist ohtu. 1 1 +user: Tere! Ma arvan, et ma saan ID-kaardiga ilma passita Euroopasse reisida? Kas see on võimalik? +assistant: Tere! Jah, ID-kaardiga saate reisida Euroopa Liitu (EL) ja Euroopa Majanduspiirkonda (EMP) kuuluvad riigid ilma passi kohta. See on võimalik, aga oluline on meeles pidada, et ID-kaart on vajalik reisidokumendina ja EI sobi paljude teiste dokumentidega, nagu näiteks elamisloa kaart või digi-ID. 1 2 +user: Tere! Ma saan probleeme ID-kaardi kasutamisega. Kas sulle teada on, kuidas kindlustada, et ID-kaart ja lugeja töötavad korralikult? +assistant: Tere! Ma saan aru, et sul on probleeme. Alguses on oluline, et sa veedad hetkel vääris aega ja saad lugema selle artikli: https://www.id.ee/artikkel/paigalda-id-tarkvara. Seega veedad hetkel vääris aega ja saad lugema selle artikli: https://www.id.ee/artikkel/paigalda-id-tarkvara. Siis on oluline, et sinul oleks arvutisse ID-tarkvara paigaldatud, ja vajadusel on ID-kaardi kasutamiseks seadistatud veebibrauserid. 2 2 +user: Tere! Ma saan probleeme mobiil-IDga allkirjastamisel. Mis võiks olla põhjus? +assistant: Tere! Võib olla mitmeid asja. Sageli on probleem ajutine ja laheneb ise. Palju olgem vaadates veateadet – see sisalda alati vajaliku info probleemi lahendamiseks. Esmalt luge veateade täies ulatuses. Kas sa tead, mis veateadet sa nägid? 1 2 +user: Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on saanud raha piiriehituseks. Kas see on tõsi? +assistant: Jah, see on tõsi. Politsei- ja Piirivalveamet on saanud toetust piiriehituse rahastamiseks Euroopa Liidu Sisejulgeolekufondi välispiiride ja viisade rahastamisvahendist perioodil 2014-2020. 1 1 +user: Tere! Ma tahan ID-tarkvara Ubuntu peale paigaldada. Kuidas ma kõige paremini alustan? +assistant: Tere! Väga hea küsimus. Ubuntu jaoks on kõige mugavam paigaldada ID-tarkvara automaatskripti abil. Alustame sellega: esmalt pead salvestama skripti oma arvuti tahvlkale. Siis avad Terminali ja navigeerid kataloogi, kuhu sa skripti salvestasid. Näiteks, kui sa seda 'Downloads' kataloogi, siis sisestad käskluse `cd /home/kasutajanimi/Downloads` (kasutajanimi asendades sinu kasutajanimega). Skripti käivitamiseks siis `sh install-open-eid.sh` ning parool sisestamisel lihtsalt vajuta edasi. 2 2 +user: Tere! Kas ma saan Eesti kodakondsuse taotleda, kui ma olen eestkostetav? +assistant: Tere! Jah, võimalik on. Aga on mõned tingimused, mida pead täistõttu täitma. Peamine asi on see, et pead olema vähemalt 15-aastane. Kas te seda teate? 1 1 +user: Tere! Ma tahan lapsele ID-kaarti taotleda. Kuidas ma riigilõivu maksta saan? +assistant: Tere! ID-kaardi taotlemisel lapsele riigilõivu tasumisel on mitmeid võimalusi. Kui taotled iseteeninduses, saad riigilõivu tasuda otse sealsamas. Kui aga teed pangaülekande, siis saaja on Rahandusministeerium, viitenumber on 2900082443. Kontopõhjad on erinevate pankade jaoks erinevad: SEB EE891010220034796011, Swedbank EE932200221023778606, Luminor Bank EE701700017001577198, LHV Pank EE777700771003813400. 1 1 +user: Tere! Ma tean, et on mobiil-ID, aga kas see on nagu tavaline ID-kaart, aga telefoni sees? +assistant: Tere! Jah, täpselt nii! Mobiil-ID on SIM-kaardi põhjal toimiv digitaalne isikutunnistus, mis on samasugune kui tavaline ID-kaart (digi-ID, elamisloakaart) – see on riiklikult garanteeritud. See on lihtsalt kasutuses mobiilseadmega. Kas sulle on oluline teada, kuidas seda kasutada? Kas sa mõtled, kuidas taotleda või kuidas sellega tegelikult midagi teha? 2 2 +user: Tere! Olen kuulnud, et mõned tooted on ohtlikud. Kuidas ma saan teada, millised tooted on just praegu probleemiks? +assistant: Tere! EL-is on selleks kiirhoiatussüsteem Safety Gate. Saa tutvuda nädala ülevaatega nende kodulehel https://ec.europa.eu/consumers/consumers_safety/safety_products/rapex/alerts/?event=main.listNotifications&lng=en või kasuta otsingut, kui sul on huvi mingi kindla toote suhtes. 0 0 +user: Tere! Ma just sain teada, et meil on uus beebi. Mida me peame nüüd kodakondsuse osas tegema? +assistant: Õnne! Kui teie laps saab Eesti kodakondsuse, siis teile saadetakse posti teel kodakondsustunnistus. See on tähtis dokument! Meil on Politsei- ja Piirivalveamet vastutab selle väljastamise. 1 1 +user: Tere! Mul on küsimus WiFi-seadmete kasutamise kohta. Mis on need piirangud, kui ma näiteks tahaksin kodus selliseid seadmeid kasutada? +assistant: Tere! WiFi-seadmete kasutamisel on oluline teada, et erinevad sagedusribad on reguleeritud erinevate piirangutega. Näiteks 2,4 GHz sagedusel on lubatud võimsus kuni 100 mW ja selle kasutamine ei ole piiratud. Täpsem info on leitav määruse „Raadiosageduste kasutamise tingimused ja tehnilised nõuded sagedusloast vabastatud raadioseadmetele“ lisast 6. 0 0 +user: Tere! Ma olen kuulnud, et lennusõitjatel on teatud õigused. Mis need on? +assistant: Tere! Jah, lennusõitjatel on mitmeid õigusi. Näiteks, kui lennud hiljenevad või tühistatakse, on teil õigus teavitus saada, hüvitist nõuda ja abi saada tagasipöörduse korral. Lisaks on teil õigus saada abi lennujaamas kui teil on erivajadused. 0 0 +user: Tere! Ma ei ole päris kindel, mis on eIDAS. Kas saate mulle sellest natuke selgeks teha? See on midagi, mis seotakse ID.ee? +assistant: Tere! Jah, eIDAS on Euroopa Liidu määrus, mis seotakse ID.ee-ga. Lihtsasti öeldud, see on seadus, mis näeb ette, kuidas digiteenuseid ja e-tehinguid turvaliselt identifitseerida ja kinnitada. See aitab inimestel digimaailmas ohksele saada. See määrus jõudis jõule 23. juulil 2014. aastal. 0 2 +user: Tere! Ma olen kuulnud, et relvaloa kehtivust on võimalik pikendada. Mis siis täpsemalt on sellega seotud, kui ma tahan seda enda puhul teha? +assistant: Tere! Relvaloa kehtivuse pikendamisel on oluline, miks sa relvaloa üldse omad. Kui relva otstarbeks on märgitud jahipidamine või sport, siis täiendavat laskekatset ei pea tegema. Kui aga on märgitud ainult enese ja vara kaitseks, siis pead selleks täiendava laskekatse sooritama. 0 1 +user: Tere! Ma arvan, et mul on ID-kaart ja ma tahan sellega arvutis midagi tegema. Mis ma pean tegema, et see tööle hakkaks? +assistant: Tere! Üsna hea küsimus! Et saaksid ID-kaardi arvutis kasutama, pead paigaldama ID-tarkvara. See tarkvara on sinu arvuti jaoks vajalik, et ID-kaardiga elektroonilisi toiminguid teha. Kas sa kasutad Windowsi, macOS või Linuxi operatsioonisüsteemi? 2 2 +user: Tere! Ma olen kuulnud, et Politsei- ja Piirivalveamet kasutab minu isikuandmeid, kui ma saan teiega ühendust. Kas see on nii ja millal? +assistant: Tere! Jah, see on õige. Me kasutame isikuandmeid pöördumiste lahendamiseks ja teile vastamiseks. Kui peame pöördumist lahendades küsimusi esitama teistele asutustele, edastame neile ainult need andmed, mis on vastuse koostamiseks tõesti vajalikud. See aitab meil privaatsust kaitsta. 1 1 +user: Tere! Kas teeksite mulle infot ID-tarkvara uusima versiooni kohta? +assistant: Tere! Jah, küllap. ID-tarkvara uusimad versioonid on ID.ee lehel saadaval. Meie süte automaatselt teie arvuti operatsioonisüsteemi ja pakub sobiva paketi. On lihtne, peate vaid tarkvara alla lahtima ja paigaldama. 2 2 +user: Tere! Ma lugesin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb ka kauplejate komisjoni töoga. Kuidas see üldiselt toimib, kui mul on mingi probleem kauplusega? +assistant: Tere! Üldjuhul, kui sul on probleem, esitad avalduse TTJAs. Komisjon annab kauplejale aega oma seisukohti selgitada ja vastata sinu väidetele. See annab neile võimaluse probleemi lahendada enne, kui asi jõuab komisjoni istungini. 0 0 +user: Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) on tellinud mitmeid uuringuid. Kas sa saaksid rääkida neist? +assistant: Tere! Jah, TTJA on tellinud mitmeid uuringuid. Üks olulisi on uuring passiivse lairibataristu rajamise kohta maapiirkondadesse, mille tellis Majandus- ja Kommunikatsiooniministeerium. Lisaks on tehtud uuring Eesti elanike tarbijate õiguste teadlikkusest ja e-kaubanduse turust. 0 0 +user: Tere! Ma arvan, et tean, et ID.ee on olemas, aga ma ei ole päris kindel, kuidas digiallkirjastamist oma veebisüsteemi juurde lisada. Kas teil on näiteks lihtne viis, kuidas seda saaksin teha? +assistant: Tere! Päev! Jah, digiallkirjastamist saab lisada oma veebisüsteemi mitmel viisil. Alustuseks on Riigi allkirjastamisteenus (SiGa) valitsusektori jaoks väga hea võimalus. See on aga tarkvara, mida peab enda serverile paigaldama. Kas teete veebirakendusi väljapoole valitsust? 1 2 +user: Tere! Ma olen ajakirjanik ja tahaksin teada, kuidas Politsei- ja Piirivalveametiga pressilisti liikmeks saada. Mis on selleks vaja? +assistant: Tere! Liitumine on lihtne. Saate saata e-kirja aadressil, mis on kirjeldatud ametisõnumites. Kirjutage kirja enda nimi, väljaande nimi ja e-posti aadress. 1 1 +user: Tere! Mul on mõned küsimused selle kohta, mida ma saan teha, kui ma olen veebist midagi ostnud ja ma ei ole sellega rahul. Mis on minu õigused? +assistant: Tere! Kui oled midagi veebist ostnud, on sul õigus lepingust taganeda 14 päeva jooksul alates sellest, kui kaubad sinuni jõudsid. See on nn 14-päevane taganemisõigus. See kehtib ka teenuste puhul, kus taganemisõiguse tähtaeg algab lepingu sõlmimisest. 0 0 +user: Tere! Ma olen Tarbijakaitse ja Tehnilise Järelevalve Ameti uus töötaja. Üks minu ülesandete kohta on raadiosageduste kasutamine. Kust ma peaksin alustama, et aru saada, kuidas seda Eestis reguleeritakse? +assistant: Tere! Suurepärane! Alustada saaks Eesti raadiosagedusplaanist. See on meil avaldatud ja saadaval Riigiteataja veebilehel: https://www.riigiteataja.ee/akt/125012019006. See dokument annab kõige olulisema ülevaate sellest, kuidas Eestis raadiosagedusi kasutatakse ja mis on sellega seotud reeglid. 0 0 +user: Tere! Ma sain uue ID-kaardi ja koodiümbriku. Kas ma pean kohe PIN- ja PUK-koodid vahetama? +assistant: Tere! Jah, väga hea, et sa sai uue ID-kaardi ja koodiümbriku! Soovitav on PIN- ja PUK-koodid kohe vahetada. See tagab parema turvalisuse. Koodiümbrikus on uusad koodid, ja on hea, kui sa neid kohe uusima kaardi külge kinnitatakse. 2 2 +user: Tere! Ma kuulsin, et Rail Balticu ehitamine on keeruline projekt. Mis kõige suuremad väljakutsed sellega seoses on olnud? +assistant: Tere! Jah, Rail Balticu ehitamine on tegelikult üsna keeruline. Üks suurim väljakutse on see, et olemasoleva raudtee ümberehitus, näiteks kurvide sirgemaks tegemine või teise rööpapaari lisamine, on tehtud analüüside järgi kallim kui uue trassi ehitamine. Lisaks on oluline arvestada, et Tartu kaudu minev trass ei võimalda kasutada Euroopa ühendamise rahastu toetust samas mahus. 0 0 +user: Tere! Ma ei ole päris kindel, mis need sertifikaadid on ja miks on need nii olulised. Kas saaksid seda veidi selgitada? +assistant: Muidugi! Ühest küljest on sertifikaadid nagu digitaalne isikutunnusjoon, mis seob sind sinu digitaalse isikutuvastusvahendiga, näiteks ID-kaardiga või mobiil-ID-ga. See tähendab, et kui sa kasutad e-teenust, kasutatakse seda sertifikaati, et kinnitada, et sa oled see, keda sa väidate olevat. On nagu digitaalne kiri, millega sa kinnitad oma identiteedi. Kuidas sa need tavaliselt kasutad? 2 2 +user: Tere! Ma saan aru, et RIA DigiDoc on mobiilirakendus. Kas see on täielikult ligipääsetav kõigile kasutajatele, kes seda kasutavad? +assistant: Tere! Jah, RIA DigiDoc on ehitatud vastavalt WCAG 2.1 AA suunetele, mis tähendab, et seda on mõeldud kasutamiseks ka inimestele, kellel on nägemisvõimete puudega või muud liiklusvõimete probleemid. Küll aga on teatud piirkonnad, kus ligipääsetavus pole täielik. Meie teates on detailsemalt kirjas, milles need puudumised on. 0 2 +user: Tere! Ma plaanin elamisloakaardi taotlemist. Kuidas ma riigilõivu maksta saan? +assistant: Tere! Elamisloakaardi taotlemisel riigilõivu tasumiseks on mitmeid võimalusi. Saaja on Rahandusministeerium. Viitenumber on 2900082443. Pangad, kuhu saad maksta, on SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor Bank (EE701700017001577198) ja LHV Pank (EE777700771003813400). 1 1 +user: Tere! Ma olen ostmas kuldehit, aga ma ei tea, millele ma peaksin tähelepanu pöörama. Mida peaksin kontrollima? +assistant: Tere! Kuldehit ostes on oluline, et sellel oleksid proovi- ja nimemärgid. Proovimärgis näitab, millist metalli on kasutatud ja selle osa sulamis, näiteks 375, 585 või 916. See märk on tuhandeksüsteemis. Nimemärgis on tootja või maaletoja nimi. See peab olema registreeritud riiklikus registris. Kas sa teadsid seda? 0 0 +user: Tere! Ma ostsin hiljuti televiisori, aga see ei tööta korralikult. Kuidas ma peaksin selle kohta kaebust esitama? +assistant: Tere! Esiteks, otsige üles ostutšekk või mingi muu dokument, mis tõendab ostu. See on oluline. Seejärel pöörduge viivitamatult selle poodi, kust televiisori ostsite. Kaebuse esitamine peaks toimuma hiljemalt kahe kuu jooksul alates vea avastamisest. 0 0 +user: Tere! Ma plaanin elamisloakaarti taotleda. Kus ma teenindusse ma peaksin minema ja mida ma kaasa võtma peaksin? +assistant: Tere! Elamisloakaardi taotlemiseks saad valida sobiva teeninduse kohta Politsei- ja Piirivalveameti veebilehelt. Kindlasti kaasa isikut tõendav dokument. Sa saad aja broneerida veebilehel https://broneering.politsei.ee/ kui soovid. 1 1 +user: Tere! Ma mõtlesin, mis see Mobiil-ID on? Kas see on uus ID-kaart? +assistant: Tere! Mobiil-ID on juurdekasv ID-kaardi kasutamisele. See on mobiiltelefoniga kaasasolev vebinimiliselt kindel identifikaator. See annab teile juurdejuurdepääsu e-teenustele, ilma et peaksite ID-kaarti kaasas kandma. Kas te mõtlete, et see on samasugune kui ID-kaart? See on juurdekasv! 2 2 +user: Tere! Ma olen veidi segadus ID-kaardi taotlemisel. Kuidas liikumisvõimetule inimesele ID-kaart taotleda saab? Mis on kõige lihtsam viis? +assistant: Tere! Liikumisvõimetule inimesele on mitmeid võimalusi ID-kaardi taotlemiseks. Iseteeninduses saab taotleda, kui te olete täiskasvanu või alaealine laps, ja teil on ID-kaart, mobiil-ID või Smart-ID. Seal peate üles laadima foto ja maksma riigilõivu pangalingi kaudu. Kas teil on iseteenindusega kogemust? 1 1 +user: Tere! Ma tean, et võin taotleda alaline elamisoigust EL-ist pärit inimesena. Aga kuidas see poliisiasutus selle menetletakse? +assistant: Tere! Jah, seda saab teha. Politsei- ja Piirivalveamet kontrollib taotluse. Nad peaksid veenduma, et teil on kõik nõuetekohased dokumendid, ja et teil on täidetud kõik elamisõiguse saamiseks vajalikud tingimused. Küll aga ei pruugiks teil midagi lisaks kuuldagi, kui kõik on korras. Kas te soovite rohkem teada? 1 1 +user: Tere! Mul on probleem Windowsiga ID tarkvara paigaldamisel. Ilmnes mingi tõrk. Kas te saate mind aidata? +assistant: Tere! Ma saan küll. Palun räägi rohkem sellest tõrkest. Kas sa saad mingi veaõht (koodiga) või veateate? Ja millise ID tarkvara sa paigaldate? 2 2 +user: Tere! Ma tahan küsida Kuressaare arestimaja kohta. Kas sinna saab pakke tuua? +assistant: Tere! Kuressaare arestimajas ei saa arestalustele pakke tuua. Vahistatule aga lubatakse üks pakk kuus alates 01.08.2020. 1 1 +user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TJAA) tegelda koduohutusega. Mis see täpsemalt tähendab? +assistant: Tere! Nõmme, TJAA jälgib, et ehitis oleks ohutu kogu oma eluea jooksul. Põhimõte on, et omanik vastutab ehitise ohutuse eest. Korterelamus on oluline vaheline erinevus reaalosade ja kaasomandi vahel. Mis on sinu jaoks kõige olulisem selles? 0 0 +user: Tere! Ma olen kuulnud, et Tarbijakaitseamet kaitseb tarbija õigusi. Aga mida see täpsemalt tähendab? Millised on mu peamised õigused ostjana? +assistant: Tere! Jah, Tarbijakaitseamet on seal, et kaitsta sind tarbijana. Peamiselt sa saad nõuda kaupa või teenust, mis vastab nõuetele, on ohutu ja mida ei ole keelatud omada või kasutada. Samuti on sul õigus saada vajalikku teavet pakutavate kaupade ja teenuste kohta, et sa saaksid teadlikult valida. Ja kui su õigusi rikutakse, saad nõu ja abi Tarbijakaitseametilt! 0 0 +user: Tere! Ma arvan, et politsei on mõnikord avalikule koosoledule sekkuda. Miks nad seda teevad, ja millistel juhtudel? +assistant: Tere! Jah, see on tõsi. Politsei saab avalisele koosoledule sekkuda, kui arvab, et on olukord, kus koosoleku pidamine ohustab inimelu või seaduslikkust. Näiteks, kui on alust arvata, et kohas, kus koosolek toimub, on olukord, kus saab tõesti kiiresti süütu tekitada. Mida täpsemalt sa küsid? 1 1 +user: Tere! Ma nägin seda MyID portaali. Mis see üldse on? Kas see on midagi uut? +assistant: Tere! MyID portaal on uus platvorm, mida pakub SK ID Solutions AS (SK), mobiil-ID, ID-kaardi, Digi-ID ja Smart-ID kontode kohta. See on koht, kus saad teavet nende kontode kohta ja sellest, mida neis tehtud on. Kas sa tahad teada rohkem, kuidas seda kasutada või mis teeb selle eriliseks? 2 2 +user: Tere! Mul on küsimusi rõivaste ja jalatste märgistamise kohta. Mis on kõige olulisem, mida ma peaks ostes vaatama? +assistant: Tere! Oluline on vaadata, kas rõival või jalatseid on näidatud toote nimetus, suurus, materjalikoostis (eriti oluline, kui ei ole nahast või karusnahast) ja hooldusjuhised ehk hooldustähistus. Need on põhilised asjad, mis annavad sulle olulist teavet toote kohta. 0 0 diff --git a/experiments/base_model_training/requirements.txt b/experiments/base_model_training/requirements.txt new file mode 100644 index 00000000..dffebe44 --- /dev/null +++ b/experiments/base_model_training/requirements.txt @@ -0,0 +1,36 @@ +# Core dependencies +numpy>=1.20.0 +pandas>=1.3.0 +scikit-learn>=1.0.0 +matplotlib>=3.4.0 +seaborn>=0.11.0 + +# PyTorch and related +torch>=1.10.0 +torchvision>=0.11.0 + +# Transformer models +transformers>=4.18.0 +datasets>=2.0.0 +tokenizers>=0.12.0 + +# MLflow for experiment tracking +mlflow>=1.25.0 + +# Metrics and evaluation +scipy>=1.7.0 + + +# Utilities +tqdm>=4.62.0 +joblib>=1.1.0 + +# Visualization +plotly>=5.6.0 + +# Optional performance monitoring +psutil>=5.9.0 +py-cpuinfo>=8.0.0 + +# For hyperparameter optimization (optional) +optuna>=2.10.0 diff --git a/experiments/base_model_training/scripts/constants.py b/experiments/base_model_training/scripts/constants.py new file mode 100644 index 00000000..d1ed9acd --- /dev/null +++ b/experiments/base_model_training/scripts/constants.py @@ -0,0 +1,15 @@ +# Constants +MODEL_CONFIG = { + "bert": { + "name": "bert-base-uncased", + "max_length": 512, + }, + "roberta": { + "name": "roberta-base", + "max_length": 512, + }, + "xlm": { + "name": "xlm-roberta-base", + "max_length": 512, + }, +} diff --git a/experiments/base_model_training/scripts/create_datasets.py b/experiments/base_model_training/scripts/create_datasets.py new file mode 100644 index 00000000..0c6ab68d --- /dev/null +++ b/experiments/base_model_training/scripts/create_datasets.py @@ -0,0 +1,333 @@ +import os +import sys +import pandas as pd +import numpy as np +from sklearn.model_selection import train_test_split +import json +import argparse +from loguru import logger +import matplotlib.pyplot as plt +import seaborn as sns + +logger.remove() +# add stout handler +logger.add(sys.stdout, format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}") + + +def load_raw_data(input_file): + """ + Load and process the raw conversation data. + + Args: + input_file: Path to the raw data file + + Returns: + DataFrame with processed conversation data + """ + logger.info(f"Loading data from {input_file}") + + if input_file.endswith(".csv"): + df = pd.read_csv(input_file) + else: + raise ValueError(f"Unsupported file format: {input_file}") + + # Check required columns + required_cols = ["turn", "speaker", "text", "agency"] + for col in required_cols: + if col not in df.columns: + raise ValueError(f"Required column '{col}' not found in data") + + return df + + +def process_conversations(df): + """ + Process the raw data into conversation-level samples for classification. + Each conversation (all turns together) becomes one training instance. + + Args: + df: DataFrame with raw conversation data + + Returns: + DataFrame with one row per conversation + """ + logger.info("Processing conversations...") + + # Group by conversation_id + conversations = [] + for conv_id, group in df.groupby("conversation_id"): + agency = group["agency"].iloc[0] + + # Format the conversation as a string with speaker labels + conversation_text = [] + for _, row in group.iterrows(): + conversation_text.append(f"{row['speaker']}: {row['text']}") + + full_text = "\n".join(conversation_text) + + conversations.append( + { + "conversation_id": conv_id, + "text": full_text, + "agency": agency, + "num_turns": len(group), + } + ) + + conversation_df = pd.DataFrame(conversations) + + # Print statistics + logger.info( + f"Created {len(conversation_df)} conversation instances from {len(df)} turns" + ) + logger.info("Conversations per agency:") + agency_counts = conversation_df["agency"].value_counts() + for agency, count in agency_counts.items(): + logger.info(f" {agency}: {count} conversations") + + return conversation_df + + +def split_dataset(df, test_size=0.2, val_size=0.1, random_state=42): + """ + Split the dataset into training, validation, and test sets. + + Args: + df: DataFrame with processed conversation data + test_size: Proportion for the test set + val_size: Proportion for the validation set + random_state: Random seed for reproducibility + + Returns: + train_df, val_df, test_df: Split DataFrames + """ + # Check if we can use stratification (at least 2 samples per class) + class_counts = df["agency"].value_counts() + use_stratify = all(count >= 2 for count in class_counts) + + if not use_stratify: + logger.warning( + "Warning: Some classes have fewer than 2 samples. Stratification disabled." + ) + stratify = None + else: + stratify = df["agency"] + + # First split: train+val vs test + train_val_df, test_df = train_test_split( + df, test_size=test_size, random_state=random_state, stratify=stratify + ) + + # For the second split, check again if we can stratify + if use_stratify: + val_class_counts = train_val_df["agency"].value_counts() + use_val_stratify = all(count >= 2 for count in val_class_counts) + val_stratify = train_val_df["agency"] if use_val_stratify else None + + if not use_val_stratify: + logger.warning( + "Warning: After first split, some classes have too few samples for stratified validation split." + ) + else: + val_stratify = None + + # Second split: train vs val (calculated from remaining data) + val_ratio = val_size / (1 - test_size) + + train_df, val_df = train_test_split( + train_val_df, + test_size=val_ratio, + random_state=random_state, + stratify=val_stratify, + ) + + logger.info( + f"Dataset splits: Train={len(train_df)}, Val={len(val_df)}, Test={len(test_df)}" + ) + + # Print class distribution in each split + logger.info("\nClass distribution:") + for split_name, split_df in [ + ("Train", train_df), + ("Val", val_df), + ("Test", test_df), + ]: + class_dist = split_df["agency"].value_counts().to_dict() + logger.info(f" {split_name}: {class_dist}") + + return train_df, val_df, test_df + + +def analyze_dataset(df, output_dir): + """ + Analyze the dataset and save visualizations. + + Args: + df: DataFrame with processed conversation data + output_dir: Directory to save analysis files + """ + os.makedirs(output_dir, exist_ok=True) + + # 1. Class distribution + plt.figure(figsize=(10, 6)) + sns.countplot(y="agency", data=df) + plt.title("Distribution of Agencies") + plt.xlabel("Count") + plt.ylabel("Agency") + plt.tight_layout() + plt.savefig(os.path.join(output_dir, "agency_distribution.png")) + plt.close() + + # 2. Number of turns distribution + plt.figure(figsize=(10, 6)) + sns.countplot(x="num_turns", data=df) + plt.title("Distribution of Conversation Length (Turns)") + plt.xlabel("Number of Turns") + plt.ylabel("Count") + plt.tight_layout() + plt.savefig(os.path.join(output_dir, "turns_distribution.png")) + plt.close() + + # 3. Text length distribution + df["text_length"] = df["text"].apply(len) + plt.figure(figsize=(10, 6)) + sns.histplot(data=df, x="text_length", bins=20) + plt.title("Distribution of Text Length") + plt.xlabel("Number of Characters") + plt.ylabel("Count") + plt.tight_layout() + plt.savefig(os.path.join(output_dir, "text_length_distribution.png")) + plt.close() + + # 4. Summary statistics as JSON + stats = { + "num_conversations": int(len(df)), + "conversations_per_agency": { + k: int(v) for k, v in df["agency"].value_counts().to_dict().items() + }, + "avg_turns": float(df["num_turns"].mean()), + "min_turns": int(df["num_turns"].min()), + "max_turns": int(df["num_turns"].max()), + "avg_text_length": float(df["text_length"].mean()), + "min_text_length": int(df["text_length"].min()), + "max_text_length": int(df["text_length"].max()), + } + + with open(os.path.join(output_dir, "dataset_stats.json"), "w") as f: + json.dump(stats, f, indent=2) + + # 5. Create a summary markdown file + with open(os.path.join(output_dir, "dataset_summary.md"), "w") as f: + f.write("# Synthetic Dataset Summary\n\n") + + f.write("## Dataset Statistics\n\n") + f.write(f"- Total conversations: {stats['num_conversations']}\n") + f.write(f"- Average turns per conversation: {stats['avg_turns']:.2f}\n") + f.write(f"- Average text length: {stats['avg_text_length']:.2f} characters\n\n") + + f.write("## Agency Distribution\n\n") + f.write("| Agency | Count | Percentage |\n") + f.write("|--------|-------|------------|\n") + + for agency, count in stats["conversations_per_agency"].items(): + percentage = (count / stats["num_conversations"]) * 100 + f.write(f"| {agency} | {count} | {percentage:.2f}% |\n") + + +def save_datasets(train_df, val_df, test_df, output_dir): + """ + Save the datasets to CSV files. + + Args: + train_df, val_df, test_df: DataFrames for each split + output_dir: Directory to save the files + """ + os.makedirs(output_dir, exist_ok=True) + + # Ensure we only keep necessary columns + columns = ["text", "agency"] + + train_df[columns].to_csv(os.path.join(output_dir, "train.csv"), index=False) + val_df[columns].to_csv(os.path.join(output_dir, "val.csv"), index=False) + test_df[columns].to_csv(os.path.join(output_dir, "test.csv"), index=False) + + print(f"Datasets saved to {output_dir}") + + +def main(): + parser = argparse.ArgumentParser( + description="Prepare datasets for agency classification" + ) + parser.add_argument( + "--input_file", + type=str, + default="data/raw/synthetic_conversations.csv", + help="Path to the raw conversation data", + ) + parser.add_argument( + "--output_dir", + type=str, + default="data/processed", + help="Directory to save the processed datasets", + ) + parser.add_argument( + "--analysis_dir", + type=str, + default="data/analysis", + help="Directory to save dataset analysis", + ) + parser.add_argument( + "--test_size", + type=float, + default=0.2, + help="Proportion of data for the test set", + ) + parser.add_argument( + "--val_size", + type=float, + default=0.1, + help="Proportion of data for the validation set", + ) + parser.add_argument( + "--augment", + action="store_true", + help="Create augmented data for additional synthetic examples", + ) + parser.add_argument( + "--augment_factor", + type=int, + default=2, + help="How many augmented examples per original", + ) + parser.add_argument( + "--random_seed", type=int, default=42, help="Random seed for reproducibility" + ) + + args = parser.parse_args() + + # Set random seed + np.random.seed(args.random_seed) + + # Load and process data + raw_df = load_raw_data(args.input_file) + conversation_df = process_conversations(raw_df) + + # Analyze dataset + analyze_dataset(conversation_df, args.analysis_dir) + + # Split dataset + train_df, val_df, test_df = split_dataset( + conversation_df, + test_size=args.test_size, + val_size=args.val_size, + random_state=args.random_seed, + ) + + # Save datasets + save_datasets(train_df, val_df, test_df, args.output_dir) + + logger.info("Dataset preparation complete!") + + +if __name__ == "__main__": + main() diff --git a/experiments/base_model_training/scripts/dataset.ipynb b/experiments/base_model_training/scripts/dataset.ipynb new file mode 100644 index 00000000..e111f726 --- /dev/null +++ b/experiments/base_model_training/scripts/dataset.ipynb @@ -0,0 +1,674 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "68393db3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset Statistics:\n", + "- Total conversations.json files: 473\n", + "- Number of agencies: 3\n", + "- Number of unique topics: 473\n", + "- Agencies found: ['output_ID.ee', 'output_Politsei-_ja_Piirivalveamet', 'output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet']\n", + "- Topics found: ['2014-2021_-_Ajutise_reisidokumendi_naÌ\\x88idised_-_Politsei-_ja_Piirivalveamet', '2019_-_Korruptsiooniga_seotud_kohtulahendid_-_Politsei-_ja_Piirivalveamet', '2021_-_Korruptsiooniga_seotud_kohtulahendid_-_Politsei-_ja_Piirivalveamet', '2023_-_Korruptsiooniga_seotud_kohtulahendid_-_Politsei-_ja_Piirivalveamet', '5G_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ahistava_jaÌ\\x88litamise_kampaania_-_Ennetusprojektid_ja_kampaaniad_-_Politsei-_ja_Piirivalveamet', 'Ajutine_reisidokument_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'Ajutise_ja_rahvusvahelise_kaitse_taotlejate_arv_-_Politsei-_ja_Piirivalveamet', 'AmatoÌ\\x88oÌ\\x88rraadioside_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Autentimine_riiklikes_e-teenustes_-_ID.ee', 'AutosoÌ\\x83it_-_Taga_enda_ja_oma_laste_turvalisus_-_Politsei-_ja_Piirivalveamet', 'Avalduse_esitamine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Avalduse_esitamine_e-postiga_-_Politseile_avalduse_esitamine_-_Politsei-_ja_Piirivalveamet', 'Avalduse_esitamine_postiga_-_Politseile_avalduse_esitamine_-_Politsei-_ja_Piirivalveamet', 'Avalduse_esitamine_veebis_-_Politseile_avalduse_esitamine_-_Politsei-_ja_Piirivalveamet', 'Avalduse_menetlemine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Avaliku_koosoleku_korraldamise_teade_-_Avaliku_koosoleku_registreerimine_-_Politsei-_ja_Piirivalveamet', 'Avaliku_uÌ\\x88rituse_korraldamine_-_Politsei-_ja_Piirivalveamet', 'Avalikud_maÌ\\x88nguvaÌ\\x88ljakud_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Broneeringuinfo_suÌ\\x88steem_BRIIS_-_Lennureisijate_broneeringuinfo_-_Politsei-_ja_Piirivalveamet', 'CE-maÌ\\x88rgis_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Digi-ID_kasutaja_meelespea_-_Digi-ID_taotlemine_-_Politsei-_ja_Piirivalveamet', 'DigiDoc4_rakenduse_ja_RIA_DigiDoc_mobiilirakenduse_kasutamine_aÌ\\x88rilistel_eesmaÌ\\x88rkidel_-_ID.ee', 'DigiDoc4_rakenduse_teade__sertifikaatide_usaldusnimekirja_uuendamine_ebaoÌ\\x83nnestus_-_ID.ee', 'DigiDoc_konteineri_formaatide_elutsuÌ\\x88kkel_-_ID.ee', 'Digiallkirjastamine_ID-kaardi,_mobiil-ID,_Smart-ID_ja_e-Templiga_-_ID.ee', 'Digiallkirjastamine_RIA_DigiDoc_mobiilirakenduses_-_ID.ee', 'Digidoc_failivormingud_BDOC,_CDOC,_ASICE_-_ID.ee', 'DigiligipaÌ\\x88aÌ\\x88setavuse_tagamine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Digitaalne_allkirjastamine_ja_elektroonilised_allkirjad_-_ID.ee', 'Digitaalsed_dokumendid__ID-kaart,_digi-ID,_elamisloakaart_ja_e-residendi_digi-ID_-_ID.ee', 'Digitembeldamine_-_ID.ee', 'Digiturvalisus_-_Taga_enda_ja_oma_laste_turvalisus_-_Politsei-_ja_Piirivalveamet', 'Dokumendi_PIN-koodid_ununenud_voÌ\\x83i_kadunud_-_Politsei-_ja_Piirivalveamet', 'Dokumendi_kasutaja_meelespea_-_Meremehe_teenistusraamatu_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Dokumendi_kasutaja_meelespea_-_MeresoÌ\\x83idutunnistuse_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Dokumendi_kaÌ\\x88ttesaamine_kaupluses_-_Politsei-_ja_Piirivalveamet', 'Dokumendifoto_noÌ\\x83uded_ja_juhised_-_Politsei-_ja_Piirivalveamet', 'Dokumente_vaÌ\\x88ljastavad_Eesti_Vabariigi_vaÌ\\x88lisesindused_-_Politsei-_ja_Piirivalveamet', 'Dokumentide_numbrid_-_Politsei-_ja_Piirivalveamet', 'Dokumentide_vaÌ\\x88ljastamine_Keilas_-_Teenindused_-_Politsei-_ja_Piirivalveamet', 'E-allkirja_kinnituslehe_salvestamine_-_ID.ee', 'E-haÌ\\x88aÌ\\x88letamine_ja_e-valimised_-_ID.ee', 'E-residendi_digi-ID_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'E-residentsus_Venemaa_ja_Valgevene_kodanikele_-_E-residendi_digi-ID_-_Politsei-_ja_Piirivalveamet', 'EL_kiirhoiatussuÌ\\x88steem_Safety_Gate_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'EL_kodaniku_taÌ\\x88htajaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Ebaausad_kauplemisvoÌ\\x83tted_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Eesti-Vene_piiri_uÌ\\x88letamise_kord_-_Politsei-_ja_Piirivalveamet', 'Eesti_kodaniku_pass_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'Eesti_vaÌ\\x88lisesinduses_-_Kuidas_riigiloÌ\\x83ivu_maksta__-_Politsei-_ja_Piirivalveamet', 'Eestis_toÌ\\x88oÌ\\x88tamine_-_Politsei-_ja_Piirivalveamet', 'Ehitamine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ehitiste_ohutus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ehitustooted_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ehted,_vaÌ\\x88aÌ\\x88rismetallist_tooted_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Eksamid_-_Eesti_kodakondsus_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_kehtetuks_tunnistamine_-_Alalise_elaniku_elamisluba_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_loÌ\\x83ppemine_voÌ\\x83i_kehtetuks_tunnistamine_-_Eestis_toÌ\\x88oÌ\\x88tamise_info_vaÌ\\x88lismaalasele_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_loÌ\\x83ppemine_voÌ\\x83i_kehtetuks_tunnistamine_-_Elamisluba_ettevoÌ\\x83tluseks_aÌ\\x88riuÌ\\x88hingu_osanikule_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_loÌ\\x83ppemine_voÌ\\x83i_kehtetuks_tunnistamine_-_Elamisluba_ettevoÌ\\x83tluseks_fuÌ\\x88uÌ\\x88silisest_isikust_ettevoÌ\\x83tjale_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_loÌ\\x83ppemine_voÌ\\x83i_kehtetuks_tunnistamine_-_Elamisluba_oÌ\\x83ppimiseks_vaÌ\\x88lismaalasele_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_loÌ\\x83ppemine_voÌ\\x83i_kehtetuks_tunnistamine_-_Elamisluba_pereliikmele_abikaasa_juurde_elama_asumiseks_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_loÌ\\x83ppemine_voÌ\\x83i_kehtetuks_tunnistamine_-_Elamisluba_taÌ\\x88isealisele_lapsele_pereliikme_juurde_elama_asumiseks_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_loÌ\\x83ppemine_voÌ\\x83i_kehtetuks_tunnistamine_-_OskustoÌ\\x88oÌ\\x88lisena_toÌ\\x88oÌ\\x88le_asumine_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_loÌ\\x83ppemine_voÌ\\x83i_kehtetuks_tunnistamine_-_Perekonnaliikme_taÌ\\x88htajaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_pikendamine_-_Eestis_toÌ\\x88oÌ\\x88tamise_info_vaÌ\\x88lismaalasele_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_pikendamine_-_Elamisluba_ettevoÌ\\x83tluseks_aÌ\\x88riuÌ\\x88hingu_osanikule_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_pikendamine_-_Elamisluba_ettevoÌ\\x83tluseks_fuÌ\\x88uÌ\\x88silisest_isikust_ettevoÌ\\x83tjale_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_pikendamine_-_Elamisluba_ettevoÌ\\x83tluseks_suurinvestorile_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_pikendamine_-_Elamisluba_pereliikmele_abikaasa_juurde_elama_asumiseks_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_pikendamine_-_OskustoÌ\\x88oÌ\\x88lisena_toÌ\\x88oÌ\\x88le_asumine_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_pikendamine_-_Rahvusvaheline_kaitse_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_pikendamine_-_TaÌ\\x88htajaline_elamisluba_puÌ\\x88sivalt_elamiseks_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_taotlemine_-_Eestis_toÌ\\x88oÌ\\x88tamise_info_toÌ\\x88oÌ\\x88andjale_-_Politsei-_ja_Piirivalveamet', 'Elamisloa_taotlemine_-_Elamisluba_oÌ\\x83ppimiseks_oÌ\\x83ppeasutusele_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaardi_kasutaja_meelespea_-_Elamisloakaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaardi_kasutaja_meelespea_-_Elamisloakaardi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaart_-_Alalise_elaniku_elamisluba_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaart_-_Eestis_toÌ\\x88oÌ\\x88tamise_info_vaÌ\\x88lismaalasele_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaart_-_Elamisluba_ettevoÌ\\x83tluseks_aÌ\\x88riuÌ\\x88hingu_osanikule_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaart_-_Elamisluba_ettevoÌ\\x83tluseks_suurinvestorile_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaart_-_Elamisluba_taÌ\\x88isealisele_lapsele_pereliikme_juurde_elama_asumiseks_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaart_-_Elamisluba_vanemale_voÌ\\x83i_vanavanemale_pereliikme_juurde_elama_asumiseks_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaart_-_OskustoÌ\\x88oÌ\\x88lisena_toÌ\\x88oÌ\\x88le_asumine_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaart_-_Pereliikme_alaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaart_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaart_-_TaÌ\\x88htajaline_elamisluba_puÌ\\x88sivalt_elamiseks_-_Politsei-_ja_Piirivalveamet', 'Elamisluba_ja_elamisoÌ\\x83igus_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'ElamisoÌ\\x83iguse_kehtetuks_tunnistamine_-_Pereliikme_alaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Elektriohutus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Elektriseadmed_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Elektroonilised_allkirjad_ja_nende_kaÌ\\x88sitlemine_Euroopas_-_ID.ee', 'EnergiamoÌ\\x83juga_toodete_energiamaÌ\\x88rgistus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Euroopa_reisiinfo_ja_-lubade_suÌ\\x88steem_-_Politsei-_ja_Piirivalveamet', 'Euroopa_tulirelvapass_-_Relvaluba_fuÌ\\x88uÌ\\x88silisele_isikule_-_Politsei-_ja_Piirivalveamet', 'Failide_kruÌ\\x88pteerimine_ja_dekruÌ\\x88pteerimine_DigiDoc4_rakendusega_-_ID.ee', 'Filmi_ja_telesarjade_tegijatele_-_Meediasuhtluse_poÌ\\x83himoÌ\\x83tted_-_Politsei-_ja_Piirivalveamet', 'Finantsteenused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Gaasiseadmed_ja_-paigaldised_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Haapsalu_arestimaja_-_Arestimajad_-_Politsei-_ja_Piirivalveamet', 'Haapsalu_teenindus_-_Teenindused_-_Politsei-_ja_Piirivalveamet', 'HaÌ\\x88daabinumber_112_-_Politsei-_ja_Piirivalveamet', 'Hoiatus-_ja_signaalrelvad_-_Politsei-_ja_Piirivalveamet', 'Hoiatustrahvi_tasumine_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet', 'Hoonete_energiatoÌ\\x83husus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'ID-kaardi,_digi-ID,_elamisloakaardi_ja_diplomaadikaardi_sertifitseerimispoliitika_-_ID.ee', 'ID-kaardi_kasutaja_meelespea_-_ID-kaardi_taotlemine_Euroopa_Liidu_kodanikule_-_Politsei-_ja_Piirivalveamet', 'ID-kaardi_kasutaja_meelespea_-_ID-kaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'ID-kaardi_kasutaja_meelespea_-_ID-kaardi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'ID-kaardi_sertifikaatide_kehtivus_-_ID.ee', 'ID-kaardi_sertifikaatide_peatamine_-_ID.ee', 'ID-kaardi_sertifikaatide_vaatamine_ja_salvestamine_-_ID.ee', 'ID-kaardiga_sisenemine_voÌ\\x83i_allkirjastamine_e-teenustes_ebaoÌ\\x83nnestub_-_ID.ee', 'ID-kaardiÂ\\xa0PIN-koodid__uue_koodiuÌ\\x88mbriku_taotlemine_-_ID.ee', 'ID-kaart_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'ID-kaart_ja_kaardilugeja__kuidas_kontrollida_ID-kaardi_lugeja_toÌ\\x88oÌ\\x88korras_olekut_-_ID.ee', 'ID-kaart_ja_pass_-_Eesti_kodakondsus_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'ID-kaart_ja_selle_kasutusvoÌ\\x83imalused_-_ID.ee', 'ID-kaart_kui_isikut_toÌ\\x83endav_dokument_-_ID.ee', 'ID-kaart_on_kadunud_voÌ\\x83i_varastatud_-_Politsei-_ja_Piirivalveamet', 'ID-tarkvara__diagnostika_failide_ning_logifailide_genereerimine_-_ID.ee', 'ID-tarkvara_andmekaitsetingimused_-_ID.ee', 'ID-tarkvara_koÌ\\x83ige_uuema_versiooni_info_-_ID.ee', 'ID-tarkvara_toetatud_operatsioonisuÌ\\x88steemid_-_ID.ee', 'ID-tarkvara_versioonide_info_(release_notes)_-_ID.ee', 'ID-tarkvara_versioonide_toeperioodi_pikkus_ehk_elutsuÌ\\x88kkel_-_ID.ee', 'Ida-Harju_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'Idapiiri_arendus_ja_investeeringud_-_Piiriehitus_-_Politsei-_ja_Piirivalveamet', 'Info_teisaldatud_soÌ\\x83iduki_omanikule_-_Politsei-_ja_Piirivalveamet', 'Internetipangas_-_Kuidas_riigiloÌ\\x83ivu_maksta__-_Politsei-_ja_Piirivalveamet', 'Iseteenindus_-_Relvaluba_fuÌ\\x88uÌ\\x88silisele_isikule_-_Politsei-_ja_Piirivalveamet', 'Isikuandmete_toÌ\\x88oÌ\\x88tlemine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Isikuandmete_toÌ\\x88oÌ\\x88tlemise_poÌ\\x83himoÌ\\x83tted_-_Andmekaitsetingimused_-_Politsei-_ja_Piirivalveamet', 'Isikukaitsevahendid_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'JaÌ\\x88aÌ\\x88l_viibimiseks_keelatud_ala_taÌ\\x88histamine_-_Politsei-_ja_Piirivalveamet', 'JaÌ\\x88aÌ\\x88olud_-_Politsei-_ja_Piirivalveamet', 'JoÌ\\x83geva_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'JoÌ\\x83geva_teenindus_-_Teenindused_-_Politsei-_ja_Piirivalveamet', 'JoÌ\\x83hvi_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'JuurdepaÌ\\x88aÌ\\x88setavus_-_Politsei-_ja_Piirivalveamet', 'KKK_â\\x80\\x93_korduma_kippuvad_kuÌ\\x88simused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Kaebuse_esitamine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Kagu_politseijaoskond_PoÌ\\x83lvas_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'Kannatanule_-_Politsei-_ja_Piirivalveamet', 'Kasulik_info_kiipkaardi_lugejate_kohta_-_ID.ee', 'Kasulikud_materjalid_-_Rahvusvaheline_kaitse_-_Politsei-_ja_Piirivalveamet', 'Kasutatud_auto_ostmine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Kauplejale_komisjoni_toÌ\\x88oÌ\\x88st_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'KaÌ\\x88itumisjuhised_korruptsiooniohu_vaÌ\\x88hendamiseks_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88rdla_arestimaja_-_Arestimajad_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88rdla_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88ttesaamine_-_E-residendi_digi-ID_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88ttesaamine_-_Eesti_passi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88ttesaamine_-_Eesti_passi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88ttesaamine_-_ID-kaardi_taotlemine_Euroopa_Liidu_kodanikule_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88ttesaamine_-_ID-kaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88ttesaamine_-_ID-kaardi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88ttesaamine_-_Isikut_toÌ\\x83endava_dokumendi_taotlemine_liikumisvoÌ\\x83imetule_inimesele_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88ttesaamine_-_Meremehe_teenistusraamatu_taotlemine_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88ttesaamine_-_MeresoÌ\\x83idutunnistuse_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Kehtetuks_tunnistatud_sertifikaatidega_ID-kaardid_-_Politsei-_ja_Piirivalveamet', 'Kemikaalid_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Kesk-Eesti_politseijaoskond_Paides_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'Kesk-Eesti_politseijaoskond_Raplas_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'Kiiruskaamera_foto_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet', 'Kodakondsustunnistus_-_Eesti_kodakondsus_eestkostetavale_-_Politsei-_ja_Piirivalveamet', 'Kodakondsustunnistus_-_Eesti_kodakondsus_lapsele_-_Politsei-_ja_Piirivalveamet', 'Kodakondsustunnistus_-_Eesti_kodakondsus_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Koduohutus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Kogu_info_sertifikaatide_kasutustingimuste_kohta_-_ID.ee', 'Komisjoni_otsused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Kordonid_-_Politsei-_ja_Piirivalveamet', 'KorruptsioonisuÌ\\x88uÌ\\x88teod_-_Politsei-_ja_Piirivalveamet', 'Kuhu_poÌ\\x88oÌ\\x88rduda__-_Kaitse_ennast_kelmide_eest_-_Politsei-_ja_Piirivalveamet', 'Kuhu_saab_ID-kaardiga_reisida__-_ID.ee', 'Kuidas_ennast_kelmide_eest_kaitsta__-_Kaitse_ennast_kelmide_eest_-_Politsei-_ja_Piirivalveamet', 'Kuidas_kaitsta_ennast_infosoÌ\\x83jas__-_Politsei-_ja_Piirivalveamet', 'Kuidas_kaÌ\\x88ituda_piirilaÌ\\x88hedasel_alal_-_Politsei-_ja_Piirivalveamet', 'Kuidas_kontrollida_ID-kaardi_ja_lugeja_toÌ\\x88oÌ\\x88korras_olekut__-_ID.ee', 'Kuidas_maksta__-_Digi-ID_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Kuidas_maksta__-_Eesti_passi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Kuidas_maksta__-_Elamisloakaardi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Kuidas_maksta__-_ID-kaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Kuidas_maksta__-_ID-kaardi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Kuidas_maksta__-_Meremehe_teenistusraamatu_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Kuidas_maksta__-_MeresoÌ\\x83idutunnistuse_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Kuidas_maksta__-_VaÌ\\x88lismaalase_passi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Kuidas_maksta__-_VaÌ\\x88lismaalase_passi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Kuidas_politsei_kiiruseuÌ\\x88letamist_menetleb__-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet', 'Kuidas_saab_noor_vaÌ\\x88ltida_pahandustesse_sattumist__-_Taga_enda_ja_oma_laste_turvalisus_-_Politsei-_ja_Piirivalveamet', 'Kuressaare_arestimaja_-_Arestimajad_-_Politsei-_ja_Piirivalveamet', 'Kuressaare_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'KuÌ\\x88simused-vastused_-_E-residendi_digi-ID_-_Politsei-_ja_Piirivalveamet', 'KuÌ\\x88simusi_piiriuÌ\\x88letusest_-_Politsei-_ja_Piirivalveamet', 'LaÌ\\x88aÌ\\x88ne-Harju_Kesklinna_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'LaÌ\\x88aÌ\\x88ne-Harju_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'LaÌ\\x88hisuhtevaÌ\\x88givalla_ennetuskampaania_-_Ennetusprojektid_ja_kampaaniad_-_Politsei-_ja_Piirivalveamet', 'Legaalmetroloogia_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Lennu-_ja_merepaÌ\\x88aÌ\\x88ste_-_Otsingu-_ja_paÌ\\x88aÌ\\x88stetoÌ\\x88oÌ\\x88d_-_Politsei-_ja_Piirivalveamet', 'Lennureisijate_oÌ\\x83igused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Lepingud_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'LigipaÌ\\x88aÌ\\x88setavuse_teatis_-_ID.ee', 'LigipaÌ\\x88aÌ\\x88setavuse_teatis_-_JuurdepaÌ\\x88aÌ\\x88setavus_-_Politsei-_ja_Piirivalveamet', 'Liiklusinfo_-_Politsei-_ja_Piirivalveamet', 'LiiklusjaÌ\\x88relevalve-_ja_ennetuskampaaniad_-_Politsei-_ja_Piirivalveamet', 'Liikluskampaania_#5viisi_-_Ennetusprojektid_ja_kampaaniad_-_Politsei-_ja_Piirivalveamet', 'LiikumisvoÌ\\x83imetule_inimesele_ID-kaardi_taotlemine_-_Isikut_toÌ\\x83endava_dokumendi_taotlemine_liikumisvoÌ\\x83imetule_inimesele_-_Politsei-_ja_Piirivalveamet', 'LiikumisvoÌ\\x83imetule_inimesele_elamisloakaardi_taotlemine_-_Isikut_toÌ\\x83endava_dokumendi_taotlemine_liikumisvoÌ\\x83imetule_inimesele_-_Politsei-_ja_Piirivalveamet', 'Lipp_-_SuÌ\\x88mboolika_-_Politsei-_ja_Piirivalveamet', 'Loodusesse_minnes_-_Eksinud_ja_teadmata_kadunud_-_Politsei-_ja_Piirivalveamet', 'LuÌ\\x88hiajalise_toÌ\\x88oÌ\\x88tamise_registreerimine_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'MaÌ\\x88nguasjad_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Meediateenused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Mehitamata_oÌ\\x83husoÌ\\x83idukid_(droonid)_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Menetlemine_-_EL_kodaniku_alaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_Eesti_kodakondsus_eestkostetavale_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_Eesti_kodakondsus_lapsele_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_Eesti_kodakondsus_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_Elamisluba_ettevoÌ\\x83tluseks_aÌ\\x88riuÌ\\x88hingu_osanikule_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_Elamisluba_ettevoÌ\\x83tluseks_fuÌ\\x88uÌ\\x88silisest_isikust_ettevoÌ\\x83tjale_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_Elamisluba_ettevoÌ\\x83tluseks_suurinvestorile_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_Elamisluba_taÌ\\x88isealisele_lapsele_pereliikme_juurde_elama_asumiseks_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_OskustoÌ\\x88oÌ\\x88lisena_toÌ\\x88oÌ\\x88le_asumine_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_Perekonnaliikme_taÌ\\x88htajaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_Pereliikme_alaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'MenetlustaÌ\\x88htaeg_-_E-residendi_digi-ID_-_Politsei-_ja_Piirivalveamet', 'Meremehe_teenistusraamat_ja_meresoÌ\\x83idutunnistus_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'Metsloomad_-_Piiri_tugevdamine_-_Politsei-_ja_Piirivalveamet', 'Mida_teha,_kui_ID-kaart_voÌ\\x83i_muu_digitaalne_dokument_on_kadunud_voÌ\\x83i_varastatud__-_ID.ee', 'Mida_teha,_kui_laÌ\\x88hedane_on_kadunud__-_Eksinud_ja_teadmata_kadunud_-_Politsei-_ja_Piirivalveamet', 'Mida_teha_liiklusoÌ\\x83nnetuse_korral_-_Politsei-_ja_Piirivalveamet', 'Miks_kogutakse_broneeringuinfot__-_Lennureisijate_broneeringuinfo_-_Politsei-_ja_Piirivalveamet', 'Miks_on_vaja_uuendada_ID-tarkvara__-_ID.ee', 'Mis_on_avalik_koosolek__-_Avaliku_koosoleku_registreerimine_-_Politsei-_ja_Piirivalveamet', 'Mis_on_juurdepaÌ\\x88aÌ\\x88setavus__-_JuurdepaÌ\\x88aÌ\\x88setavus_-_Politsei-_ja_Piirivalveamet', 'Mis_see_on__-_Elamisloakaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Mis_see_on__-_Elamisluba_ettevoÌ\\x83tluseks_aÌ\\x88riuÌ\\x88hingu_osanikule_-_Politsei-_ja_Piirivalveamet', 'Mis_see_on__-_Elamisluba_oÌ\\x83ppimiseks_vaÌ\\x88lismaalasele_-_Politsei-_ja_Piirivalveamet', 'Mis_see_on__-_ID-kaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Mis_see_on__-_ID-kaardi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Mis_see_on__-_Meremehe_teenistusraamatu_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Mis_see_on__-_Mobiil-ID_-_Politsei-_ja_Piirivalveamet', 'Mis_see_on__-_Piiriehitus_-_Politsei-_ja_Piirivalveamet', 'Mis_see_on__-_Rahvusvaheline_koostoÌ\\x88oÌ\\x88_-_Politsei-_ja_Piirivalveamet', 'Mis_see_on__-_VaÌ\\x88lismaalase_passi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Mis_see_on__-_VaÌ\\x88lismaalase_passi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Mis_vahe_on_.bdoc_ja_.asice_laiendiga_digiallkirjastatud_dokumentidel__-_ID.ee', 'Mobiil-ID_PIN-koodid_-_ID.ee', 'Mobiil-ID__digitaalne_isikutunnistus_nutitelefonis_-_ID.ee', 'Mobiil-ID_kasutamine_-_ID.ee', 'Mobiil-ID_kasutamisel_tekkis_toÌ\\x83rkeid_-_ID.ee', 'Mobiil-ID_sertifikaadid__uÌ\\x88ldinfo_-_ID.ee', 'Mobiil-ID_taotlemine_ja_aktiveerimine_-_ID.ee', 'Mobiil-IDga_allkirjastamine_ebaoÌ\\x83nnestub_-_ID.ee', 'MyID_portaal_-_ID.ee', 'Narva_arestimaja_-_Arestimajad_-_Politsei-_ja_Piirivalveamet', 'Narva_piiripunkti_toÌ\\x88oÌ\\x88korraldus_-_Politsei-_ja_Piirivalveamet', 'Narva_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'NoÌ\\x83uanded_vanematele_laste_turvalisuse_tagamiseks_-_Taga_enda_ja_oma_laste_turvalisus_-_Politsei-_ja_Piirivalveamet', 'NoÌ\\x83uded_avaliku_koosoleku_korraldamisele_-_Avaliku_koosoleku_registreerimine_-_Politsei-_ja_Piirivalveamet', 'Numbriliikuvus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Numeratsioon_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ohtlikud_tooted_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ohutus_avalikus_kohas_-_Taga_enda_ja_oma_laste_turvalisus_-_Politsei-_ja_Piirivalveamet', 'Ohutusraamat_lapsevanematele_-_Ennetusprojektid_ja_kampaaniad_-_Politsei-_ja_Piirivalveamet', 'Ostmine_e-poest_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ostmine_vaÌ\\x88lismaalt_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Otsuse_vaidlustamine_-_Viisa_ja_viibimisaja_pikendamine_-_Politsei-_ja_Piirivalveamet', 'OÌ\\x83igus_tutvuda_enda_kohta_kogutud_andmetega_-_Andmekaitsetingimused_-_Politsei-_ja_Piirivalveamet', 'OÌ\\x83ppeasutuse_kohustused_-_Elamisluba_oÌ\\x83ppimiseks_-_info_oÌ\\x83ppeasutusele_-_Politsei-_ja_Piirivalveamet', 'OÌ\\x83ppeasutuse_kohustused_-_Elamisluba_oÌ\\x83ppimiseks_oÌ\\x83ppeasutusele_-_Politsei-_ja_Piirivalveamet', 'PIN-_ja_PUK-koodid__soovitused_turvalisuse_tagamiseks_-_ID.ee', 'PIN-_ja_PUK-koodidega_koodiuÌ\\x88mbrik_-_ID.ee', 'PIN-kood_on_blokeeritud_ehk_lukku_laÌ\\x88inud_-_ID.ee', 'PIN-koodide_ja_PUK-koodi_muutmine_-_ID.ee', 'PPA_ja_prefektuuride_uÌ\\x88ldkontaktid_-_Politsei-_ja_Piirivalveamet', 'PPA_lugu_ja_vaÌ\\x88aÌ\\x88rtused_-_Politsei-_ja_Piirivalveamet', 'PPA_sotsiaalmeedias_-_Meediasuhtluse_poÌ\\x83himoÌ\\x83tted_-_Politsei-_ja_Piirivalveamet', 'Pagulase_reisidokument_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'Paide_arestimaja_-_Arestimajad_-_Politsei-_ja_Piirivalveamet', 'Paide_teenindus_-_Teenindused_-_Politsei-_ja_Piirivalveamet', 'Paigalda_ID-tarkvara_-_ID.ee', 'Pakettreisid_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Passi_kasutaja_meelespea_-_Eesti_passi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Passi_kasutaja_meelespea_-_VaÌ\\x88lismaalase_passi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Passi_kasutaja_meelespea_-_VaÌ\\x88lismaalase_passi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'PaÌ\\x88aÌ\\x88stetoÌ\\x88oÌ\\x88del_kasutatav_tehnika_-_Otsingu-_ja_paÌ\\x88aÌ\\x88stetoÌ\\x88oÌ\\x88d_-_Politsei-_ja_Piirivalveamet', 'PaÌ\\x88rnu_arestimaja_-_Arestimajad_-_Politsei-_ja_Piirivalveamet', 'PaÌ\\x88rnu_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'Piirangud_riiki_sisenemisel_Venemaa_FoÌ\\x88deratsiooni_kodanikele_alates_19.09.2022_-_Politsei-_ja_Piirivalveamet', 'Piiri_kaitsmine_-_Piiri_tugevdamine_-_Politsei-_ja_Piirivalveamet', 'Piiri_taÌ\\x88histamine_-_Politsei-_ja_Piirivalveamet', 'Piiriehituse_hange_-_Piiriehitus_-_Politsei-_ja_Piirivalveamet', 'Piiripunktid_-_Politsei-_ja_Piirivalveamet', 'Piirivalve_ajalugu_-_Ajalugu_-_Politsei-_ja_Piirivalveamet', 'Piirkondlik_politseitoÌ\\x88oÌ\\x88_-_Politsei-_ja_Piirivalveamet', 'Pikendamine_-_Elamisluba_alaealisele_lapsele_pereliikme_juurde_elama_asumiseks_-_Politsei-_ja_Piirivalveamet', 'Pikendamine_-_Elamisluba_oÌ\\x83ppimiseks_vaÌ\\x88lismaalasele_-_Politsei-_ja_Piirivalveamet', 'PinPad_kaardilugeja_draiverite_kasutamine_-_ID.ee', 'Politsei-_ja_Piirivalveameti_loomine_-_Ajalugu_-_Politsei-_ja_Piirivalveamet', 'Politsei-_ja_Piirivalveameti_poolt_vaÌ\\x88lja_antavate_dokumentide_naÌ\\x88idised_-_Politsei-_ja_Piirivalveamet', 'Politsei_ajalugu_-_Ajalugu_-_Politsei-_ja_Piirivalveamet', 'Politsei_sekkumine_-_Avaliku_koosoleku_registreerimine_-_Politsei-_ja_Piirivalveamet', 'PolitseitoÌ\\x88oÌ\\x88_abivajajate_otsingutel_-_Eksinud_ja_teadmata_kadunud_-_Politsei-_ja_Piirivalveamet', 'PoÌ\\x83lva_teenindus_-_Teenindused_-_Politsei-_ja_Piirivalveamet', 'PoÌ\\x88oÌ\\x88rdumine_PPA_poole_-_Politsei-_ja_Piirivalveamet', 'PoÌ\\x88oÌ\\x88rdumistele_vastamine_-_Andmekaitsetingimused_-_Politsei-_ja_Piirivalveamet', 'Pressilistidega_liitumine_-_Meediasuhtluse_poÌ\\x83himoÌ\\x83tted_-_Politsei-_ja_Piirivalveamet', 'Probleemid_kaardilugeja_kasutamisel_-_ID.ee', 'Projekt_â\\x80\\x9eAlaealiste_erikohtlemise_suÌ\\x88steemi_loomineâ\\x80\\x9c_-_Ennetusprojektid_ja_kampaaniad_-_Politsei-_ja_Piirivalveamet', 'Puudusega_toode,_garantii_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'PuÌ\\x88rotehnika,_ilutulestik_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'RIA_DigiDoc_mobiilirakendus_-_ID.ee', 'RIA_DigiDoc_rakenduse_versioonide_info_(release_notes)_-_ID.ee', 'Raadiosagedused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Raadiosageduste_kasutamine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Rahaasjad_korda!_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Rahvusvahelise_kaitse_pikendamine_-_Info_seoses_soÌ\\x83jaga_Ukrainas_-_Politsei-_ja_Piirivalveamet', 'Rahvusvahelise_kaitse_taotlemine_-_Info_seoses_soÌ\\x83jaga_Ukrainas_-_Politsei-_ja_Piirivalveamet', 'Rail_Baltic_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Rail_Balticu_keskkonnamoÌ\\x83jude_hindamine_(KMH)_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Rakvere_arestimaja_-_Arestimajad_-_Politsei-_ja_Piirivalveamet', 'Rakvere_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'Rapla_arestimaja_-_Arestimajad_-_Politsei-_ja_Piirivalveamet', 'Rapla_teenindus_-_Teenindused_-_Politsei-_ja_Piirivalveamet', 'Raudteeohutuse_meelespea_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Raudteeohutuse_oÌ\\x83ppematerjalid_ja_koolitused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Reisimine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Reklaam_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Relva_muÌ\\x88uÌ\\x88mise_luba_-_Relvaluba_fuÌ\\x88uÌ\\x88silisele_isikule_-_Politsei-_ja_Piirivalveamet', 'Relva_soetamine_-_Relvaluba_juriidilisele_isikule_-_Politsei-_ja_Piirivalveamet', 'Relvade_sisse-_ja_vaÌ\\x88ljavedu_-_Relvaluba_fuÌ\\x88uÌ\\x88silisele_isikule_-_Politsei-_ja_Piirivalveamet', 'Relvaloa_kehtivuse_loÌ\\x83ppemine_-_Politsei-_ja_Piirivalveamet', 'Relvaloa_vahetamine_-_Relvaluba_fuÌ\\x88uÌ\\x88silisele_isikule_-_Politsei-_ja_Piirivalveamet', 'Relvaluba_-_Relvaluba_fuÌ\\x88uÌ\\x88silisele_isikule_-_Politsei-_ja_Piirivalveamet', 'Relvaluba_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'Relvaluba_juriidilisele_isikule_-_Politsei-_ja_Piirivalveamet', 'Relvalubadega_seotud_blanketid_-_Politsei-_ja_Piirivalveamet', 'Relvasoetamisluba_-_Relvaluba_fuÌ\\x88uÌ\\x88silisele_isikule_-_Politsei-_ja_Piirivalveamet', 'Reostusest_teatamine_-_ReostustoÌ\\x83rje_-_Politsei-_ja_Piirivalveamet', 'ReostustoÌ\\x83rjevahendid_ja_â\\x80\\x93tehnika_-_ReostustoÌ\\x83rje_-_Politsei-_ja_Piirivalveamet', 'Riigihanked_-_Politsei-_ja_Piirivalveamet', 'Riigiinfo_telefon_1247_-_Politsei-_ja_Piirivalveamet', 'RiigiloÌ\\x83ivu_tasumisest_vabastamine_-_Politsei-_ja_Piirivalveamet', 'Riigiportaal_eesti.ee_-_ID.ee', 'RiskikaÌ\\x88itumise_uuring_-_Ennetusprojektid_ja_kampaaniad_-_Politsei-_ja_Piirivalveamet', 'RoÌ\\x83ivad_ja_jalatsid_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Saadetud_trahviteated_aastate_loÌ\\x83ikes_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet', 'Seotud_organisatsioonid_-_Politsei-_ja_Piirivalveamet', 'Sertifikaadid_ja_nende_turvalisus_-_ID.ee', 'Sideteenused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Sideteenuste_kaart_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Sideturg_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Soovid_DigiDoc4_rakenduses_digiallkirja_anda_-_ID.ee', 'Soovid_ID-tarkvara_DigiDoc4_oma_arvutis_uÌ\\x88les_leida__-_ID.ee', 'Soovid_alustada_ID-kaardi_elektroonilist_kasutamist_-_ID.ee', 'Soovid_alustada_mobiil-ID_kasutamist_-_ID.ee', 'Soovid_digi-ID_kaarti_-_ID.ee', 'Soovid_uut_ID-kaarti_-_ID.ee', 'Soovin_Smart-ID_kasutajaks_hakata_-_ID.ee', 'SoÌ\\x83iduki_eest_vastutav_isik_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet', 'TTJA_otsuste_vaidlustamine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'TV-_ja_raadioringhaÌ\\x88aÌ\\x88ling_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Taga_enda_ja_oma_laste_turvalisus_-_Taga_enda_ja_oma_laste_turvalisus_-_Politsei-_ja_Piirivalveamet', 'Tallinna_politseiorkester_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_EL_kodaniku_alaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_Eesti_kodakondsus_eestkostetavale_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_Eesti_kodakondsus_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_Elamisluba_alaealisele_lapsele_pereliikme_juurde_elama_asumiseks_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_Elamisluba_ettevoÌ\\x83tluseks_aÌ\\x88riuÌ\\x88hingu_osanikule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_Elamisluba_ettevoÌ\\x83tluseks_iduettevoÌ\\x83tjale_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_Elamisluba_oÌ\\x83ppimiseks_vaÌ\\x88lismaalasele_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_Elamisluba_taÌ\\x88isealisele_lapsele_pereliikme_juurde_elama_asumiseks_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_OskustoÌ\\x88oÌ\\x88lisena_toÌ\\x88oÌ\\x88le_asumine_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_Perekonnaliikme_taÌ\\x88htajaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_Pereliikme_alaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_TaÌ\\x88htajaline_elamisluba_puÌ\\x88sivalt_elamiseks_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_iseteeninduses_-_Eesti_passi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_iseteeninduses_-_Elamisloakaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_iseteeninduses_-_Elamisloakaardi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_iseteeninduses_-_ID-kaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_iseteeninduses_-_VaÌ\\x88lismaalase_passi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_Ajutise_reisidokumendi_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_Digi-ID_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_Eesti_passi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_Elamisloakaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_Elamisloakaardi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_ID-kaardi_taotlemine_Euroopa_Liidu_kodanikule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_ID-kaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_MeresoÌ\\x83idutunnistuse_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_VaÌ\\x88lismaalase_passi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_VaÌ\\x88lismaalase_passi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_vaÌ\\x88lismaal_-_Digi-ID_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_vaÌ\\x88lismaal_-_Eesti_passi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_vaÌ\\x88lismaal_-_Eesti_passi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_vaÌ\\x88lismaal_-_Elamisloakaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_vaÌ\\x88lismaal_-_Elamisloakaardi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Tarbija_oÌ\\x83igused_ja_kohustused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tarbijaharidus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tarbijate_kaitse_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tarbijavaidluste_komisjon_-_KKK_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tarbijavaidluste_komisjon_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tarbijavaidluste_komisjonist_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tartu_teenindus_-_Teenindused_-_Politsei-_ja_Piirivalveamet', 'TaÌ\\x88iendav_laskekatse_relvaloa_kehtivuse_pikendamisel_-_Relvaluba_fuÌ\\x88uÌ\\x88silisele_isikule_-_Politsei-_ja_Piirivalveamet', 'Teavita_meid_probleemist_-_ID.ee', 'Teenindused_-_Politsei-_ja_Piirivalveamet', 'Teeninduses_-_Kuidas_riigiloÌ\\x83ivu_maksta__-_Politsei-_ja_Piirivalveamet', 'Teenuse_ohutus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Teenuste_lepingud_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Telefoni-_ja_koduuksemuÌ\\x88uÌ\\x88k_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Telli_dokument_Selverisse!_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_Alalise_elaniku_elamisluba_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_EL_kodaniku_alaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_Eesti_kodakondsus_eestkostetavale_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_Eestis_toÌ\\x88oÌ\\x88tamise_info_vaÌ\\x88lismaalasele_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_Elamisluba_ettevoÌ\\x83tluseks_aÌ\\x88riuÌ\\x88hingu_osanikule_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_Elamisluba_ettevoÌ\\x83tluseks_fuÌ\\x88uÌ\\x88silisest_isikust_ettevoÌ\\x83tjale_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_Elamisluba_ettevoÌ\\x83tluseks_suurinvestorile_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_Elamisluba_oÌ\\x83ppimiseks_-_info_oÌ\\x83ppeasutusele_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_Elamisluba_oÌ\\x83ppimiseks_oÌ\\x83ppeasutusele_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_OskustoÌ\\x88oÌ\\x88lisena_toÌ\\x88oÌ\\x88le_asumine_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_Perekonnaliikme_taÌ\\x88htajaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_Pereliikme_alaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_TaÌ\\x88htajaline_elamisluba_puÌ\\x88sivalt_elamiseks_-_Politsei-_ja_Piirivalveamet', 'Toetatud_PPA_projektid_-_Piiriehitus_-_Politsei-_ja_Piirivalveamet', 'Toodete_ligipaÌ\\x88aÌ\\x88setavus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'ToÌ\\x83stuksed_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'ToÌ\\x88oÌ\\x88andja_kohustused_-_Eestis_toÌ\\x88oÌ\\x88tamise_info_toÌ\\x88oÌ\\x88andjale_-_Politsei-_ja_Piirivalveamet', 'ToÌ\\x88oÌ\\x88d_raudtee_kaitsevoÌ\\x88oÌ\\x88ndis_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'TrahvimaÌ\\x88aÌ\\x88rad_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet', 'Trahviotsuse_edasi_kaebamine_seoses_kiir-_voÌ\\x83i_uÌ\\x88ldmenetlusega_-_Trahvid_-_Politsei-_ja_Piirivalveamet', 'Trahviteade_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet', 'Trahviteate_kaÌ\\x88ttetoimetamine_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet', 'Tsiviilrelvade_kaÌ\\x88itlemise_tegevusluba_-_Majandustegevuse_load_-_Politsei-_ja_Piirivalveamet', 'TurvanoÌ\\x83utele_mittevastavad_ID-kaardid_-_Politsei-_ja_Piirivalveamet', 'Turvateenuse_osutamise_tegevusluba_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'Turvategevuse_tegevusalad_-_Majandustegevuse_load_-_Politsei-_ja_Piirivalveamet', 'Ubuntu__ID-tarkvara_paigaldamine,_uuendamine_ja_eemaldamine_-_ID.ee', 'Ukraina_soÌ\\x83japoÌ\\x83genike_Eestis_olemise_voÌ\\x83imalused_-_Info_seoses_soÌ\\x83jaga_Ukrainas_-_Politsei-_ja_Piirivalveamet', 'UurimistoÌ\\x88oÌ\\x88de_laÌ\\x88biviimine_PPA-s_-_Politsei-_ja_Piirivalveamet', 'Uuringud_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'UÌ\\x88htse_laadija_noÌ\\x83uded_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'UÌ\\x88ldhuviteenused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'UÌ\\x88ldinfo_-_Alalise_elaniku_elamisluba_-_Politsei-_ja_Piirivalveamet', 'UÌ\\x88ldinfo_-_Elamisluba_ettevoÌ\\x83tluseks_fuÌ\\x88uÌ\\x88silisest_isikust_ettevoÌ\\x83tjale_-_Politsei-_ja_Piirivalveamet', 'UÌ\\x88ldinfo_-_Elamisluba_taÌ\\x88isealisele_lapsele_pereliikme_juurde_elama_asumiseks_-_Politsei-_ja_Piirivalveamet', 'UÌ\\x88ldinfo_-_Elamisluba_vanemale_voÌ\\x83i_vanavanemale_pereliikme_juurde_elama_asumiseks_-_Politsei-_ja_Piirivalveamet', 'UÌ\\x88ldinfo_-_TaÌ\\x88htajaline_elamisluba_puÌ\\x88sivalt_elamiseks_-_Politsei-_ja_Piirivalveamet', 'UÌ\\x88leeuroopaline_sissemurdmiste_fookuspaÌ\\x88ev_-_Ennetusprojektid_ja_kampaaniad_-_Politsei-_ja_Piirivalveamet', 'UÌ\\x88lekanne_vaÌ\\x88lisriigi_pangast_-_Kuidas_riigiloÌ\\x83ivu_maksta__-_Politsei-_ja_Piirivalveamet', 'Vaidlustuse_lahendamine_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet', 'Varjupaiga_taotlemine_-_Rahvusvaheline_kaitse_-_Politsei-_ja_Piirivalveamet', 'Varrukaembleem_-_SuÌ\\x88mboolika_-_Politsei-_ja_Piirivalveamet', 'VaÌ\\x88lismaalase_pass_-_RiigiloÌ\\x83ivude_maÌ\\x88aÌ\\x88rad_-_Politsei-_ja_Piirivalveamet', 'VaÌ\\x88lja_ehitatud_piir_-_Piiriehitus_-_Politsei-_ja_Piirivalveamet', 'VaÌ\\x88ljastuskohad_-_E-residendi_digi-ID_-_Politsei-_ja_Piirivalveamet', 'Vee-_ja_oÌ\\x83husoÌ\\x83iduki_raadioload_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Veebilehe_id.ee_privaatsustingimused_-_ID.ee', 'Veebilehitsejate_seadistamine_ID-kaardi_kasutamiseks_-_ID.ee', 'Venemaa_ei_luba_Ukraina_kodanikel_siseneda_Venemaale_-_Info_seoses_soÌ\\x83jaga_Ukrainas_-_Politsei-_ja_Piirivalveamet', 'Vihje_saatmine_-_Politsei-_ja_Piirivalveamet', 'Viibimisaja_pikendamine_-_Viisa_ja_viibimisaja_pikendamine_-_Politsei-_ja_Piirivalveamet', 'Viljandi_arestimaja_-_Arestimajad_-_Politsei-_ja_Piirivalveamet', 'Viljandi_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'VoÌ\\x83ru_arestimaja_-_Arestimajad_-_Politsei-_ja_Piirivalveamet', 'VoÌ\\x83ru_teenindus_-_Teenindused_-_Politsei-_ja_Piirivalveamet', 'Web_eID_-_ID.ee', 'Wi-Fi_seadmete_kasutus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Windows__ID-tarkvara_paigaldus_loÌ\\x83ppes_toÌ\\x83rkega_-_ID.ee', 'eIDAS_ehk_e-identimise_ja_e-tehingute_maÌ\\x88aÌ\\x88rus_-_ID.ee', 'macOS__ID-tarkvara_paigaldamise_ja_eemaldamise_juhised_-_ID.ee']\n", + "\n", + "==================================================\n", + "\n", + "[]\n", + "[]\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "[]\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "[]\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'metadata.json', 'faqs.json']\n", + "['conversations.json', 'faqs.json', 'metadata.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'faqs.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['faqs.json', 'metadata.json', 'conversations.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "['faqs.json', 'conversations.json', 'metadata.json']\n", + "['metadata.json', 'conversations.json', 'faqs.json']\n", + "Conversion complete! Output saved to conversations.csv\n", + "\n", + "Preview of generated CSV:\n", + "['conversation_id', 'turn', 'speaker', 'text', 'agency']\n", + "['ddc5b609-a646-4700-bc06-f1b1a0f8691a', '1', 'user', 'Tere! Ma olen kuulnud, et on oluline kontrollida isikukaitsevahendite ohutust. Kust ma peaksin alustama nende kohta infot leidmiseks?', 'output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet']\n", + "['ddc5b609-a646-4700-bc06-f1b1a0f8691a', '2', 'assistant', 'Tere! Oluline küsimus. Alustuseks on oluline teada, et isikukaitsevahendite ohutust reguleerib Euroopa Parlamendi ja Nõukogu määrus (EL) 2016/425. See määrus kehtib täismahus alates 21. aprillist 2018. Lisainfot saate ka Eesti Standardimis- ja Akrediteerimiskeskuse koduleheküljelt www.evs.ee, kus on erinevad standardid olenevalt tootekategooriast.', 'output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet']\n", + "['2ae2967c-c5c4-4af3-a696-849323e69a68', '1', 'user', 'Tere! Ma olen kuulnud, et Tarbijakaitseamet järelleb isikukaitsevahendite ohutust. Mis see tähendab täpsemalt?', 'output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet']\n", + "['2ae2967c-c5c4-4af3-a696-849323e69a68', '2', 'assistant', 'Tere! See tähendab, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TJAT) järelleb, et isikukaitsevahendid vastaksid ohutusnõuetele. Peamiseks seadusandluseks on Euroopa Parlamendi ja Nõukogu määrus (EL) 2016/425.', 'output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet']\n" + ] + } + ], + "source": [ + "import os\n", + "import json\n", + "import csv\n", + "import uuid\n", + "from pathlib import Path\n", + "\n", + "\n", + "def convert_conversations_to_csv(\n", + " input_dir=\"/home/ckittask/ria/Dataset-Generator/output_dataset_gemma12\",\n", + " output_file=\"conversations.csv\",\n", + "):\n", + " \"\"\"\n", + " Convert conversations.json files from nested directory structure to CSV format.\n", + "\n", + " Args:\n", + " input_dir (str): Root directory containing agency folders\n", + " output_file (str): Output CSV file name\n", + " \"\"\"\n", + "\n", + " # Open CSV file for writing\n", + " with open(output_file, \"w\", newline=\"\", encoding=\"utf-8\") as csvfile:\n", + " fieldnames = [\"conversation_id\", \"turn\", \"speaker\", \"text\", \"agency\"]\n", + " writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n", + "\n", + " # Write header\n", + " writer.writeheader()\n", + "\n", + " # Walk through the directory structure\n", + " convs = 0\n", + " for root, dirs, files in os.walk(input_dir):\n", + " # Check if conversations.json exists in current directory\n", + " print(files)\n", + " if \"conversations.json\" in files:\n", + " json_path = os.path.join(root, \"conversations.json\")\n", + "\n", + " # Extract agency name from path\n", + " path_parts = Path(root).parts\n", + " if len(path_parts) >= 2:\n", + " agency = path_parts[\n", + " -2\n", + " ] # Assuming structure: output_dataset/agency1/topic1/\n", + " else:\n", + " agency = \"unknown\"\n", + "\n", + " try:\n", + " # Read and parse JSON file\n", + " with open(json_path, \"r\", encoding=\"utf-8\") as jsonfile:\n", + " data = json.load(jsonfile)\n", + "\n", + " # Process each conversation in the JSON file\n", + " count = 0\n", + " for conversation in data:\n", + " if agency == \"output_Politsei-_ja_Piirivalveamet\":\n", + " if count == 1:\n", + " continue\n", + " count += 1\n", + " if \"messages\" in conversation:\n", + " # Generate unique conversation ID\n", + " conversation_id = str(uuid.uuid4())\n", + " convs += 1\n", + " # Process each message in the conversation\n", + " for turn, message in enumerate(conversation[\"messages\"], 1):\n", + " writer.writerow(\n", + " {\n", + " \"conversation_id\": conversation_id,\n", + " \"turn\": turn,\n", + " \"speaker\": message.get(\"role\", \"\"),\n", + " \"text\": message.get(\"content\", \"\"),\n", + " \"agency\": agency,\n", + " }\n", + " )\n", + " if turn == 2:\n", + " break\n", + "\n", + " # print(f\"Processed: {json_path}\")\n", + "\n", + " except json.JSONDecodeError as e:\n", + " print(f\"Error reading JSON file {json_path}: {e}\")\n", + " except Exception as e:\n", + " print(f\"Error processing file {json_path}: {e}\")\n", + " print(f\"Total conversations processed: {convs}\")\n", + " print(f\"Conversion complete! Output saved to {output_file}\")\n", + "\n", + "\n", + "def get_statistics(\n", + " input_dir=\"/home/ckittask/ria/Dataset-Generator/output_dataset_gemma12\",\n", + "):\n", + " \"\"\"\n", + " Print statistics about the dataset structure.\n", + " \"\"\"\n", + " total_files = 0\n", + " agencies = set()\n", + " topics = set()\n", + "\n", + " for root, dirs, files in os.walk(input_dir):\n", + " if \"conversations.json\" in files:\n", + " total_files += 1\n", + " path_parts = Path(root).parts\n", + " if len(path_parts) >= 2:\n", + " agencies.add(path_parts[-2])\n", + " if len(path_parts) >= 3:\n", + " topics.add(path_parts[-1])\n", + "\n", + " print(\"Dataset Statistics:\")\n", + " print(f\"- Total conversations.json files: {total_files}\")\n", + " print(f\"- Number of agencies: {len(agencies)}\")\n", + " print(f\"- Number of unique topics: {len(topics)}\")\n", + " print(f\"- Agencies found: {sorted(agencies)}\")\n", + " print(f\"- Topics found: {sorted(topics)}\")\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " # Print dataset statistics first\n", + " get_statistics()\n", + " print(\"\\n\" + \"=\" * 50 + \"\\n\")\n", + "\n", + " # Convert to CSV\n", + " convert_conversations_to_csv()\n", + "\n", + " # Optional: Preview the first few rows of the generated CSV\n", + " print(\"\\nPreview of generated CSV:\")\n", + " try:\n", + " with open(\"conversations.csv\", \"r\", encoding=\"utf-8\") as f:\n", + " reader = csv.reader(f)\n", + " for i, row in enumerate(reader):\n", + " if i < 5: # Show first 5 rows\n", + " print(row)\n", + " else:\n", + " break\n", + " except FileNotFoundError:\n", + " print(\"CSV file not found.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "010f3df2", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ria", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/experiments/base_model_training/scripts/evaluate.py b/experiments/base_model_training/scripts/evaluate.py new file mode 100644 index 00000000..33d20556 --- /dev/null +++ b/experiments/base_model_training/scripts/evaluate.py @@ -0,0 +1,606 @@ +import os +import sys +import argparse +import json +import time +import torch +import numpy as np +from loguru import logger +import pandas as pd +from transformers import AutoModelForSequenceClassification, AutoTokenizer +from torch.utils.data import DataLoader +import mlflow +import psutil +import matplotlib.pyplot as plt +import seaborn as sns + +# Import from our utils script +from utils import ( + set_random_seeds, + compute_metrics, + measure_inference_speed, + measure_model_size, +) + +# Import custom dataset class from train.py +from train import TextClassificationDataset + +logger.remove() +# add stout handler +logger.add(sys.stdout, format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}") + + +def load_model_and_tokenizer(model_path, device): + """ + Load a model and tokenizer from the specified path. + + Args: + model_path (str): Path to the saved model + device (torch.device): Device to load the model onto + + Returns: + tuple: (model, tokenizer) + """ + logger.info(f"Loading model from {model_path}") + model = AutoModelForSequenceClassification.from_pretrained(model_path) + tokenizer = AutoTokenizer.from_pretrained(model_path) + + model.to(device) + return model, tokenizer + + +def load_test_data(data_path, tokenizer, max_seq_length=128): + """ + Load and prepare test data. + """ + logger.info(f"Loading test data from {data_path}") + + if data_path.endswith(".csv"): + df = pd.read_csv(data_path) + elif data_path.endswith(".json"): + df = pd.read_json(data_path, lines=True) + else: + raise ValueError(f"Unsupported file format: {data_path}") + + text_col = next( + (col for col in ["text", "content", "sentence"] if col in df.columns), None + ) + label_col = next( + (col for col in ["label", "class", "target"] if col in df.columns), None + ) + + if text_col is None or label_col is None: + raise ValueError("Could not identify text and label columns in the data") + + texts = df[text_col].values + labels = df[label_col].values + + # Create dataset and dataloader + dataset = TextClassificationDataset(texts, labels, tokenizer, max_seq_length) + dataloader = DataLoader(dataset, batch_size=32, shuffle=False, num_workers=4) + + return dataloader, texts, labels + + +def evaluate_model(model, dataloader, device): + """ + Evaluate the model on the test data. + """ + logger.info("Evaluating model accuracy...") + model.eval() + all_preds = [] + all_labels = [] + all_probs = [] + + with torch.no_grad(): + for batch in dataloader: + input_ids = batch["input_ids"].to(device) + attention_mask = batch["attention_mask"].to(device) + labels = batch["label"].to(device) + + outputs = model(input_ids=input_ids, attention_mask=attention_mask) + + logits = outputs.logits + probs = torch.nn.functional.softmax(logits, dim=1) + preds = torch.argmax(logits, dim=1) + + all_preds.extend(preds.cpu().numpy()) + all_labels.extend(labels.cpu().numpy()) + all_probs.extend(probs.cpu().numpy()) + + return np.array(all_preds), np.array(all_labels), np.array(all_probs) + + +def measure_inference_performance( + model, dataloader, device, num_runs=100, batch_sizes=None +): + """ + Measure inference performance metrics. + """ + logger.info("Measuring inference performance...") + performance_metrics = {} + + # Get a sample batch + sample_batch = next(iter(dataloader)) + + # Measure inference time for a single batch + inference_time = measure_inference_speed( + model, sample_batch, device, num_runs=num_runs, warm_up=10 + ) + + performance_metrics["avg_inference_time_seconds"] = inference_time + performance_metrics["avg_inference_time_ms"] = inference_time * 1000 + performance_metrics["samples_per_second"] = ( + sample_batch["input_ids"].shape[0] / inference_time + ) + + model_size_mb = measure_model_size(model) + performance_metrics["model_size_mb"] = model_size_mb + + memory_before = psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024) # MB + + with torch.no_grad(): + _ = model( + input_ids=sample_batch["input_ids"].to(device), + attention_mask=sample_batch["attention_mask"].to(device), + ) + + memory_after = psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024) # MB + performance_metrics["memory_usage_mb"] = memory_after - memory_before + + if batch_sizes and device == torch.device("cuda"): + batch_time_results = {} + for batch_size in batch_sizes: + batch = { + "input_ids": sample_batch["input_ids"][:1].repeat(batch_size, 1), + "attention_mask": sample_batch["attention_mask"][:1].repeat( + batch_size, 1 + ), + } + + # Measure time and average over 10 runs + start_time = time.time() + with torch.no_grad(): + for _ in range(10): + _ = model( + input_ids=batch["input_ids"].to(device), + attention_mask=batch["attention_mask"].to(device), + ) + end_time = time.time() + + batch_time = (end_time - start_time) / 10 + batch_time_results[batch_size] = batch_time + + performance_metrics["batch_size_benchmarks"] = batch_time_results + + return performance_metrics + + +def log_evaluation_results( + accuracy_metrics, performance_metrics, model_name, output_dir, run_id=None +): + """ + Log evaluation results to files and MLflow. + """ + os.makedirs(output_dir, exist_ok=True) + + all_metrics = { + "model_name": model_name, + "accuracy_metrics": accuracy_metrics, + "performance_metrics": performance_metrics, + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + } + + metrics_path = os.path.join(output_dir, f"{model_name}_evaluation.json") + with open(metrics_path, "w") as f: + + def convert_for_json(obj): + if isinstance( + obj, (np.int_, np.intc, np.intp, np.int8, np.int16, np.int32, np.int64) + ): + return int(obj) + elif isinstance(obj, (np.float_, np.float16, np.float32, np.float64)): + return float(obj) + elif isinstance(obj, (np.ndarray,)): + return obj.tolist() + elif isinstance(obj, dict): + return {k: convert_for_json(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [convert_for_json(i) for i in obj] + else: + return obj + + json.dump(convert_for_json(all_metrics), f, indent=2) + + if run_id: + try: + mlflow.set_tracking_uri(os.environ.get("MLFLOW_TRACKING_URI", "mlruns")) + with mlflow.start_run(run_id=run_id): + # Log accuracy metrics + for k, v in accuracy_metrics.items(): + if k != "confusion_matrix" and not isinstance( + v, (dict, list, np.ndarray) + ): + mlflow.log_metric(f"test_{k}", v) + + # Log performance metrics + for k, v in performance_metrics.items(): + if not isinstance(v, (dict, list, np.ndarray)): + mlflow.log_metric(k, v) + + # Log the full results file + mlflow.log_artifact(metrics_path) + except Exception as e: + logger.warning(f"Warning: Failed to log to MLflow: {str(e)}") + + logger.info(f"Evaluation results saved to {metrics_path}") + + +def create_evaluation_plots( + predictions, true_labels, probabilities, model_name, output_dir +): + """ + Create evaluation plots and save them. + """ + os.makedirs(output_dir, exist_ok=True) + + cm = np.zeros((len(np.unique(true_labels)), len(np.unique(true_labels))), dtype=int) + for i, j in zip(true_labels, predictions): + cm[i][j] += 1 + + plt.figure(figsize=(10, 8)) + sns.heatmap( + cm, + annot=True, + fmt="d", + cmap="Blues", + xticklabels=[f"Class {i}" for i in range(cm.shape[0])], + yticklabels=[f"Class {i}" for i in range(cm.shape[0])], + ) + plt.xlabel("Predicted") + plt.ylabel("True") + plt.title(f"{model_name} Confusion Matrix") + plt.tight_layout() + + cm_path = os.path.join(output_dir, f"{model_name}_confusion_matrix.png") + plt.savefig(cm_path) + plt.close() + + # For binary classification, plot ROC curve + if probabilities.shape[1] == 2: + from sklearn.metrics import roc_curve, auc + + fpr, tpr, _ = roc_curve(true_labels, probabilities[:, 1]) + roc_auc = auc(fpr, tpr) + + plt.figure(figsize=(8, 6)) + plt.plot(fpr, tpr, lw=2, label=f"ROC curve (area = {roc_auc:.2f})") + plt.plot([0, 1], [0, 1], "k--", lw=2) + plt.xlim([0.0, 1.0]) + plt.ylim([0.0, 1.05]) + plt.xlabel("False Positive Rate") + plt.ylabel("True Positive Rate") + plt.title(f"{model_name} ROC Curve") + plt.legend(loc="lower right") + + roc_path = os.path.join(output_dir, f"{model_name}_roc_curve.png") + plt.savefig(roc_path) + plt.close() + + # Precision-Recall curve + from sklearn.metrics import precision_recall_curve, average_precision_score + + precision, recall, _ = precision_recall_curve(true_labels, probabilities[:, 1]) + avg_precision = average_precision_score(true_labels, probabilities[:, 1]) + + plt.figure(figsize=(8, 6)) + plt.plot(recall, precision, lw=2, label=f"PR curve (AP = {avg_precision:.2f})") + plt.xlim([0.0, 1.0]) + plt.ylim([0.0, 1.05]) + plt.xlabel("Recall") + plt.ylabel("Precision") + plt.title(f"{model_name} Precision-Recall Curve") + plt.legend(loc="lower left") + + pr_path = os.path.join(output_dir, f"{model_name}_pr_curve.png") + plt.savefig(pr_path) + plt.close() + + try: + mlflow.log_artifact(cm_path) + if probabilities.shape[1] == 2: + mlflow.log_artifact(roc_path) + mlflow.log_artifact(pr_path) + except Exception as e: + logger.warning(f"Warning: Failed to log plots to MLflow: {str(e)}") + + +def plot_performance_comparison(model_names, performance_metrics_list, output_dir): + """ + Create performance comparison plots across models. + + """ + os.makedirs(output_dir, exist_ok=True) + + metrics_to_plot = ["avg_inference_time_ms", "samples_per_second", "model_size_mb"] + + for metric in metrics_to_plot: + values = [metrics.get(metric, 0) for metrics in performance_metrics_list] + + plt.figure(figsize=(10, 6)) + bars = plt.bar(model_names, values) + plt.title(f"Comparison of {metric}") + plt.ylabel(metric.replace("_", " ").title()) + plt.xlabel("Model") + + for bar, value in zip(bars, values): + if metric == "avg_inference_time_ms": + label = f"{value:.2f} ms" + elif metric == "samples_per_second": + label = f"{value:.1f}" + else: + label = f"{value:.1f} MB" + + plt.text( + bar.get_x() + bar.get_width() / 2, + bar.get_height() + 0.1, + label, + ha="center", + va="bottom", + ) + + plt.xticks(rotation=45) + plt.tight_layout() + + plot_path = os.path.join(output_dir, f"compare_{metric}.png") + plt.savefig(plot_path) + plt.close() + + try: + mlflow.log_artifact(plot_path) + except Exception as e: + logger.warning( + f"Warning: Failed to log comparison plot to MLflow: {str(e)}" + ) + + +def evaluate_and_compare_models(model_paths, model_names, test_data_path, output_dir): + """ + Evaluate and compare multiple models on the same test data. + + """ + os.makedirs(output_dir, exist_ok=True) + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + logger.info(f"Using device: {device}") + + all_accuracy_metrics = [] + all_performance_metrics = [] + + for model_path, model_name in zip(model_paths, model_names): + logger.info(f"\nEvaluating model: {model_name}") + + # Load model and tokenizer + model, tokenizer = load_model_and_tokenizer(model_path, device) + + # Load test data + dataloader, _, labels = load_test_data(test_data_path, tokenizer) + + # Determine number of classes + num_labels = len(np.unique(labels)) + logger.info(f"Detected {num_labels} classes") + + # Evaluate model accuracy + predictions, true_labels, probabilities = evaluate_model( + model, dataloader, device + ) + + accuracy_metrics, _ = compute_metrics(true_labels, predictions, probabilities) + all_accuracy_metrics.append(accuracy_metrics) + + model_output_dir = os.path.join(output_dir, model_name) + os.makedirs(model_output_dir, exist_ok=True) + + create_evaluation_plots( + predictions, true_labels, probabilities, model_name, model_output_dir + ) + + # Measure inference performance + batch_sizes = ( + [1, 2, 4, 8, 16, 32, 64] if device == torch.device("cuda") else None + ) + + performance_metrics = measure_inference_performance( + model, dataloader, device, batch_sizes=batch_sizes + ) + all_performance_metrics.append(performance_metrics) + + # Print results + logger.info(f"\nAccuracy Metrics for {model_name}:") + for k, v in accuracy_metrics.items(): + if k != "confusion_matrix": + logger.info(f" {k}: {v}") + + logger.info(f"\nPerformance Metrics for {model_name}:") + for k, v in performance_metrics.items(): + if not isinstance(v, dict): + print(f" {k}: {v}") + + # Log to files + log_evaluation_results( + accuracy_metrics, performance_metrics, model_name, model_output_dir + ) + + plot_performance_comparison(model_names, all_performance_metrics, output_dir) + + comparison_data = [] + for i, (model_name, acc_metrics, perf_metrics) in enumerate( + zip(model_names, all_accuracy_metrics, all_performance_metrics) + ): + model_data = { + "model_name": model_name, + "accuracy": acc_metrics.get("accuracy", 0), + "precision": acc_metrics.get("precision", 0), + "recall": acc_metrics.get("recall", 0), + "f1": acc_metrics.get("f1", 0), + "avg_inference_time_ms": perf_metrics.get("avg_inference_time_ms", 0), + "samples_per_second": perf_metrics.get("samples_per_second", 0), + "model_size_mb": perf_metrics.get("model_size_mb", 0), + } + comparison_data.append(model_data) + + comparison_df = pd.DataFrame(comparison_data) + + # Save comparison table + comparison_path = os.path.join(output_dir, "model_comparison.csv") + comparison_df.to_csv(comparison_path, index=False) + + # Create summary table for markdown + markdown_table = "# Model Comparison\n\n" + markdown_table += "| Model | Accuracy | F1 Score | Precision | Recall | Inference Time | Samples/sec | Size (MB) |\n" + markdown_table += "|-------|----------|----------|-----------|--------|---------------|-------------|----------|\n" + + for _, row in comparison_df.iterrows(): + markdown_table += f"| {row['model_name']} | {row['accuracy']:.4f} | {row['f1']:.4f} | {row['precision']:.4f} | {row['recall']:.4f} | {row['avg_inference_time_ms']:.2f} ms | {row['samples_per_second']:.1f} | {row['model_size_mb']:.1f} |\n" + + markdown_path = os.path.join(output_dir, "model_comparison.md") + with open(markdown_path, "w") as f: + f.write(markdown_table) + + logger.info(f"\nComparison results saved to {output_dir}") + + # Log to MLflow if active + try: + mlflow.log_artifact(comparison_path) + mlflow.log_artifact(markdown_path) + except Exception as e: + # If MLflow is not set up or fails, we log a warning + logger.warning(f"Warning: Failed to log comparison results to MLflow: {str(e)}") + + return comparison_df + + +def main(): + parser = argparse.ArgumentParser( + description="Evaluate transformer-based text classifiers" + ) + + # Single model evaluation + parser.add_argument("--model_path", type=str, help="Path to the model directory") + parser.add_argument("--model_name", type=str, help="Name of the model") + parser.add_argument("--test_data", type=str, help="Path to the test data") + parser.add_argument( + "--output_dir", + type=str, + default="evaluation_results", + help="Directory to save results", + ) + parser.add_argument( + "--mlflow_run_id", type=str, help="MLflow run ID to log results to" + ) + + # Multiple model comparison + parser.add_argument( + "--compare", action="store_true", help="Compare multiple models" + ) + parser.add_argument( + "--model_paths", + type=str, + nargs="+", + help="Paths to model directories for comparison", + ) + parser.add_argument( + "--model_names", type=str, nargs="+", help="Names of models for comparison" + ) + + # Performance evaluation options + parser.add_argument( + "--num_runs", + type=int, + default=100, + help="Number of inference runs to average over", + ) + parser.add_argument( + "--seed", type=int, default=42, help="Random seed for reproducibility" + ) + + args = parser.parse_args() + + # Set random seeds + set_random_seeds(args.seed) + + if args.compare: + # Validate arguments for comparison + if ( + not args.model_paths + or not args.model_names + or len(args.model_paths) != len(args.model_names) + ): + parser.error( + "For comparison, --model_paths and --model_names must be provided with equal length" + ) + + # Evaluate and compare models + evaluate_and_compare_models( + args.model_paths, args.model_names, args.test_data, args.output_dir + ) + else: + # Validate arguments for single model evaluation + if not args.model_path or not args.model_name or not args.test_data: + parser.error( + "--model_path, --model_name, and --test_data are required for single model evaluation" + ) + + # Set up device + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + # Load model and tokenizer + model, tokenizer = load_model_and_tokenizer(args.model_path, device) + + # Load test data + dataloader, _, _ = load_test_data(args.test_data, tokenizer) + + # Create output directory + os.makedirs(args.output_dir, exist_ok=True) + + # Evaluate model accuracy + predictions, true_labels, probabilities = evaluate_model( + model, dataloader, device + ) + + # Compute accuracy metrics + accuracy_metrics, _ = compute_metrics(true_labels, predictions, probabilities) + + # Create evaluation plots + create_evaluation_plots( + predictions, true_labels, probabilities, args.model_name, args.output_dir + ) + + # Measure inference performance + performance_metrics = measure_inference_performance( + model, dataloader, device, num_runs=args.num_runs + ) + + # Print results + logger.info("\nAccuracy Metrics:") + for k, v in accuracy_metrics.items(): + if k != "confusion_matrix": + print(f" {k}: {v}") + + logger.info("\nPerformance Metrics:") + for k, v in performance_metrics.items(): + if not isinstance(v, dict): + print(f" {k}: {v}") + + # Log to files and MLflow + log_evaluation_results( + accuracy_metrics, + performance_metrics, + args.model_name, + args.output_dir, + args.mlflow_run_id, + ) + + +if __name__ == "__main__": + main() diff --git a/experiments/base_model_training/scripts/inference.py b/experiments/base_model_training/scripts/inference.py new file mode 100644 index 00000000..113f26fd --- /dev/null +++ b/experiments/base_model_training/scripts/inference.py @@ -0,0 +1,983 @@ +import os +import sys +import argparse +import json +import time +import torch +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +from loguru import logger +import mlflow +import psutil +import platform + +# Import utilities +from utils import set_random_seeds, measure_model_size +import gc +from transformers import ( + AutoModelForSequenceClassification, + AutoTokenizer, +) + +logger.remove() +# add stout handler +logger.add(sys.stdout, format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}") + + +def get_system_info(): + """ + Get information about the system hardware. + + Returns: + dict: Dictionary of system information + """ + system_info = { + "platform": platform.platform(), + "processor": platform.processor(), + "python_version": platform.python_version(), + "torch_version": torch.__version__, + "cuda_available": torch.cuda.is_available(), + "cpu_count": os.cpu_count(), + "memory_total_gb": psutil.virtual_memory().total / (1024**3), + } + + if torch.cuda.is_available(): + system_info.update( + { + "cuda_version": torch.version.cuda, + "gpu_name": torch.cuda.get_device_name(0), + "gpu_count": torch.cuda.device_count(), + "gpu_memory_gb": torch.cuda.get_device_properties(0).total_memory + / (1024**3), + } + ) + + return system_info + + +def create_dummy_input(tokenizer, batch_size, seq_length, device): + """ + Create a dummy input batch for benchmarking. + + Args: + tokenizer: The tokenizer to use + batch_size: Number of examples in the batch + seq_length: Sequence length for each example + device: Device to put the tensor on + + Returns: + dict: Dictionary with input_ids and attention_mask + """ + # Create a dummy text sequence + text = " ".join(["word"] * (seq_length // 2)) + + # Tokenize it + sample_encoding = tokenizer( + text, + padding="max_length", + truncation=True, + max_length=seq_length, + return_tensors="pt", + ) + + # Expand to the desired batch size + input_ids = sample_encoding["input_ids"].repeat(batch_size, 1).to(device) + attention_mask = sample_encoding["attention_mask"].repeat(batch_size, 1).to(device) + + return {"input_ids": input_ids, "attention_mask": attention_mask} + + +def benchmark_inference_time(model, inputs, num_runs=100, warm_up=10): + """ + Benchmark the inference time for a model. + + Args: + model: The model to benchmark + inputs: Input tensors for the model + num_runs: Number of runs to average over + warm_up: Number of warm-up runs + + Returns: + dict: Dictionary of timing statistics + """ + model.eval() + timings = [] + + # Warm-up runs + with torch.no_grad(): + for _ in range(warm_up): + _ = model(**inputs) + + # Timed runs + with torch.no_grad(): + for _ in range(num_runs): + start_time = time.time() + _ = model(**inputs) + # Make sure GPU operations are completed + if torch.cuda.is_available(): + torch.cuda.synchronize() + end_time = time.time() + timings.append(end_time - start_time) + + # Calculate statistics + timings_ms = np.array(timings) * 1000 + + stats = { + "avg_time_ms": np.mean(timings_ms), + "min_time_ms": np.min(timings_ms), + "max_time_ms": np.max(timings_ms), + "median_time_ms": np.median(timings_ms), + "std_time_ms": np.std(timings_ms), + "p90_time_ms": np.percentile(timings_ms, 90), + "p95_time_ms": np.percentile(timings_ms, 95), + "p99_time_ms": np.percentile(timings_ms, 99), + } + + # Calculate throughput (samples per second) + batch_size = inputs["input_ids"].shape[0] + stats["samples_per_second"] = batch_size * 1000 / stats["avg_time_ms"] + + return stats + + +def benchmark_memory_usage(model, inputs): + """ + Benchmark the memory usage for a model. + + Args: + model: The model to benchmark + inputs: Input tensors for the model + + Returns: + dict: Dictionary of memory usage statistics + """ + # Force garbage collection + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + # Measure memory before + if torch.cuda.is_available(): + torch.cuda.reset_peak_memory_stats() + memory_before = torch.cuda.memory_allocated() / (1024**2) + else: + memory_before = psutil.Process(os.getpid()).memory_info().rss / (1024**2) + + # Run inference + model.eval() + with torch.no_grad(): + _ = model(**inputs) + # Make sure GPU operations are completed + if torch.cuda.is_available(): + torch.cuda.synchronize() + + # Measure memory after + if torch.cuda.is_available(): + memory_peak = torch.cuda.max_memory_allocated() / (1024**2) + memory_current = torch.cuda.memory_allocated() / (1024**2) + memory_usage = memory_peak - memory_before + else: + memory_current = psutil.Process(os.getpid()).memory_info().rss / (1024**2) + memory_usage = memory_current - memory_before + memory_peak = memory_current + + return { + "memory_before_mb": memory_before, + "memory_peak_mb": memory_peak, + "memory_current_mb": memory_current, + "memory_usage_mb": memory_usage, + } + + +def benchmark_model( + model_path, device, batch_sizes, seq_lengths, num_runs=100, warm_up=10 +): + """ + Run comprehensive benchmarks for a model. + """ + logger.info(f"Loading model from {model_path}...") + + # Load model and tokenizer + model = AutoModelForSequenceClassification.from_pretrained(model_path) + tokenizer = AutoTokenizer.from_pretrained(model_path) + + model.to(device) + model.eval() + + # Measure model size + model_size_mb = measure_model_size(model) + logger.info(f"Model size: {model_size_mb:.2f} MB") + + # Run benchmarks for different batch sizes and sequence lengths + results = { + "model_path": model_path, + "device": str(device), + "model_size_mb": model_size_mb, + "benchmarks": {}, + } + + for batch_size in batch_sizes: + for seq_length in seq_lengths: + logger.info( + f"Benchmarking batch_size={batch_size}, seq_length={seq_length}..." + ) + + # Create dummy inputs + inputs = create_dummy_input(tokenizer, batch_size, seq_length, device) + + # Benchmark timing + timing_stats = benchmark_inference_time( + model, inputs, num_runs=num_runs, warm_up=warm_up + ) + + # Benchmark memory + memory_stats = benchmark_memory_usage(model, inputs) + + # Store results + key = f"batch_{batch_size}_seq_{seq_length}" + results["benchmarks"][key] = { + "batch_size": batch_size, + "seq_length": seq_length, + "timing": timing_stats, + "memory": memory_stats, + } + + return results + + +def compare_batch_throughput(results, output_dir): + """ + Create comparison plots for throughput vs batch size. + + Args: + results: Dictionary of benchmark results + output_dir: Directory to save plots + """ + os.makedirs(output_dir, exist_ok=True) + + # Extract batch sizes and sequence lengths + batch_sizes = sorted( + list({result["batch_size"] for result in results["benchmarks"].values()}) + ) + seq_lengths = sorted( + list({result["seq_length"] for result in results["benchmarks"].values()}) + ) + + # Create plots for each sequence length + for seq_length in seq_lengths: + data = [] + + for batch_size in batch_sizes: + key = f"batch_{batch_size}_seq_{seq_length}" + if key in results["benchmarks"]: + throughput = results["benchmarks"][key]["timing"]["samples_per_second"] + latency = results["benchmarks"][key]["timing"]["avg_time_ms"] + data.append( + { + "batch_size": batch_size, + "throughput": throughput, + "latency": latency, + } + ) + + if not data: + continue + + df = pd.DataFrame(data) + + # Create throughput plot + plt.figure(figsize=(10, 6)) + plt.plot(df["batch_size"], df["throughput"], marker="o") + plt.xlabel("Batch Size") + plt.ylabel("Throughput (samples/second)") + plt.title(f"Throughput vs Batch Size (Sequence Length = {seq_length})") + plt.grid(True) + plt.tight_layout() + + throughput_path = os.path.join(output_dir, f"throughput_seq_{seq_length}.png") + plt.savefig(throughput_path) + plt.close() + + # Create latency plot + plt.figure(figsize=(10, 6)) + plt.plot(df["batch_size"], df["latency"], marker="o") + plt.xlabel("Batch Size") + plt.ylabel("Latency (ms)") + plt.title(f"Latency vs Batch Size (Sequence Length = {seq_length})") + plt.grid(True) + plt.tight_layout() + + latency_path = os.path.join(output_dir, f"latency_seq_{seq_length}.png") + plt.savefig(latency_path) + plt.close() + + # Log to MLflow if active + try: + mlflow.log_artifact(throughput_path) + mlflow.log_artifact(latency_path) + except Exception as e: + logger.error( + f"Failed to log throughput/latency plots for seq {seq_length}: {e}" + ) + + +def compare_sequence_length_impact(results, output_dir): + """ + Create comparison plots for latency vs sequence length. + + Args: + results: Dictionary of benchmark results + output_dir: Directory to save plots + """ + os.makedirs(output_dir, exist_ok=True) + + # Extract batch sizes and sequence lengths + batch_sizes = sorted( + list({result["batch_size"] for result in results["benchmarks"].values()}) + ) + seq_lengths = sorted( + list({result["seq_length"] for result in results["benchmarks"].values()}) + ) + + # Create plots for each batch size + for batch_size in batch_sizes: + data = [] + + for seq_length in seq_lengths: + key = f"batch_{batch_size}_seq_{seq_length}" + if key in results["benchmarks"]: + latency = results["benchmarks"][key]["timing"]["avg_time_ms"] + data.append({"seq_length": seq_length, "latency": latency}) + + if not data: + continue + + df = pd.DataFrame(data) + + # Create latency plot + plt.figure(figsize=(10, 6)) + plt.plot(df["seq_length"], df["latency"], marker="o") + plt.xlabel("Sequence Length") + plt.ylabel("Latency (ms)") + plt.title(f"Latency vs Sequence Length (Batch Size = {batch_size})") + plt.grid(True) + plt.tight_layout() + + latency_path = os.path.join(output_dir, f"seq_latency_batch_{batch_size}.png") + plt.savefig(latency_path) + plt.close() + + # Log to MLflow if active + try: + mlflow.log_artifact(latency_path) + except Exception as e: + logger.error(f"Failed to log latency plot for batch size {batch_size}: {e}") + + +def create_heatmap(results, output_dir): + """ + Create a heatmap of latency across batch sizes and sequence lengths. + + Args: + results: Dictionary of benchmark results + output_dir: Directory to save plots + """ + os.makedirs(output_dir, exist_ok=True) + + # Extract batch sizes and sequence lengths + batch_sizes = sorted( + list({result["batch_size"] for result in results["benchmarks"].values()}) + ) + seq_lengths = sorted( + list({result["seq_length"] for result in results["benchmarks"].values()}) + ) + + # Create data for heatmap + latency_matrix = np.zeros((len(batch_sizes), len(seq_lengths))) + throughput_matrix = np.zeros((len(batch_sizes), len(seq_lengths))) + + for i, batch_size in enumerate(batch_sizes): + for j, seq_length in enumerate(seq_lengths): + key = f"batch_{batch_size}_seq_{seq_length}" + if key in results["benchmarks"]: + latency_matrix[i, j] = results["benchmarks"][key]["timing"][ + "avg_time_ms" + ] + throughput_matrix[i, j] = results["benchmarks"][key]["timing"][ + "samples_per_second" + ] + + # Create latency heatmap + plt.figure(figsize=(12, 8)) + sns.heatmap( + latency_matrix, + annot=True, + fmt=".1f", + xticklabels=seq_lengths, + yticklabels=batch_sizes, + cmap="YlOrRd", + ) + plt.xlabel("Sequence Length") + plt.ylabel("Batch Size") + plt.title("Inference Latency (ms)") + plt.tight_layout() + + latency_heatmap_path = os.path.join(output_dir, "latency_heatmap.png") + plt.savefig(latency_heatmap_path) + plt.close() + + # Create throughput heatmap + plt.figure(figsize=(12, 8)) + sns.heatmap( + throughput_matrix, + annot=True, + fmt=".1f", + xticklabels=seq_lengths, + yticklabels=batch_sizes, + cmap="viridis", + ) + plt.xlabel("Sequence Length") + plt.ylabel("Batch Size") + plt.title("Throughput (samples/second)") + plt.tight_layout() + + throughput_heatmap_path = os.path.join(output_dir, "throughput_heatmap.png") + plt.savefig(throughput_heatmap_path) + plt.close() + + # Log to MLflow if active + try: + mlflow.log_artifact(latency_heatmap_path) + mlflow.log_artifact(throughput_heatmap_path) + except Exception as e: + logger.error(f"Failed to log heatmap plots to MLflow: {e}") + + +def create_summary_report(results, system_info, output_dir): + """ + Create a markdown summary report of the benchmarks. + + Args: + results: Dictionary of benchmark results + system_info: Dictionary of system information + output_dir: Directory to save the report + """ + os.makedirs(output_dir, exist_ok=True) + + model_path = results["model_path"] + model_name = os.path.basename(model_path) + + # Create report content + report = f"# Inference Performance Report for {model_name}\n\n" + + # System information + report += "## System Information\n\n" + report += "| Component | Details |\n" + report += "|-----------|--------|\n" + for key, value in system_info.items(): + if isinstance(value, float): + value = f"{value:.2f}" + report += f"| {key} | {value} |\n" + + report += "\n## Model Information\n\n" + report += f"- Model path: {model_path}\n" + report += f"- Model size: {results['model_size_mb']:.2f} MB\n" + report += f"- Device: {results['device']}\n\n" + + # Create a summary table for batch size = 1 + report += "## Inference Latency (Batch Size = 1)\n\n" + report += "| Sequence Length | Latency (ms) | Throughput (samples/sec) |\n" + report += "|-----------------|-------------|---------------------------|\n" + + seq_lengths = sorted( + list({result["seq_length"] for result in results["benchmarks"].values()}) + ) + + for seq_length in seq_lengths: + key = f"batch_1_seq_{seq_length}" + if key in results["benchmarks"]: + latency = results["benchmarks"][key]["timing"]["avg_time_ms"] + throughput = results["benchmarks"][key]["timing"]["samples_per_second"] + report += f"| {seq_length} | {latency:.2f} | {throughput:.2f} |\n" + + # Create a table for batch effects on typical length + mid_seq = seq_lengths[len(seq_lengths) // 2] if seq_lengths else 128 + + report += f"\n## Batch Size Impact (Sequence Length = {mid_seq})\n\n" + report += "| Batch Size | Latency (ms) | Throughput (samples/sec) |\n" + report += "|------------|-------------|---------------------------|\n" + + batch_sizes = sorted( + list({result["batch_size"] for result in results["benchmarks"].values()}) + ) + + for batch_size in batch_sizes: + key = f"batch_{batch_size}_seq_{mid_seq}" + if key in results["benchmarks"]: + latency = results["benchmarks"][key]["timing"]["avg_time_ms"] + throughput = results["benchmarks"][key]["timing"]["samples_per_second"] + report += f"| {batch_size} | {latency:.2f} | {throughput:.2f} |\n" + + # Memory usage + report += "\n## Memory Usage\n\n" + report += "| Batch Size | Sequence Length | Memory Usage (MB) |\n" + report += "|------------|-----------------|-------------------|\n" + + for batch_size in batch_sizes: + for seq_length in seq_lengths: + key = f"batch_{batch_size}_seq_{seq_length}" + if key in results["benchmarks"]: + memory = results["benchmarks"][key]["memory"]["memory_usage_mb"] + report += f"| {batch_size} | {seq_length} | {memory:.2f} |\n" + + # Detailed timing statistics for a typical case + key = f"batch_1_seq_{mid_seq}" + if key in results["benchmarks"]: + report += ( + "\n## Detailed Timing Statistics (Batch Size = 1, Sequence Length = " + + str(mid_seq) + + ")\n\n" + ) + report += "| Metric | Value |\n" + report += "|--------|-------|\n" + + timing = results["benchmarks"][key]["timing"] + for k, v in timing.items(): + if k != "samples_per_second": + report += f"| {k} | {v:.2f} ms |\n" + + # Write the report + report_path = os.path.join(output_dir, f"{model_name}_inference_report.md") + with open(report_path, "w") as f: + f.write(report) + + logger.info(f"Report saved to {report_path}") + + # Log to MLflow if active + try: + mlflow.log_artifact(report_path) + except Exception as e: + logger.error(f"Failed to log report to MLflow: {e}") + + +def compare_models(model_results, output_dir): + """ + Compare multiple models in terms of inference performance. + + Args: + model_results: List of benchmark results for different models + output_dir: Directory to save comparison results + """ + os.makedirs(output_dir, exist_ok=True) + + model_names = [os.path.basename(results["model_path"]) for results in model_results] + + # Extract batch sizes and sequence lengths + batch_sizes = set() + seq_lengths = set() + + for results in model_results: + for result in results["benchmarks"].values(): + batch_sizes.add(result["batch_size"]) + seq_lengths.add(result["seq_length"]) + + batch_sizes = sorted(list(batch_sizes)) + seq_lengths = sorted(list(seq_lengths)) + + # Compare latency for batch size = 1 across different sequence lengths + data = [] + + for model_idx, results in enumerate(model_results): + for seq_length in seq_lengths: + key = f"batch_1_seq_{seq_length}" + if key in results["benchmarks"]: + latency = results["benchmarks"][key]["timing"]["avg_time_ms"] + throughput = results["benchmarks"][key]["timing"]["samples_per_second"] + + data.append( + { + "model": model_names[model_idx], + "seq_length": seq_length, + "latency": latency, + "throughput": throughput, + } + ) + + if data: + df = pd.DataFrame(data) + + # Plot latency comparison + plt.figure(figsize=(12, 6)) + sns.lineplot(data=df, x="seq_length", y="latency", hue="model", marker="o") + plt.xlabel("Sequence Length") + plt.ylabel("Latency (ms)") + plt.title("Inference Latency Comparison (Batch Size = 1)") + plt.grid(True) + plt.tight_layout() + + latency_comp_path = os.path.join(output_dir, "model_latency_comparison.png") + plt.savefig(latency_comp_path) + plt.close() + + # Plot throughput comparison + plt.figure(figsize=(12, 6)) + sns.lineplot(data=df, x="seq_length", y="throughput", hue="model", marker="o") + plt.xlabel("Sequence Length") + plt.ylabel("Throughput (samples/second)") + plt.title("Inference Throughput Comparison (Batch Size = 1)") + plt.grid(True) + plt.tight_layout() + + throughput_comp_path = os.path.join( + output_dir, "model_throughput_comparison.png" + ) + plt.savefig(throughput_comp_path) + plt.close() + + # Compare latency for different batch sizes with a fixed sequence length + mid_seq = seq_lengths[len(seq_lengths) // 2] if seq_lengths else 128 + data = [] + + for model_idx, results in enumerate(model_results): + for batch_size in batch_sizes: + key = f"batch_{batch_size}_seq_{mid_seq}" + if key in results["benchmarks"]: + latency = results["benchmarks"][key]["timing"]["avg_time_ms"] + throughput = results["benchmarks"][key]["timing"]["samples_per_second"] + + data.append( + { + "model": model_names[model_idx], + "batch_size": batch_size, + "latency": latency, + "throughput": throughput, + } + ) + + if data: + df = pd.DataFrame(data) + + # Plot latency comparison for different batch sizes + plt.figure(figsize=(12, 6)) + sns.lineplot(data=df, x="batch_size", y="latency", hue="model", marker="o") + plt.xlabel("Batch Size") + plt.ylabel("Latency (ms)") + plt.title(f"Inference Latency Comparison (Sequence Length = {mid_seq})") + plt.grid(True) + plt.tight_layout() + + batch_latency_path = os.path.join(output_dir, "batch_latency_comparison.png") + plt.savefig(batch_latency_path) + plt.close() + + # Plot throughput comparison for different batch sizes + plt.figure(figsize=(12, 6)) + sns.lineplot(data=df, x="batch_size", y="throughput", hue="model", marker="o") + plt.xlabel("Batch Size") + plt.ylabel("Throughput (samples/second)") + plt.title(f"Inference Throughput Comparison (Sequence Length = {mid_seq})") + plt.grid(True) + plt.tight_layout() + + batch_throughput_path = os.path.join( + output_dir, "batch_throughput_comparison.png" + ) + plt.savefig(batch_throughput_path) + plt.close() + + # Create a comparison table + comparison_table = "# Model Inference Performance Comparison\n\n" + + # Model size comparison + comparison_table += "## Model Size\n\n" + comparison_table += "| Model | Size (MB) |\n" + comparison_table += "|-------|----------|\n" + + for model_idx, results in enumerate(model_results): + comparison_table += ( + f"| {model_names[model_idx]} | {results['model_size_mb']:.2f} |\n" + ) + + # Latency comparison for batch=1, seq=128 + comparison_table += "\n## Single Sample Inference Latency (ms)\n\n" + comparison_table += "| Model | " + for seq_length in seq_lengths: + comparison_table += f"Seq={seq_length} | " + comparison_table += "\n|-------|" + for _ in seq_lengths: + comparison_table += "-------|" + comparison_table += "\n" + + for model_idx, results in enumerate(model_results): + comparison_table += f"| {model_names[model_idx]} | " + for seq_length in seq_lengths: + key = f"batch_1_seq_{seq_length}" + if key in results["benchmarks"]: + latency = results["benchmarks"][key]["timing"]["avg_time_ms"] + comparison_table += f"{latency:.2f} | " + else: + comparison_table += "N/A | " + comparison_table += "\n" + + # Throughput comparison for batch=32, seq=128 + comparison_table += "\n## Throughput (samples/second) with Batch Size = 32\n\n" + comparison_table += "| Model | " + for seq_length in seq_lengths: + comparison_table += f"Seq={seq_length} | " + comparison_table += "\n|-------|" + for _ in seq_lengths: + comparison_table += "-------|" + comparison_table += "\n" + + for model_idx, results in enumerate(model_results): + comparison_table += f"| {model_names[model_idx]} | " + for seq_length in seq_lengths: + key = f"batch_32_seq_{seq_length}" + if key in results["benchmarks"]: + throughput = results["benchmarks"][key]["timing"]["samples_per_second"] + comparison_table += f"{throughput:.2f} | " + else: + comparison_table += "N/A | " + comparison_table += "\n" + + # Write the comparison table + comparison_path = os.path.join(output_dir, "model_comparison.md") + with open(comparison_path, "w") as f: + f.write(comparison_table) + + logger.info(f"Comparison saved to {comparison_path}") + + # Log to MLflow if active + try: + mlflow.log_artifact(latency_comp_path) + mlflow.log_artifact(throughput_comp_path) + mlflow.log_artifact(batch_latency_path) + mlflow.log_artifact(batch_throughput_path) + mlflow.log_artifact(comparison_path) + except Exception as e: + logger.error(f"Failed to log comparison plots to MLflow: {e}") + + +def main(): + parser = argparse.ArgumentParser( + description="Measure inference performance for transformer models" + ) + + # Single model benchmark + parser.add_argument("--model_path", type=str, help="Path to the model") + parser.add_argument( + "--output_dir", + type=str, + default="inference_results", + help="Directory to save results", + ) + parser.add_argument( + "--mlflow_run_id", type=str, help="MLflow run ID to log results to" + ) + + # Multiple model comparison + parser.add_argument( + "--compare", action="store_true", help="Compare multiple models" + ) + parser.add_argument( + "--model_paths", type=str, nargs="+", help="Paths to models for comparison" + ) + + # Benchmark parameters + parser.add_argument( + "--batch_sizes", + type=int, + nargs="+", + default=[1, 2, 4, 8, 16, 32, 64, 128], + help="Batch sizes to benchmark", + ) + parser.add_argument( + "--seq_lengths", + type=int, + nargs="+", + default=[32, 64, 128, 256, 512], + help="Sequence lengths to benchmark", + ) + parser.add_argument( + "--num_runs", type=int, default=100, help="Number of runs to average over" + ) + parser.add_argument( + "--warm_up", type=int, default=10, help="Number of warm-up runs" + ) + parser.add_argument( + "--seed", type=int, default=42, help="Random seed for reproducibility" + ) + parser.add_argument( + "--cpu_only", + action="store_true", + help="Force CPU inference even if CUDA is available", + ) + + args = parser.parse_args() + + # Set random seeds + set_random_seeds(args.seed) + + # Set up device + if args.cpu_only: + device = torch.device("cpu") + else: + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + logger.info(f"Using device: {device}") + + # Get system information + system_info = get_system_info() + logger.info("System information:") + for key, value in system_info.items(): + logger.info(f" {key}: {value}") + + if args.compare: + # Validate arguments for comparison + if not args.model_paths: + parser.error("--model_paths must be provided for comparison") + + # Benchmark multiple models + model_results = [] + + for model_path in args.model_paths: + logger.info(f"\nBenchmarking model: {model_path}") + model_output_dir = os.path.join( + args.output_dir, os.path.basename(model_path) + ) + os.makedirs(model_output_dir, exist_ok=True) + + results = benchmark_model( + model_path, + device, + args.batch_sizes, + args.seq_lengths, + args.num_runs, + args.warm_up, + ) + + # Save results to file + results_path = os.path.join(model_output_dir, "benchmark_results.json") + with open(results_path, "w") as f: + # Convert any non-serializable values + def convert_for_json(obj): + if isinstance( + obj, + ( + np.int_, + np.intc, + np.intp, + np.int8, + np.int16, + np.int32, + np.int64, + ), + ): + return int(obj) + elif isinstance( + obj, (np.float_, np.float16, np.float32, np.float64) + ): + return float(obj) + elif isinstance(obj, (np.ndarray,)): + return obj.tolist() + elif isinstance(obj, dict): + return {k: convert_for_json(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [convert_for_json(i) for i in obj] + else: + return obj + + json.dump(convert_for_json(results), f, indent=2) + + # Create individual model plots + compare_batch_throughput(results, model_output_dir) + compare_sequence_length_impact(results, model_output_dir) + create_heatmap(results, model_output_dir) + + # Create summary report + create_summary_report(results, system_info, model_output_dir) + + model_results.append(results) + + # Compare models + compare_models(model_results, args.output_dir) + + else: + # Validate arguments for single model + if not args.model_path: + parser.error("--model_path is required for single model benchmarking") + + # Benchmark single model + results = benchmark_model( + args.model_path, + device, + args.batch_sizes, + args.seq_lengths, + args.num_runs, + args.warm_up, + ) + + # Save results to file + os.makedirs(args.output_dir, exist_ok=True) + results_path = os.path.join(args.output_dir, "benchmark_results.json") + with open(results_path, "w") as f: + # Convert any non-serializable values + def convert_for_json(obj): + if isinstance( + obj, + (np.int_, np.intc, np.intp, np.int8, np.int16, np.int32, np.int64), + ): + return int(obj) + elif isinstance(obj, (np.float_, np.float16, np.float32, np.float64)): + return float(obj) + elif isinstance(obj, (np.ndarray,)): + return obj.tolist() + elif isinstance(obj, dict): + return {k: convert_for_json(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [convert_for_json(i) for i in obj] + else: + return obj + + json.dump(convert_for_json(results), f, indent=2) + + # Create plots + compare_batch_throughput(results, args.output_dir) + compare_sequence_length_impact(results, args.output_dir) + create_heatmap(results, args.output_dir) + + # Create summary report + create_summary_report(results, system_info, args.output_dir) + + # Log to MLflow if run ID is provided + if args.mlflow_run_id: + try: + mlflow.set_tracking_uri(os.environ.get("MLFLOW_TRACKING_URI", "mlruns")) + with mlflow.start_run(run_id=args.mlflow_run_id): + # Log the benchmark results file + mlflow.log_artifact(results_path) + + # Log key metrics + key = "batch_1_seq_128" + if key in results["benchmarks"]: + mlflow.log_metric( + "inference_latency_ms", + results["benchmarks"][key]["timing"]["avg_time_ms"], + ) + mlflow.log_metric( + "inference_throughput", + results["benchmarks"][key]["timing"]["samples_per_second"], + ) + + mlflow.log_metric("model_size_mb", results["model_size_mb"]) + + # Log system info + mlflow.log_dict(system_info, "system_info.json") + except Exception as e: + logger.warning(f"Warning: Failed to log to MLflow: {str(e)}") + + +if __name__ == "__main__": + main() diff --git a/experiments/base_model_training/scripts/mlflow_log.py b/experiments/base_model_training/scripts/mlflow_log.py new file mode 100644 index 00000000..bab9bacb --- /dev/null +++ b/experiments/base_model_training/scripts/mlflow_log.py @@ -0,0 +1,766 @@ +import os +import argparse +import json +import glob +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +from datetime import datetime +import mlflow +from mlflow.tracking import MlflowClient + + +def setup_mlflow(tracking_uri, experiment_name): + """ + Set up MLflow tracking and create or get the experiment. + + Args: + tracking_uri (str): URI for MLflow tracking server + experiment_name (str): Name of the experiment + + Returns: + str: Experiment ID + """ + mlflow.set_tracking_uri(tracking_uri) + + # Get or create the experiment + experiment = mlflow.get_experiment_by_name(experiment_name) + if experiment: + experiment_id = experiment.experiment_id + else: + experiment_id = mlflow.create_experiment(experiment_name) + + print(f"MLflow experiment '{experiment_name}' (ID: {experiment_id}) is ready") + return experiment_id + + +def log_model_training(model_dir, metrics_file, artifacts_dir=None, tags=None): + """ + Log model training metrics and artifacts to MLflow. + + Args: + model_dir (str): Directory containing the model files + metrics_file (str): Path to the metrics JSON file + artifacts_dir (str, optional): Directory containing additional artifacts + tags (dict, optional): Tags to add to the run + + Returns: + str: MLflow run ID + """ + # Load metrics + with open(metrics_file, "r") as f: + metrics_data = json.load(f) + + model_type = metrics_data.get("model_type", os.path.basename(model_dir)) + + # Start MLflow run + with mlflow.start_run( + run_name=f"{model_type}_training_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + ) as run: + run_id = run.info.run_id + + # Log parameters + params = { + k: v + for k, v in metrics_data.items() + if not isinstance(v, (dict, list)) and k != "timestamp" + } + mlflow.log_params(params) + + # Log metrics + if "test_metrics" in metrics_data: + for k, v in metrics_data["test_metrics"].items(): + if not isinstance(v, (dict, list, np.ndarray)): + mlflow.log_metric(f"test_{k}", v) + + # Log training time if available + if "training_time_seconds" in metrics_data: + mlflow.log_metric( + "training_time_seconds", metrics_data["training_time_seconds"] + ) + + # Log inference time if available + if "avg_inference_time_seconds" in metrics_data: + mlflow.log_metric( + "avg_inference_time_seconds", metrics_data["avg_inference_time_seconds"] + ) + + # Log the model if available + model_files = glob.glob(os.path.join(model_dir, "*.bin")) + glob.glob( + os.path.join(model_dir, "*.pt") + ) + if model_files: + mlflow.log_artifact(model_dir, "model") + + # Log the metrics file itself + mlflow.log_artifact(metrics_file, "metrics") + + # Log additional artifacts if provided + if artifacts_dir and os.path.exists(artifacts_dir): + for artifact_file in glob.glob(os.path.join(artifacts_dir, "*")): + if os.path.isfile(artifact_file): + mlflow.log_artifact(artifact_file, "artifacts") + + # Add tags if provided + if tags: + mlflow.set_tags(tags) + + # Add default tags + mlflow.set_tag("model_type", model_type) + mlflow.set_tag("stage", "training") + + print(f"Training metrics logged to MLflow run: {run_id}") + return run_id + + +def log_model_evaluation(evaluation_file, run_id=None, artifacts_dir=None, tags=None): + """ + Log model evaluation metrics to MLflow. + + Args: + evaluation_file (str): Path to the evaluation results JSON file + run_id (str, optional): Existing MLflow run ID to log to + artifacts_dir (str, optional): Directory containing additional artifacts + tags (dict, optional): Tags to add to the run + + Returns: + str: MLflow run ID + """ + # Load evaluation results + with open(evaluation_file, "r") as f: + eval_data = json.load(f) + + model_name = eval_data.get( + "model_name", os.path.basename(os.path.dirname(evaluation_file)) + ) + + # Determine whether to create a new run or use an existing one + if run_id: + # Check if the run exists + try: + client = MlflowClient() + client.get_run(run_id) + new_run = False + except Exception as e: + print(f"Error checking run ID {run_id}: {e}") + # If run ID does not exist, create a new run + print(f"Run ID {run_id} not found. Creating a new run.") + new_run = True + else: + new_run = True + + if new_run: + # Start a new MLflow run + with mlflow.start_run( + run_name=f"{model_name}_evaluation_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + ) as run: + run_id = run.info.run_id + + # Log accuracy metrics + if "accuracy_metrics" in eval_data: + for k, v in eval_data["accuracy_metrics"].items(): + if not isinstance(v, (dict, list, np.ndarray)): + mlflow.log_metric(f"eval_{k}", v) + + # Log performance metrics + if "performance_metrics" in eval_data: + for k, v in eval_data["performance_metrics"].items(): + if not isinstance(v, (dict, list, np.ndarray)): + mlflow.log_metric(k, v) + + # Log the evaluation file itself + mlflow.log_artifact(evaluation_file, "evaluation") + + # Log additional artifacts if provided + if artifacts_dir and os.path.exists(artifacts_dir): + for artifact_file in glob.glob(os.path.join(artifacts_dir, "*")): + if os.path.isfile(artifact_file): + mlflow.log_artifact(artifact_file, "artifacts") + + # Add tags if provided + if tags: + mlflow.set_tags(tags) + + # Add default tags + mlflow.set_tag("model_name", model_name) + mlflow.set_tag("stage", "evaluation") + else: + # Log to existing run + with mlflow.start_run(run_id=run_id): + # Log accuracy metrics + if "accuracy_metrics" in eval_data: + for k, v in eval_data["accuracy_metrics"].items(): + if not isinstance(v, (dict, list, np.ndarray)): + mlflow.log_metric(f"eval_{k}", v) + + # Log performance metrics + if "performance_metrics" in eval_data: + for k, v in eval_data["performance_metrics"].items(): + if not isinstance(v, (dict, list, np.ndarray)): + mlflow.log_metric(k, v) + + # Log the evaluation file itself + mlflow.log_artifact(evaluation_file, "evaluation") + + # Log additional artifacts if provided + if artifacts_dir and os.path.exists(artifacts_dir): + for artifact_file in glob.glob(os.path.join(artifacts_dir, "*")): + if os.path.isfile(artifact_file): + mlflow.log_artifact(artifact_file, "artifacts") + + # Add tags if provided + if tags: + mlflow.set_tags(tags) + + # Update stage tag + mlflow.set_tag("stage", "training+evaluation") + + print(f"Evaluation metrics logged to MLflow run: {run_id}") + return run_id + + +def log_inference_performance( + inference_file, run_id=None, artifacts_dir=None, tags=None +): + """ + Log inference performance metrics to MLflow. + + Args: + inference_file (str): Path to the inference results JSON file + run_id (str, optional): Existing MLflow run ID to log to + artifacts_dir (str, optional): Directory containing additional artifacts + tags (dict, optional): Tags to add to the run + + Returns: + str: MLflow run ID + """ + # Load inference results + with open(inference_file, "r") as f: + inference_data = json.load(f) + + model_name = os.path.basename( + inference_data.get("model_path", os.path.dirname(inference_file)) + ) + + # Determine whether to create a new run or use an existing one + if run_id: + # Check if the run exists + try: + client = MlflowClient() + client.get_run(run_id) + new_run = False + except Exception as e: + print(f"Error checking run ID {run_id}: {e}") + # If run ID does not exist, create a new run + print(f"Run ID {run_id} not found. Creating a new run.") + new_run = True + else: + new_run = True + + if new_run: + # Start a new MLflow run + with mlflow.start_run( + run_name=f"{model_name}_inference_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + ) as run: + run_id = run.info.run_id + + # Log model size + if "model_size_mb" in inference_data: + mlflow.log_metric("model_size_mb", inference_data["model_size_mb"]) + + # Log batch size = 1, seq_length = 128 metrics as standard benchmarks + key = "batch_1_seq_128" + if "benchmarks" in inference_data and key in inference_data["benchmarks"]: + timing = inference_data["benchmarks"][key]["timing"] + mlflow.log_metric("inference_latency_ms", timing["avg_time_ms"]) + mlflow.log_metric("inference_throughput", timing["samples_per_second"]) + + memory = inference_data["benchmarks"][key]["memory"] + mlflow.log_metric("inference_memory_mb", memory["memory_usage_mb"]) + + # Log the inference file itself + mlflow.log_artifact(inference_file, "inference") + + # Log additional artifacts if provided + if artifacts_dir and os.path.exists(artifacts_dir): + for artifact_file in glob.glob(os.path.join(artifacts_dir, "*")): + if os.path.isfile(artifact_file): + mlflow.log_artifact(artifact_file, "artifacts") + + # Add tags if provided + if tags: + mlflow.set_tags(tags) + + # Add default tags + mlflow.set_tag("model_name", model_name) + mlflow.set_tag("stage", "inference") + else: + # Log to existing run + with mlflow.start_run(run_id=run_id): + # Log model size + if "model_size_mb" in inference_data: + mlflow.log_metric("model_size_mb", inference_data["model_size_mb"]) + + # Log batch size = 1, seq_length = 128 metrics as standard benchmarks + key = "batch_1_seq_128" + if "benchmarks" in inference_data and key in inference_data["benchmarks"]: + timing = inference_data["benchmarks"][key]["timing"] + mlflow.log_metric("inference_latency_ms", timing["avg_time_ms"]) + mlflow.log_metric("inference_throughput", timing["samples_per_second"]) + + memory = inference_data["benchmarks"][key]["memory"] + mlflow.log_metric("inference_memory_mb", memory["memory_usage_mb"]) + + # Log the inference file itself + mlflow.log_artifact(inference_file, "inference") + + # Log additional artifacts if provided + if artifacts_dir and os.path.exists(artifacts_dir): + for artifact_file in glob.glob(os.path.join(artifacts_dir, "*")): + if os.path.isfile(artifact_file): + mlflow.log_artifact(artifact_file, "artifacts") + + # Add tags if provided + if tags: + mlflow.set_tags(tags) + + # Update stage tag to indicate this run includes all stages + mlflow.set_tag("stage", "training+evaluation+inference") + + print(f"Inference metrics logged to MLflow run: {run_id}") + return run_id + + +def create_comparison_artifacts(experiment_id, output_dir, model_types=None): + """ + Create and log comparison artifacts for multiple models in an experiment. + + Args: + experiment_id (str): MLflow experiment ID + output_dir (str): Directory to save artifacts + model_types (list, optional): List of model types to compare + """ + os.makedirs(output_dir, exist_ok=True) + + # Get all runs for the experiment + client = MlflowClient() + runs = client.search_runs(experiment_ids=[experiment_id]) + + # Filter runs if model_types is provided + if model_types: + filtered_runs = [] + for run in runs: + if ( + "model_type" in run.data.tags + and run.data.tags["model_type"] in model_types + ): + filtered_runs.append(run) + runs = filtered_runs + + if not runs: + print("No runs found for comparison") + return + + # Extract metrics for comparison + comparison_data = [] + + for run in runs: + run_data = { + "run_id": run.info.run_id, + "model_type": run.data.tags.get("model_type", "unknown"), + "stage": run.data.tags.get("stage", "unknown"), + } + + # Add all metrics + for key, value in run.data.metrics.items(): + run_data[key] = value + + comparison_data.append(run_data) + + # Convert to DataFrame + comparison_df = pd.DataFrame(comparison_data) + + # Save comparison data + comparison_csv = os.path.join(output_dir, "model_comparison.csv") + comparison_df.to_csv(comparison_csv, index=False) + + # Create comparison plots + metrics_to_compare = [ + "test_accuracy", + "test_f1", + "test_precision", + "test_recall", + "avg_inference_time_seconds", + "inference_latency_ms", + "inference_throughput", + "model_size_mb", + "training_time_seconds", + ] + + for metric in metrics_to_compare: + if metric in comparison_df.columns: + plt.figure(figsize=(10, 6)) + + # Filter rows that have this metric + metric_df = comparison_df[comparison_df[metric].notna()] + + if len(metric_df) > 0: + # Create plot + ax = sns.barplot(x="model_type", y=metric, data=metric_df) + + # Add value labels on top of each bar + for i, v in enumerate(metric_df[metric]): + ax.text(i, v, f"{v:.4f}", ha="center", va="bottom") + + plt.title(f"Comparison of {metric}") + plt.tight_layout() + + # Save plot + plot_path = os.path.join(output_dir, f"compare_{metric}.png") + plt.savefig(plot_path) + plt.close() + + # Create radar chart for model comparison if we have at least 2 models + model_groups = comparison_df.groupby("model_type") + if len(model_groups) >= 2: + # Select metrics for radar chart + radar_metrics = [ + m + for m in ["test_accuracy", "test_f1", "test_precision", "test_recall"] + if m in comparison_df.columns + ] + + if radar_metrics: + # Get mean values for each model type and metric + radar_data = model_groups[radar_metrics].mean() + + # Create radar chart + create_radar_chart( + radar_data, os.path.join(output_dir, "model_radar_comparison.png") + ) + + # Create a summary markdown document + create_comparison_markdown(comparison_df, output_dir) + + # Log artifacts to MLflow + with mlflow.start_run(): + for file in glob.glob(os.path.join(output_dir, "*")): + mlflow.log_artifact(file) + + mlflow.set_tag("artifact_type", "model_comparison") + + +def create_radar_chart(data, output_path): + """ + Create a radar chart for model comparison. + + Args: + data (DataFrame): DataFrame with models as rows and metrics as columns + output_path (str): Path to save the radar chart + """ + # Number of metrics + metrics = data.columns.tolist() + num_metrics = len(metrics) + + # Number of models + models = data.index.tolist() + + # Create angles for each metric + angles = np.linspace(0, 2 * np.pi, num_metrics, endpoint=False).tolist() + angles += angles[:1] # Close the polygon + + # Create figure + _, ax = plt.subplots(figsize=(10, 8), subplot_kw=dict(polar=True)) + + # Add each model to the chart + for i, model in enumerate(models): + values = data.loc[model].tolist() + values += values[:1] # Close the polygon + + # Plot values + ax.plot(angles, values, linewidth=2, label=model) + ax.fill(angles, values, alpha=0.1) + + # Add metric labels + ax.set_xticks(angles[:-1]) + ax.set_xticklabels(metrics) + + # Add legend and title + ax.legend(loc="upper right") + plt.title("Model Comparison Radar Chart") + + # Save the chart + plt.tight_layout() + plt.savefig(output_path) + plt.close() + + +def create_comparison_markdown(comparison_df, output_dir): + """ + Create a markdown document summarizing model comparisons. + + Args: + comparison_df (DataFrame): DataFrame with model comparison data + output_dir (str): Directory to save the markdown file + """ + # Filter to get the most complete runs for each model type + best_runs = [] + for model_type, group in comparison_df.groupby("model_type"): + # Find the run with the most metrics + metric_counts = group.notna().sum(axis=1) + best_run_idx = metric_counts.idxmax() + best_runs.append(group.loc[best_run_idx]) + + if not best_runs: + return + + best_runs_df = pd.DataFrame(best_runs) + + # Create markdown content + md_content = "# Model Comparison Summary\n\n" + md_content += "## Performance Metrics\n\n" + + # Create table header + md_content += "| Model |" + + accuracy_metrics = [ + col + for col in best_runs_df.columns + if col.startswith("test_") and col != "test_metrics" + ] + for metric in accuracy_metrics: + md_content += f" {metric} |" + + md_content += "\n|" + " --- |" * (len(accuracy_metrics) + 1) + "\n" + + # Add rows for each model + for _, row in best_runs_df.iterrows(): + md_content += f"| {row['model_type']} |" + + for metric in accuracy_metrics: + if pd.notna(row.get(metric)): + md_content += f" {row[metric]:.4f} |" + else: + md_content += " - |" + + md_content += "\n" + + # Add inference performance section + md_content += "\n## Inference Performance\n\n" + + # Create table header + md_content += "| Model |" + + inference_metrics = [ + "inference_latency_ms", + "inference_throughput", + "model_size_mb", + ] + for metric in inference_metrics: + if metric in best_runs_df.columns: + md_content += f" {metric} |" + + md_content += ( + "\n|" + + " --- |" + * (len([m for m in inference_metrics if m in best_runs_df.columns]) + 1) + + "\n" + ) + + # Add rows for each model + for _, row in best_runs_df.iterrows(): + md_content += f"| {row['model_type']} |" + + for metric in inference_metrics: + if metric in best_runs_df.columns and pd.notna(row.get(metric)): + if metric == "inference_latency_ms": + md_content += f" {row[metric]:.2f} ms |" + elif metric == "inference_throughput": + md_content += f" {row[metric]:.2f} samples/s |" + elif metric == "model_size_mb": + md_content += f" {row[metric]:.2f} MB |" + else: + md_content += f" {row[metric]:.4f} |" + else: + md_content += " - |" + + md_content += "\n" + + # Add training information section if available + if "training_time_seconds" in best_runs_df.columns: + md_content += "\n## Training Information\n\n" + + # Create table header + md_content += "| Model | Training Time |\n" + md_content += "| --- | --- |\n" + + # Add rows for each model + for _, row in best_runs_df.iterrows(): + md_content += f"| {row['model_type']} |" + + if pd.notna(row.get("training_time_seconds")): + # Convert seconds to a readable format + seconds = row["training_time_seconds"] + if seconds < 60: + time_str = f"{seconds:.2f} seconds" + elif seconds < 3600: + minutes = seconds / 60 + time_str = f"{minutes:.2f} minutes" + else: + hours = seconds / 3600 + time_str = f"{hours:.2f} hours" + + md_content += f" {time_str} |\n" + else: + md_content += " - |\n" + + # Write markdown file + md_path = os.path.join(output_dir, "model_comparison_summary.md") + with open(md_path, "w") as f: + f.write(md_content) + + +def main(): + parser = argparse.ArgumentParser(description="Log model experiments to MLflow") + + # MLflow setup + parser.add_argument( + "--tracking_uri", type=str, default="mlruns", help="MLflow tracking URI" + ) + parser.add_argument( + "--experiment_name", + type=str, + default="text_classification", + help="MLflow experiment name", + ) + + # Subparsers for different commands + subparsers = parser.add_subparsers(dest="command", help="Command to execute") + + # Training parser + train_parser = subparsers.add_parser("train", help="Log training metrics") + train_parser.add_argument( + "--model_dir", + type=str, + required=True, + help="Directory containing the model files", + ) + train_parser.add_argument( + "--metrics_file", type=str, required=True, help="Path to the metrics JSON file" + ) + train_parser.add_argument( + "--artifacts_dir", type=str, help="Directory containing additional artifacts" + ) + + # Evaluation parser + eval_parser = subparsers.add_parser("evaluate", help="Log evaluation metrics") + eval_parser.add_argument( + "--evaluation_file", + type=str, + required=True, + help="Path to the evaluation results JSON file", + ) + eval_parser.add_argument( + "--run_id", type=str, help="Existing MLflow run ID to log to" + ) + eval_parser.add_argument( + "--artifacts_dir", type=str, help="Directory containing additional artifacts" + ) + + # Inference parser + inference_parser = subparsers.add_parser("inference", help="Log inference metrics") + inference_parser.add_argument( + "--inference_file", + type=str, + required=True, + help="Path to the inference results JSON file", + ) + inference_parser.add_argument( + "--run_id", type=str, help="Existing MLflow run ID to log to" + ) + inference_parser.add_argument( + "--artifacts_dir", type=str, help="Directory containing additional artifacts" + ) + + # Compare parser + compare_parser = subparsers.add_parser( + "compare", help="Create model comparison artifacts" + ) + compare_parser.add_argument( + "--output_dir", + type=str, + required=True, + help="Directory to save comparison artifacts", + ) + compare_parser.add_argument( + "--model_types", type=str, nargs="+", help="List of model types to compare" + ) + + # Log complete run parser (training + evaluation + inference) + complete_parser = subparsers.add_parser( + "complete", help="Log a complete run with all metrics" + ) + complete_parser.add_argument( + "--model_dir", + type=str, + required=True, + help="Directory containing the model files", + ) + complete_parser.add_argument( + "--training_metrics", + type=str, + required=True, + help="Path to the training metrics JSON file", + ) + complete_parser.add_argument( + "--evaluation_file", + type=str, + required=True, + help="Path to the evaluation results JSON file", + ) + complete_parser.add_argument( + "--inference_file", + type=str, + required=True, + help="Path to the inference results JSON file", + ) + complete_parser.add_argument( + "--artifacts_dir", type=str, help="Directory containing additional artifacts" + ) + + args = parser.parse_args() + + # Setup MLflow + experiment_id = setup_mlflow(args.tracking_uri, args.experiment_name) + + if args.command == "train": + log_model_training(args.model_dir, args.metrics_file, args.artifacts_dir) + + elif args.command == "evaluate": + log_model_evaluation(args.evaluation_file, args.run_id, args.artifacts_dir) + + elif args.command == "inference": + log_inference_performance(args.inference_file, args.run_id, args.artifacts_dir) + + elif args.command == "compare": + create_comparison_artifacts(experiment_id, args.output_dir, args.model_types) + + elif args.command == "complete": + # Start with training + run_id = log_model_training(args.model_dir, args.training_metrics) + + # Add evaluation metrics to the same run + log_model_evaluation(args.evaluation_file, run_id) + + # Add inference metrics to the same run + log_inference_performance(args.inference_file, run_id, args.artifacts_dir) + + print(f"Complete run logged with ID: {run_id}") + + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/experiments/base_model_training/scripts/test.ipynb b/experiments/base_model_training/scripts/test.ipynb new file mode 100644 index 00000000..8deb687d --- /dev/null +++ b/experiments/base_model_training/scripts/test.ipynb @@ -0,0 +1,355 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "f9b7f2b9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset Statistics:\n", + "- Total conversations.json files: 175\n", + "- Number of agencies: 3\n", + "- Number of unique topics: 175\n", + "- Agencies found: ['output_ID.ee', 'output_Politsei-_ja_Piirivalveamet', 'output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet']\n", + "- Topics found: ['2023_-_Korruptsiooniga_seotud_kohtulahendid_-_Politsei-_ja_Piirivalveamet', '5G_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ajutise_ja_rahvusvahelise_kaitse_taotlejate_arv_-_Politsei-_ja_Piirivalveamet', 'AmatoÌ\\x88oÌ\\x88rraadioside_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Autentimine_riiklikes_e-teenustes_-_ID.ee', 'Avalduse_esitamine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Avalduse_esitamine_veebis_-_Politseile_avalduse_esitamine_-_Politsei-_ja_Piirivalveamet', 'Avalduse_menetlemine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Avaliku_uÌ\\x88rituse_korraldamine_-_Politsei-_ja_Piirivalveamet', 'Avalikud_maÌ\\x88nguvaÌ\\x88ljakud_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'CE-maÌ\\x88rgis_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'DigiDoc4_rakenduse_ja_RIA_DigiDoc_mobiilirakenduse_kasutamine_aÌ\\x88rilistel_eesmaÌ\\x88rkidel_-_ID.ee', 'DigiDoc4_rakenduse_teade__sertifikaatide_usaldusnimekirja_uuendamine_ebaoÌ\\x83nnestus_-_ID.ee', 'DigiDoc_konteineri_formaatide_elutsuÌ\\x88kkel_-_ID.ee', 'Digiallkirjastamine_ID-kaardi,_mobiil-ID,_Smart-ID_ja_e-Templiga_-_ID.ee', 'Digiallkirjastamine_RIA_DigiDoc_mobiilirakenduses_-_ID.ee', 'Digidoc_failivormingud_BDOC,_CDOC,_ASICE_-_ID.ee', 'Digitaalne_allkirjastamine_ja_elektroonilised_allkirjad_-_ID.ee', 'Digitaalsed_dokumendid__ID-kaart,_digi-ID,_elamisloakaart_ja_e-residendi_digi-ID_-_ID.ee', 'Digitembeldamine_-_ID.ee', 'E-allkirja_kinnituslehe_salvestamine_-_ID.ee', 'E-haÌ\\x88aÌ\\x88letamine_ja_e-valimised_-_ID.ee', 'Ebaausad_kauplemisvoÌ\\x83tted_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ehitamine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ehitiste_ohutus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ehitustooted_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ehted,_vaÌ\\x88aÌ\\x88rismetallist_tooted_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Elamisloa_loÌ\\x83ppemine_voÌ\\x83i_kehtetuks_tunnistamine_-_Elamisluba_ettevoÌ\\x83tluseks_fuÌ\\x88uÌ\\x88silisest_isikust_ettevoÌ\\x83tjale_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaardi_kasutaja_meelespea_-_Elamisloakaardi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Elamisloakaart_-_OskustoÌ\\x88oÌ\\x88lisena_toÌ\\x88oÌ\\x88le_asumine_-_Politsei-_ja_Piirivalveamet', 'Elektriohutus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Elektroonilised_allkirjad_ja_nende_kaÌ\\x88sitlemine_Euroopas_-_ID.ee', 'EnergiamoÌ\\x83juga_toodete_energiamaÌ\\x88rgistus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Euroopa_reisiinfo_ja_-lubade_suÌ\\x88steem_-_Politsei-_ja_Piirivalveamet', 'Failide_kruÌ\\x88pteerimine_ja_dekruÌ\\x88pteerimine_DigiDoc4_rakendusega_-_ID.ee', 'Finantsteenused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Gaasiseadmed_ja_-paigaldised_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Hoonete_energiatoÌ\\x83husus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'ID-kaardi,_digi-ID,_elamisloakaardi_ja_diplomaadikaardi_sertifitseerimispoliitika_-_ID.ee', 'ID-kaardi_kasutaja_meelespea_-_ID-kaardi_taotlemine_Euroopa_Liidu_kodanikule_-_Politsei-_ja_Piirivalveamet', 'ID-kaardi_sertifikaatide_kehtivus_-_ID.ee', 'ID-kaardi_sertifikaatide_peatamine_-_ID.ee', 'ID-kaardi_sertifikaatide_vaatamine_ja_salvestamine_-_ID.ee', 'ID-kaardiga_sisenemine_voÌ\\x83i_allkirjastamine_e-teenustes_ebaoÌ\\x83nnestub_-_ID.ee', 'ID-kaardiÂ\\xa0PIN-koodid__uue_koodiuÌ\\x88mbriku_taotlemine_-_ID.ee', 'ID-kaart_ja_kaardilugeja__kuidas_kontrollida_ID-kaardi_lugeja_toÌ\\x88oÌ\\x88korras_olekut_-_ID.ee', 'ID-kaart_ja_selle_kasutusvoÌ\\x83imalused_-_ID.ee', 'ID-kaart_kui_isikut_toÌ\\x83endav_dokument_-_ID.ee', 'ID-tarkvara__diagnostika_failide_ning_logifailide_genereerimine_-_ID.ee', 'ID-tarkvara_andmekaitsetingimused_-_ID.ee', 'ID-tarkvara_koÌ\\x83ige_uuema_versiooni_info_-_ID.ee', 'ID-tarkvara_toetatud_operatsioonisuÌ\\x88steemid_-_ID.ee', 'ID-tarkvara_versioonide_info_(release_notes)_-_ID.ee', 'ID-tarkvara_versioonide_toeperioodi_pikkus_ehk_elutsuÌ\\x88kkel_-_ID.ee', 'Isikuandmete_toÌ\\x88oÌ\\x88tlemine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Isikuandmete_toÌ\\x88oÌ\\x88tlemise_poÌ\\x83himoÌ\\x83tted_-_Andmekaitsetingimused_-_Politsei-_ja_Piirivalveamet', 'Isikukaitsevahendid_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'JaÌ\\x88aÌ\\x88l_viibimiseks_keelatud_ala_taÌ\\x88histamine_-_Politsei-_ja_Piirivalveamet', 'JaÌ\\x88aÌ\\x88olud_-_Politsei-_ja_Piirivalveamet', 'KKK_â\\x80\\x93_korduma_kippuvad_kuÌ\\x88simused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Kaebuse_esitamine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Kasulik_info_kiipkaardi_lugejate_kohta_-_ID.ee', 'KaÌ\\x88ttesaamine_-_ID-kaardi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'KaÌ\\x88ttesaamine_-_Meremehe_teenistusraamatu_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Kemikaalid_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Kesk-Eesti_politseijaoskond_Raplas_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'Kodakondsustunnistus_-_Eesti_kodakondsus_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Koduohutus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Kogu_info_sertifikaatide_kasutustingimuste_kohta_-_ID.ee', 'Komisjoni_otsused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'KorruptsioonisuÌ\\x88uÌ\\x88teod_-_Politsei-_ja_Piirivalveamet', 'Kuhu_saab_ID-kaardiga_reisida__-_ID.ee', 'Kuidas_kontrollida_ID-kaardi_ja_lugeja_toÌ\\x88oÌ\\x88korras_olekut__-_ID.ee', 'Kuressaare_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'KuÌ\\x88simused-vastused_-_E-residendi_digi-ID_-_Politsei-_ja_Piirivalveamet', 'Lennu-_ja_merepaÌ\\x88aÌ\\x88ste_-_Otsingu-_ja_paÌ\\x88aÌ\\x88stetoÌ\\x88oÌ\\x88d_-_Politsei-_ja_Piirivalveamet', 'Lennureisijate_oÌ\\x83igused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'LigipaÌ\\x88aÌ\\x88setavuse_teatis_-_ID.ee', 'MaÌ\\x88nguasjad_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Mehitamata_oÌ\\x83husoÌ\\x83idukid_(droonid)_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Menetlemine_-_EL_kodaniku_alaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_OskustoÌ\\x88oÌ\\x88lisena_toÌ\\x88oÌ\\x88le_asumine_-_Politsei-_ja_Piirivalveamet', 'Menetlemine_-_Perekonnaliikme_taÌ\\x88htajaline_elamisoÌ\\x83igus_-_Politsei-_ja_Piirivalveamet', 'Mida_teha,_kui_ID-kaart_voÌ\\x83i_muu_digitaalne_dokument_on_kadunud_voÌ\\x83i_varastatud__-_ID.ee', 'Miks_on_vaja_uuendada_ID-tarkvara__-_ID.ee', 'Mis_see_on__-_Rahvusvaheline_koostoÌ\\x88oÌ\\x88_-_Politsei-_ja_Piirivalveamet', 'Mis_vahe_on_.bdoc_ja_.asice_laiendiga_digiallkirjastatud_dokumentidel__-_ID.ee', 'Mobiil-ID_PIN-koodid_-_ID.ee', 'Mobiil-ID__digitaalne_isikutunnistus_nutitelefonis_-_ID.ee', 'Mobiil-ID_kasutamine_-_ID.ee', 'Mobiil-ID_kasutamisel_tekkis_toÌ\\x83rkeid_-_ID.ee', 'Mobiil-ID_sertifikaadid__uÌ\\x88ldinfo_-_ID.ee', 'Mobiil-ID_taotlemine_ja_aktiveerimine_-_ID.ee', 'Mobiil-IDga_allkirjastamine_ebaoÌ\\x83nnestub_-_ID.ee', 'MyID_portaal_-_ID.ee', 'Numbriliikuvus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Numeratsioon_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ohtlikud_tooted_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Ostmine_e-poest_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'PIN-_ja_PUK-koodid__soovitused_turvalisuse_tagamiseks_-_ID.ee', 'PIN-_ja_PUK-koodidega_koodiuÌ\\x88mbrik_-_ID.ee', 'PIN-kood_on_blokeeritud_ehk_lukku_laÌ\\x88inud_-_ID.ee', 'PIN-koodide_ja_PUK-koodi_muutmine_-_ID.ee', 'PPA_sotsiaalmeedias_-_Meediasuhtluse_poÌ\\x83himoÌ\\x83tted_-_Politsei-_ja_Piirivalveamet', 'Paigalda_ID-tarkvara_-_ID.ee', 'Pakettreisid_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'PinPad_kaardilugeja_draiverite_kasutamine_-_ID.ee', 'Politsei_sekkumine_-_Avaliku_koosoleku_registreerimine_-_Politsei-_ja_Piirivalveamet', 'PolitseitoÌ\\x88oÌ\\x88_abivajajate_otsingutel_-_Eksinud_ja_teadmata_kadunud_-_Politsei-_ja_Piirivalveamet', 'Probleemid_kaardilugeja_kasutamisel_-_ID.ee', 'Puudusega_toode,_garantii_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'PuÌ\\x88rotehnika,_ilutulestik_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'RIA_DigiDoc_mobiilirakendus_-_ID.ee', 'RIA_DigiDoc_rakenduse_versioonide_info_(release_notes)_-_ID.ee', 'Raadiosagedused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Raadiosageduste_kasutamine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Rahaasjad_korda!_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Rail_Baltic_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Rail_Balticu_keskkonnamoÌ\\x83jude_hindamine_(KMH)_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Rakvere_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet', 'Raudteeohutuse_meelespea_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Reisimine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Reklaam_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'RiigiloÌ\\x83ivu_tasumisest_vabastamine_-_Politsei-_ja_Piirivalveamet', 'Riigiportaal_eesti.ee_-_ID.ee', 'RoÌ\\x83ivad_ja_jalatsid_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Sertifikaadid_ja_nende_turvalisus_-_ID.ee', 'Sideteenused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Sideteenuste_kaart_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Sideturg_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Soovid_DigiDoc4_rakenduses_digiallkirja_anda_-_ID.ee', 'Soovid_ID-tarkvara_DigiDoc4_oma_arvutis_uÌ\\x88les_leida__-_ID.ee', 'Soovid_alustada_ID-kaardi_elektroonilist_kasutamist_-_ID.ee', 'Soovid_alustada_mobiil-ID_kasutamist_-_ID.ee', 'Soovid_digi-ID_kaarti_-_ID.ee', 'Soovid_uut_ID-kaarti_-_ID.ee', 'Soovin_Smart-ID_kasutajaks_hakata_-_ID.ee', 'TTJA_otsuste_vaidlustamine_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'TV-_ja_raadioringhaÌ\\x88aÌ\\x88ling_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tallinna_politseiorkester_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_-_Elamisluba_ettevoÌ\\x83tluseks_aÌ\\x88riuÌ\\x88hingu_osanikule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_Eesti_passi_taotlemine_taÌ\\x88iskasvanule_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_MeresoÌ\\x83idutunnistuse_taotlemine_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_teeninduses_-_VaÌ\\x88lismaalase_passi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Taotlemine_vaÌ\\x88lismaal_-_Elamisloakaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet', 'Tarbijaharidus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tarbijavaidluste_komisjon_-_KKK_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tarbijavaidluste_komisjon_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tarbijavaidluste_komisjonist_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tartu_teenindus_-_Teenindused_-_Politsei-_ja_Piirivalveamet', 'Teavita_meid_probleemist_-_ID.ee', 'Teenuse_ohutus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Teenuste_lepingud_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Tingimused_-_Alalise_elaniku_elamisluba_-_Politsei-_ja_Piirivalveamet', 'Tingimused_-_Eesti_kodakondsus_eestkostetavale_-_Politsei-_ja_Piirivalveamet', 'Toodete_ligipaÌ\\x88aÌ\\x88setavus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'ToÌ\\x83stuksed_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'ToÌ\\x88oÌ\\x88d_raudtee_kaitsevoÌ\\x88oÌ\\x88ndis_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'TrahvimaÌ\\x88aÌ\\x88rad_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet', 'Trahviotsuse_edasi_kaebamine_seoses_kiir-_voÌ\\x83i_uÌ\\x88ldmenetlusega_-_Trahvid_-_Politsei-_ja_Piirivalveamet', 'Trahviteate_kaÌ\\x88ttetoimetamine_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet', 'Ubuntu__ID-tarkvara_paigaldamine,_uuendamine_ja_eemaldamine_-_ID.ee', 'Uuringud_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'UÌ\\x88htse_laadija_noÌ\\x83uded_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'UÌ\\x88ldhuviteenused_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'VaÌ\\x88ljastuskohad_-_E-residendi_digi-ID_-_Politsei-_ja_Piirivalveamet', 'Vee-_ja_oÌ\\x83husoÌ\\x83iduki_raadioload_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Veebilehe_id.ee_privaatsustingimused_-_ID.ee', 'Veebilehitsejate_seadistamine_ID-kaardi_kasutamiseks_-_ID.ee', 'VoÌ\\x83ru_teenindus_-_Teenindused_-_Politsei-_ja_Piirivalveamet', 'Web_eID_-_ID.ee', 'Wi-Fi_seadmete_kasutus_Tarbijakaitse_ja_Tehnilise_JaÌ\\x88relevalve_Amet', 'Windows__ID-tarkvara_paigaldus_loÌ\\x83ppes_toÌ\\x83rkega_-_ID.ee', 'eIDAS_ehk_e-identimise_ja_e-tehingute_maÌ\\x88aÌ\\x88rus_-_ID.ee', 'macOS__ID-tarkvara_paigaldamise_ja_eemaldamise_juhised_-_ID.ee']\n", + "\n", + "==================================================\n", + "\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Isikukaitsevahendid_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Kaebuse_esitamine_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Vee-_ja_õhusõiduki_raadioload_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Wi-Fi_seadmete_kasutus_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Tööd_raudtee_kaitsevööndis_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Ühtse_laadija_nõuded_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Avalduse_esitamine_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/TTJA_otsuste_vaidlustamine_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Elektriohutus_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Avalikud_mänguväljakud_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Tarbijavaidluste_komisjonist_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Reisimine_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Avalduse_menetlemine_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Sideturg_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Mänguasjad_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Pürotehnika,_ilutulestik_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Hoonete_energiatõhusus_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Rail_Baltic_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Ohtlikud_tooted_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Mehitamata_õhusõidukid_(droonid)_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Tarbijavaidluste_komisjon_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Numbriliikuvus_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Teenuse_ohutus_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Numeratsioon_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/5G_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Rail_Balticu_keskkonnamõjude_hindamine_(KMH)_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Sideteenused_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Raadiosagedused_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Tõstuksed_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Gaasiseadmed_ja_-paigaldised_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Pakettreisid_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Isikuandmete_töötlemine_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Sideteenuste_kaart_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Üldhuviteenused_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Toodete_ligipääsetavus_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Ehitiste_ohutus_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Ehitustooted_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Ehted,_väärismetallist_tooted_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Rahaasjad_korda!_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Puudusega_toode,_garantii_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Komisjoni_otsused_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Ebaausad_kauplemisvõtted_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/KKK_–_korduma_kippuvad_küsimused_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/CE-märgis_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Tarbijavaidluste_komisjon_-_KKK_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Koduohutus_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Rõivad_ja_jalatsid_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Reklaam_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Raudteeohutuse_meelespea_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Teenuste_lepingud_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/TV-_ja_raadioringhääling_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Raadiosageduste_kasutamine_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Amatöörraadioside_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Ehitamine_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Lennureisijate_õigused_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Finantsteenused_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Kemikaalid_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Energiamõjuga_toodete_energiamärgistus_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Tarbijaharidus_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Ostmine_e-poest_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet/Uuringud_Tarbijakaitse_ja_Tehnilise_Järelevalve_Amet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Kuressaare_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Tingimused_-_Eesti_kodakondsus_eestkostetavale_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Taotlemine_teeninduses_-_Eesti_passi_taotlemine_täiskasvanule_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/PPA_sotsiaalmeedias_-_Meediasuhtluse_põhimõtted_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Menetlemine_-_Perekonnaliikme_tähtajaline_elamisõigus_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Taotlemine_teeninduses_-_Välismaalase_passi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Rakvere_politseijaoskond_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Avaliku_ürituse_korraldamine_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Taotlemine_-_Elamisluba_ettevõtluseks_äriühingu_osanikule_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Menetlemine_-_EL_kodaniku_alaline_elamisõigus_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Ajutise_ja_rahvusvahelise_kaitse_taotlejate_arv_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Politsei_sekkumine_-_Avaliku_koosoleku_registreerimine_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Politseitöö_abivajajate_otsingutel_-_Eksinud_ja_teadmata_kadunud_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/2023_-_Korruptsiooniga_seotud_kohtulahendid_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Korruptsioonisüüteod_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Kodakondsustunnistus_-_Eesti_kodakondsus_täiskasvanule_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Elamisloakaardi_kasutaja_meelespea_-_Elamisloakaardi_taotlemine_täiskasvanule_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Kättesaamine_-_ID-kaardi_taotlemine_täiskasvanule_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Riigilõivu_tasumisest_vabastamine_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Lennu-_ja_merepääste_-_Otsingu-_ja_päästetööd_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Tallinna_politseiorkester_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Kättesaamine_-_Meremehe_teenistusraamatu_taotlemine_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Jääolud_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Trahvimäärad_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Menetlemine_-_Oskustöölisena_tööle_asumine_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Küsimused-vastused_-_E-residendi_digi-ID_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Avalduse_esitamine_veebis_-_Politseile_avalduse_esitamine_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Tingimused_-_Alalise_elaniku_elamisluba_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Mis_see_on__-_Rahvusvaheline_koostöö_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Võru_teenindus_-_Teenindused_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Euroopa_reisiinfo_ja_-lubade_süsteem_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Elamisloa_lõppemine_või_kehtetuks_tunnistamine_-_Elamisluba_ettevõtluseks_füüsilisest_isikust_ettevõtjale_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Trahviotsuse_edasi_kaebamine_seoses_kiir-_või_üldmenetlusega_-_Trahvid_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Trahviteate_kättetoimetamine_-_Kiiruskaamerad_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Jääl_viibimiseks_keelatud_ala_tähistamine_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Elamisloakaart_-_Oskustöölisena_tööle_asumine_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Kesk-Eesti_politseijaoskond_Raplas_-_Politseijaoskonnad_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Tartu_teenindus_-_Teenindused_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Väljastuskohad_-_E-residendi_digi-ID_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Taotlemine_teeninduses_-_Meresõidutunnistuse_taotlemine_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/ID-kaardi_kasutaja_meelespea_-_ID-kaardi_taotlemine_Euroopa_Liidu_kodanikule_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Taotlemine_välismaal_-_Elamisloakaardi_taotlemine_lapsele_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_Politsei-_ja_Piirivalveamet/Isikuandmete_töötlemise_põhimõtted_-_Andmekaitsetingimused_-_Politsei-_ja_Piirivalveamet/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-tarkvara_kõige_uuema_versiooni_info_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-kaardiga_sisenemine_või_allkirjastamine_e-teenustes_ebaõnnestub_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Mobiil-IDga_allkirjastamine_ebaõnnestub_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-kaart_kui_isikut_tõendav_dokument_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-tarkvara_andmekaitsetingimused_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Soovid_uut_ID-kaarti_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/PIN-koodide_ja_PUK-koodi_muutmine_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Mobiil-ID_PIN-koodid_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/RIA_DigiDoc_rakenduse_versioonide_info_(release_notes)_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-tarkvara__diagnostika_failide_ning_logifailide_genereerimine_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Windows__ID-tarkvara_paigaldus_lõppes_tõrkega_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-kaardi,_digi-ID,_elamisloakaardi_ja_diplomaadikaardi_sertifitseerimispoliitika_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/PIN-kood_on_blokeeritud_ehk_lukku_läinud_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Soovid_ID-tarkvara_DigiDoc4_oma_arvutis_üles_leida__-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Mobiil-ID__digitaalne_isikutunnistus_nutitelefonis_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Mobiil-ID_kasutamine_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/RIA_DigiDoc_mobiilirakendus_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-kaardi_sertifikaatide_peatamine_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/E-allkirja_kinnituslehe_salvestamine_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Riigiportaal_eesti.ee_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Web_eID_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-kaardi PIN-koodid__uue_koodiümbriku_taotlemine_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Teavita_meid_probleemist_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Mobiil-ID_taotlemine_ja_aktiveerimine_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Kogu_info_sertifikaatide_kasutustingimuste_kohta_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Digiallkirjastamine_ID-kaardi,_mobiil-ID,_Smart-ID_ja_e-Templiga_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Soovid_alustada_ID-kaardi_elektroonilist_kasutamist_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Soovid_DigiDoc4_rakenduses_digiallkirja_anda_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Soovid_digi-ID_kaarti_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/eIDAS_ehk_e-identimise_ja_e-tehingute_määrus_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/PIN-_ja_PUK-koodidega_koodiümbrik_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-tarkvara_versioonide_toeperioodi_pikkus_ehk_elutsükkel_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Autentimine_riiklikes_e-teenustes_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Digiallkirjastamine_RIA_DigiDoc_mobiilirakenduses_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Ligipääsetavuse_teatis_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Elektroonilised_allkirjad_ja_nende_käsitlemine_Euroopas_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Kuhu_saab_ID-kaardiga_reisida__-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Digitaalsed_dokumendid__ID-kaart,_digi-ID,_elamisloakaart_ja_e-residendi_digi-ID_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Mobiil-ID_kasutamisel_tekkis_tõrkeid_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Failide_krüpteerimine_ja_dekrüpteerimine_DigiDoc4_rakendusega_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/E-hääletamine_ja_e-valimised_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Mis_vahe_on_.bdoc_ja_.asice_laiendiga_digiallkirjastatud_dokumentidel__-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/DigiDoc_konteineri_formaatide_elutsükkel_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-tarkvara_versioonide_info_(release_notes)_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Kuidas_kontrollida_ID-kaardi_ja_lugeja_töökorras_olekut__-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Soovin_Smart-ID_kasutajaks_hakata_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/MyID_portaal_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Sertifikaadid_ja_nende_turvalisus_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-kaart_ja_kaardilugeja__kuidas_kontrollida_ID-kaardi_lugeja_töökorras_olekut_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Paigalda_ID-tarkvara_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/PinPad_kaardilugeja_draiverite_kasutamine_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-kaardi_sertifikaatide_vaatamine_ja_salvestamine_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Soovid_alustada_mobiil-ID_kasutamist_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Miks_on_vaja_uuendada_ID-tarkvara__-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Veebilehe_id.ee_privaatsustingimused_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Veebilehitsejate_seadistamine_ID-kaardi_kasutamiseks_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Probleemid_kaardilugeja_kasutamisel_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Digidoc_failivormingud_BDOC,_CDOC,_ASICE_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Kasulik_info_kiipkaardi_lugejate_kohta_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Ubuntu__ID-tarkvara_paigaldamine,_uuendamine_ja_eemaldamine_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Digitembeldamine_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/DigiDoc4_rakenduse_teade__sertifikaatide_usaldusnimekirja_uuendamine_ebaõnnestus_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Digitaalne_allkirjastamine_ja_elektroonilised_allkirjad_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/PIN-_ja_PUK-koodid__soovitused_turvalisuse_tagamiseks_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/macOS__ID-tarkvara_paigaldamise_ja_eemaldamise_juhised_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Mida_teha,_kui_ID-kaart_või_muu_digitaalne_dokument_on_kadunud_või_varastatud__-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-kaardi_sertifikaatide_kehtivus_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/DigiDoc4_rakenduse_ja_RIA_DigiDoc_mobiilirakenduse_kasutamine_ärilistel_eesmärkidel_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-kaart_ja_selle_kasutusvõimalused_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/ID-tarkvara_toetatud_operatsioonisüsteemid_-_ID.ee/conversations.json\n", + "Processed: /home/ckittask/ria/Dataset-Generator/output_dataset_gemma12/output_ID.ee/Mobiil-ID_sertifikaadid__üldinfo_-_ID.ee/conversations.json\n", + "Conversion complete! Output saved to conversations.csv\n", + "\n", + "Preview of generated CSV:\n", + "['conversation_id', 'turn', 'speaker', 'text', 'agency']\n", + "['ce428ee6-5fc4-48cc-92b3-eb844a79834d', '1', 'user', 'Tere! Ma olen kuulnud, et on oluline kontrollida isikukaitsevahendite ohutust. Kust ma peaksin alustama nende kohta infot leidmiseks?', 'home']\n", + "['ce428ee6-5fc4-48cc-92b3-eb844a79834d', '2', 'assistant', 'Tere! Oluline küsimus. Alustuseks on oluline teada, et isikukaitsevahendite ohutust reguleerib Euroopa Parlamendi ja Nõukogu määrus (EL) 2016/425. See määrus kehtib täismahus alates 21. aprillist 2018. Lisainfot saate ka Eesti Standardimis- ja Akrediteerimiskeskuse koduleheküljelt www.evs.ee, kus on erinevad standardid olenevalt tootekategooriast.', 'home']\n", + "['ce428ee6-5fc4-48cc-92b3-eb844a79834d', '3', 'user', 'Määrus 2016/425… See on päris palju infot. Kuidas ma aru saan, kas isikukaitsevahend, mida ma ostan, on nõuetele vastav?', 'home']\n", + "['ce428ee6-5fc4-48cc-92b3-eb844a79834d', '4', 'assistant', 'Hea küsimus! Nõuetele vastav isikukaitsevahend peab läbinud vastavushindamise (eriti II ja III ohukategooria puhul on teavitatud asutuse kaasamine vajalik) ning omama vastavat dokumentatsiooni. See dokumentatsioon sisaldab tehnilist dokumentatsiooni ja vastavusdeklaratsiooni. Oluline on ka CE-märk – see peab olema nähtav ja loetav tootel. III ohukategooria puhul peab CE-märgile järgnema teavitatud asutuse identifitseerimisnumber.', 'home']\n" + ] + } + ], + "source": [ + "import os\n", + "import json\n", + "import csv\n", + "import uuid\n", + "from pathlib import Path\n", + "\n", + "\n", + "def convert_conversations_to_csv(\n", + " input_dir=\"/home/ckittask/ria/Dataset-Generator/output_dataset_gemma12\",\n", + " output_file=\"conversations.csv\",\n", + "):\n", + " \"\"\"\n", + " Convert conversations.json files from nested directory structure to CSV format.\n", + "\n", + " Args:\n", + " input_dir (str): Root directory containing agency folders\n", + " output_file (str): Output CSV file name\n", + " \"\"\"\n", + "\n", + " # Open CSV file for writing\n", + " with open(output_file, \"w\", newline=\"\", encoding=\"utf-8\") as csvfile:\n", + " fieldnames = [\"conversation_id\", \"turn\", \"speaker\", \"text\", \"agency\"]\n", + " writer = csv.DictWriter(csvfile, fieldnames=fieldnames)\n", + "\n", + " # Write header\n", + " writer.writeheader()\n", + "\n", + " # Walk through the directory structure\n", + " for root, dirs, files in os.walk(input_dir):\n", + " # Check if conversations.json exists in current directory\n", + " if \"conversations.json\" in files:\n", + " json_path = os.path.join(root, \"conversations.json\")\n", + "\n", + " # Extract agency name from path\n", + " path_parts = Path(root).parts\n", + " if len(path_parts) >= 2:\n", + " agency = path_parts[\n", + " 1\n", + " ] # Assuming structure: output_dataset/agency1/topic1/\n", + " else:\n", + " agency = \"unknown\"\n", + "\n", + " try:\n", + " # Read and parse JSON file\n", + " with open(json_path, \"r\", encoding=\"utf-8\") as jsonfile:\n", + " data = json.load(jsonfile)\n", + "\n", + " # Process each conversation in the JSON file\n", + " for conversation in data:\n", + " if \"messages\" in conversation:\n", + " # Generate unique conversation ID\n", + " conversation_id = str(uuid.uuid4())\n", + "\n", + " # Process each message in the conversation\n", + " for turn, message in enumerate(conversation[\"messages\"], 1):\n", + " writer.writerow(\n", + " {\n", + " \"conversation_id\": conversation_id,\n", + " \"turn\": turn,\n", + " \"speaker\": message.get(\"role\", \"\"),\n", + " \"text\": message.get(\"content\", \"\"),\n", + " \"agency\": agency,\n", + " }\n", + " )\n", + "\n", + " print(f\"Processed: {json_path}\")\n", + "\n", + " except json.JSONDecodeError as e:\n", + " print(f\"Error reading JSON file {json_path}: {e}\")\n", + " except Exception as e:\n", + " print(f\"Error processing file {json_path}: {e}\")\n", + "\n", + " print(f\"Conversion complete! Output saved to {output_file}\")\n", + "\n", + "\n", + "def get_statistics(\n", + " input_dir=\"/home/ckittask/ria/Dataset-Generator/output_dataset_gemma12\",\n", + "):\n", + " \"\"\"\n", + " Print statistics about the dataset structure.\n", + " \"\"\"\n", + " total_files = 0\n", + " agencies = set()\n", + " topics = set()\n", + "\n", + " for root, dirs, files in os.walk(input_dir):\n", + " if \"conversations.json\" in files:\n", + " total_files += 1\n", + " path_parts = Path(root).parts\n", + " if len(path_parts) >= 2:\n", + " agencies.add(path_parts[-2])\n", + " if len(path_parts) >= 3:\n", + " topics.add(path_parts[-1])\n", + "\n", + " print(\"Dataset Statistics:\")\n", + " print(f\"- Total conversations.json files: {total_files}\")\n", + " print(f\"- Number of agencies: {len(agencies)}\")\n", + " print(f\"- Number of unique topics: {len(topics)}\")\n", + " print(f\"- Agencies found: {sorted(agencies)}\")\n", + " print(f\"- Topics found: {sorted(topics)}\")\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " # Print dataset statistics first\n", + " get_statistics()\n", + " print(\"\\n\" + \"=\" * 50 + \"\\n\")\n", + "\n", + " # Convert to CSV\n", + " convert_conversations_to_csv()\n", + "\n", + " # Optional: Preview the first few rows of the generated CSV\n", + " print(\"\\nPreview of generated CSV:\")\n", + " try:\n", + " with open(\"conversations.csv\", \"r\", encoding=\"utf-8\") as f:\n", + " reader = csv.reader(f)\n", + " for i, row in enumerate(reader):\n", + " if i < 5: # Show first 5 rows\n", + " print(row)\n", + " else:\n", + " break\n", + " except FileNotFoundError:\n", + " print(\"CSV file not found.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ria", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/experiments/base_model_training/scripts/train.py b/experiments/base_model_training/scripts/train.py new file mode 100644 index 00000000..d2392d28 --- /dev/null +++ b/experiments/base_model_training/scripts/train.py @@ -0,0 +1,611 @@ +import os +import argparse +import time +import json +import random +import numpy as np +import pandas as pd +from datetime import datetime + +import torch +from torch.utils.data import Dataset, DataLoader +from torch.optim import AdamW + +from transformers import ( + AutoTokenizer, + AutoModelForSequenceClassification, + get_linear_schedule_with_warmup, + set_seed, +) + +from sklearn.metrics import ( + accuracy_score, + precision_recall_fscore_support, + roc_auc_score, + confusion_matrix, + roc_curve, +) + +import mlflow +import matplotlib.pyplot as plt +import seaborn as sns + +# Define constants and configurations +AVAILABLE_MODELS = { + "bert": "tartuNLP/EstBERT", + "roberta": "FacebookAI/xlm-roberta-base", + "xlm": "FacebookAI/xlm-mlm-100-1280", +} + + +class TextClassificationDataset(Dataset): + def __init__(self, texts, labels, tokenizer, max_length=128): + self.texts = texts + self.labels = labels + self.tokenizer = tokenizer + self.max_length = max_length + + def __len__(self): + return len(self.texts) + + def __getitem__(self, idx): + text = str(self.texts[idx]) + label = self.labels[idx] + + encoding = self.tokenizer( + text, + add_special_tokens=True, + max_length=self.max_length, + padding="max_length", + truncation=True, + return_attention_mask=True, + return_tensors="pt", + ) + + return { + "input_ids": encoding["input_ids"].flatten(), + "attention_mask": encoding["attention_mask"].flatten(), + "label": torch.tensor(label, dtype=torch.long), + } + + +def set_random_seeds(seed_val=42): + random.seed(seed_val) + np.random.seed(seed_val) + torch.manual_seed(seed_val) + torch.cuda.manual_seed_all(seed_val) + set_seed(seed_val) + + +def load_data(data_path, split=None): + """ + Load the data from the given path with consistent label mapping. + """ + if split: + data_path = os.path.join(data_path, f"{split}.csv") + + if data_path.endswith(".csv"): + df = pd.read_csv(data_path) + elif data_path.endswith(".json"): + df = pd.read_json(data_path, lines=True) + else: + raise ValueError(f"Unsupported file format: {data_path}") + + text_col = "text" if "text" in df.columns else "content" + + # Check for 'agency' column (our case) or fall back to 'label' or 'class' + if "agency" in df.columns: + label_col = "agency" + elif "label" in df.columns: + label_col = "label" + elif "class" in df.columns: + label_col = "class" + else: + raise ValueError( + "No suitable label column found in the data. Need 'agency', 'label', or 'class'." + ) + + # If labels are strings (e.g., agency names), convert to integers + if df[label_col].dtype == "object": + # Define consistent mapping file path + mapping_file = os.path.join(os.path.dirname(data_path), "label_mapping.json") + + # Try to load existing mapping first + if os.path.exists(mapping_file): + print(f"Loading existing label mapping from {mapping_file}") + with open(mapping_file, "r") as f: + label_map = json.load(f) + else: + # Create consistent label mapping by sorting the unique labels + unique_labels = sorted(df[label_col].unique()) + label_map = {label: i for i, label in enumerate(unique_labels)} + + # Save the mapping for consistency across splits + os.makedirs(os.path.dirname(mapping_file), exist_ok=True) + with open(mapping_file, "w") as f: + json.dump(label_map, f, indent=2) + print(f"Created new label mapping: {label_map}") + + # Convert string labels to integers using consistent mapping + labels = df[label_col].map(label_map).values + + # Check for any unmapped labels + if np.any(pd.isna(labels)): + unmapped = df[label_col][pd.isna(labels)].unique() + raise ValueError(f"Found unmapped labels: {unmapped}") + + else: + labels = df[label_col].values + + print( + f"Loaded {len(df)} samples with label distribution: {dict(zip(*np.unique(labels, return_counts=True)))}" + ) + + return df[text_col].values, labels + + +def compute_metrics(preds, labels, probs=None): + """ + Compute various classification metrics. + + Args: + preds: Predicted labels + labels: True labels + probs: Prediction probabilities for ROC/PR curves + + Returns: + Dictionary of metrics + """ + + precision, recall, f1, _ = precision_recall_fscore_support( + labels, preds, average="weighted" + ) + + acc = accuracy_score(labels, preds) + + metrics = { + "accuracy": acc, + "precision": precision, + "recall": recall, + "f1": f1, + } + + # Compute confusion matrix + cm = confusion_matrix(labels, preds) + + # Calculate class-wise metrics + class_precision, class_recall, class_f1, _ = precision_recall_fscore_support( + labels, preds, average=None + ) + + # Add class metrics to the return dict + for i, (p, r, f) in enumerate(zip(class_precision, class_recall, class_f1)): + metrics[f"class_{i}_precision"] = p + metrics[f"class_{i}_recall"] = r + metrics[f"class_{i}_f1"] = f + + # Compute ROC AUC if probabilities are provided and it's a binary task + if probs is not None: + if probs.shape[1] == 2: # Binary classification + metrics["roc_auc"] = roc_auc_score(labels, probs[:, 1]) + # Compute FPR@95TPR + fpr, tpr, thresholds = roc_curve(labels, probs[:, 1]) + if any(tpr >= 0.95): + idx = np.argmin(np.abs(tpr - 0.95)) + metrics["fpr@95tpr"] = fpr[idx] + else: # Multi-class + try: + # One-hot encode the labels for multi-class ROC AUC + labels_one_hot = np.zeros((len(labels), probs.shape[1])) + for i, label in enumerate(labels): + labels_one_hot[i, label] = 1 + + metrics["roc_auc"] = roc_auc_score( + labels_one_hot, probs, average="weighted", multi_class="ovr" + ) + except Exception as e: + print(f"Warning: Could not compute ROC AUC for multi-class: {str(e)}") + + return metrics, cm + + +def log_confusion_matrix(cm, class_names=None): + """ + Create and log a confusion matrix figure to MLflow. + + Args: + cm: Confusion matrix + class_names: Names of the classes + """ + if class_names is None: + class_names = [f"Class {i}" for i in range(cm.shape[0])] + print(f"Confusion Matrix:\n{cm}") + plt.figure(figsize=(10, 8)) + sns.heatmap( + cm, + annot=True, + fmt="d", + cmap="Blues", + xticklabels=class_names, + yticklabels=class_names, + ) + plt.xlabel("Predicted") + plt.ylabel("True") + plt.title("Confusion Matrix") + + # Save the figure + print("Saving confusion matrix figure...") + confusion_matrix_path = "confusion_matrix.png" + plt.tight_layout() + plt.savefig(confusion_matrix_path) + plt.close() + + # Log the figure to MLflow + mlflow.log_artifact(confusion_matrix_path) + + # Clean up the file + # os.remove(confusion_matrix_path) + + +def train_epoch(model, dataloader, optimizer, scheduler, device): + """Run a single training epoch.""" + model.train() + total_loss = 0 + + for batch in dataloader: + optimizer.zero_grad() + + input_ids = batch["input_ids"].to(device) + attention_mask = batch["attention_mask"].to(device) + labels = batch["label"].to(device) + + outputs = model( + input_ids=input_ids, attention_mask=attention_mask, labels=labels + ) + + loss = outputs.loss + total_loss += loss.item() + + loss.backward() + torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) + optimizer.step() + scheduler.step() + + return total_loss / len(dataloader) + + +def evaluate(model, dataloader, device, num_labels): + """Evaluate the model on the given dataloader.""" + model.eval() + all_preds = [] + all_labels = [] + all_probs = [] + + with torch.no_grad(): + for batch in dataloader: + input_ids = batch["input_ids"].to(device) + attention_mask = batch["attention_mask"].to(device) + labels = batch["label"].to(device) + + outputs = model(input_ids=input_ids, attention_mask=attention_mask) + + logits = outputs.logits + + # Convert logits to probabilities + probs = torch.nn.functional.softmax(logits, dim=1) + + # Get predicted class (argmax) + preds = torch.argmax(logits, dim=1) + + all_preds.extend(preds.cpu().numpy()) + all_labels.extend(labels.cpu().numpy()) + all_probs.extend(probs.cpu().numpy()) + # write text input, all_preds, all_labels to a file + with open("predictions.txt", "w") as f: + for text, pred, label in zip(dataloader.dataset.texts, all_preds, all_labels): + f.write(f"{text}\t{pred}\t{label}\n") + print(f"Evaluated {len(all_preds)} samples.") + + return np.array(all_preds), np.array(all_labels), np.array(all_probs) + + +def measure_inference_time(model, dataloader, device, num_runs=100): + """Measure the average inference time.""" + model.eval() + batch = next(iter(dataloader)) + + input_ids = batch["input_ids"].to(device) + attention_mask = batch["attention_mask"].to(device) + + # Warm-up + for _ in range(10): + with torch.no_grad(): + _ = model(input_ids=input_ids, attention_mask=attention_mask) + + # Measure inference time + start_time = time.time() + for _ in range(num_runs): + with torch.no_grad(): + _ = model(input_ids=input_ids, attention_mask=attention_mask) + end_time = time.time() + + avg_time = (end_time - start_time) / num_runs + print(f"Average inference time over {num_runs} runs: {avg_time:.4f} seconds") + return avg_time + + +def train_and_evaluate(args): + """Main training and evaluation function.""" + # Set up random seeds for reproducibility + set_random_seeds(args.seed) + + # Set up device + device = "cpu" # torch.device( + # "cuda" if torch.cuda.is_available() and not args.no_cuda else "cpu" + # ) + + # Load data + train_texts, train_labels = load_data(os.path.join(args.data_dir, "train.csv")) + val_texts, val_labels = load_data(os.path.join(args.data_dir, "val.csv")) + test_texts, test_labels = load_data(os.path.join(args.data_dir, "test.csv")) + + # Determine number of classes + num_labels = len(np.unique(train_labels)) + print(np.unique(train_labels)) + print(train_labels) + # Set up model name + model_name = AVAILABLE_MODELS.get(args.model_type, args.model_type) + + # Set up tokenizer and model + tokenizer = AutoTokenizer.from_pretrained(model_name) + model = AutoModelForSequenceClassification.from_pretrained( + model_name, num_labels=num_labels + ) + model.to(device) + + # Create datasets + train_dataset = TextClassificationDataset( + train_texts, train_labels, tokenizer, max_length=args.max_seq_length + ) + # show sample of train_dataset + print(f"Sample from train dataset: {train_texts[0]} -> {train_labels[0]}") + + val_dataset = TextClassificationDataset( + val_texts, val_labels, tokenizer, max_length=args.max_seq_length + ) + # show sample of val_dataset + print(f"Sample from validation dataset: {val_texts[0]} -> {val_labels[0]}") + test_dataset = TextClassificationDataset( + test_texts, test_labels, tokenizer, max_length=args.max_seq_length + ) + # show sample of test_dataset + print(f"Sample from test dataset: {test_texts[0]} -> {test_labels[0]}") + + # Create dataloaders + train_dataloader = DataLoader( + train_dataset, batch_size=args.batch_size, shuffle=True + ) + val_dataloader = DataLoader(val_dataset, batch_size=args.batch_size) + test_dataloader = DataLoader(test_dataset, batch_size=args.batch_size) + + # Set up optimizer and scheduler + optimizer = AdamW( + model.parameters(), lr=args.learning_rate, weight_decay=args.weight_decay + ) + + total_steps = len(train_dataloader) * args.num_epochs + warmup_steps = int(total_steps * args.warmup_ratio) + + scheduler = get_linear_schedule_with_warmup( + optimizer, num_warmup_steps=warmup_steps, num_training_steps=total_steps + ) + + # Set up MLflow + mlflow.set_tracking_uri(args.mlflow_tracking_uri) + experiment_name = f"text_classification_{args.model_type}" + mlflow.set_experiment(experiment_name) + + # Start MLflow run + with mlflow.start_run( + run_name=f"{args.model_type}_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + ): + # Log parameters + mlflow.log_params( + { + "model_type": args.model_type, + "model_name": model_name, + "num_epochs": args.num_epochs, + "batch_size": args.batch_size, + "learning_rate": args.learning_rate, + "weight_decay": args.weight_decay, + "warmup_ratio": args.warmup_ratio, + "max_seq_length": args.max_seq_length, + "seed": args.seed, + "device": str(device), + "num_labels": num_labels, + "train_samples": len(train_dataset), + "val_samples": len(val_dataset), + "test_samples": len(test_dataset), + } + ) + + # Training loop + start_time = time.time() + + best_val_f1 = 0.0 + best_epoch = 0 + + for epoch in range(args.num_epochs): + train_loss = train_epoch( + model, train_dataloader, optimizer, scheduler, device + ) + + mlflow.log_metric("train_loss", train_loss, step=epoch) + + # Evaluate on validation set + val_preds, val_labels, val_probs = evaluate( + model, val_dataloader, device, num_labels + ) + + val_metrics, _ = compute_metrics(val_preds, val_labels, val_probs) + print(f"Epoch {epoch + 1}/{args.num_epochs} - Validation Metrics:") + print("\n".join([f" {k}: {v:.4f}" for k, v in val_metrics.items()])) + + for metric_name, metric_value in val_metrics.items(): + mlflow.log_metric(f"val_{metric_name}", metric_value, step=epoch) + + # Save best model + if val_metrics["f1"] > best_val_f1: + best_val_f1 = val_metrics["f1"] + best_epoch = epoch + + # Save the model + model_dir = os.path.join( + args.output_dir, f"{args.model_type}_epoch_{epoch}" + ) + os.makedirs(model_dir, exist_ok=True) + model.save_pretrained(model_dir) + tokenizer.save_pretrained(model_dir) + + # Log the model + mlflow.pytorch.log_model( + model, + f"{args.model_type}_best_model", + registered_model_name=f"{args.model_type}_classifier", + ) + + training_time = time.time() - start_time + mlflow.log_metric("training_time_seconds", training_time) + print(f"Training completed in {training_time:.2f} seconds.") + # Load the best model for final evaluation + best_model_path = os.path.join( + args.output_dir, f"{args.model_type}_epoch_{best_epoch}" + ) + if os.path.exists(best_model_path): + model = AutoModelForSequenceClassification.from_pretrained(best_model_path) + model.to(device) + + # Evaluate on test set + test_preds, test_labels, test_probs = evaluate( + model, test_dataloader, device, num_labels + ) + + test_metrics, test_cm = compute_metrics(test_preds, test_labels, test_probs) + print("Test Metrics:") + print(f"Best epoch: {best_epoch + 1}, Best validation F1: {best_val_f1:.4f}") + + for metric_name, metric_value in test_metrics.items(): + print(f" {metric_name}: {metric_value:.4f}") + mlflow.log_metric(f"test_{metric_name}", metric_value) + + # Log confusion matrix + log_confusion_matrix(test_cm) + + # Measure inference time + inference_time = measure_inference_time(model, test_dataloader, device) + mlflow.log_metric("avg_inference_time_seconds", inference_time) + + # Save metrics to a JSON file for easy access + metrics_path = os.path.join(args.output_dir, f"{args.model_type}_metrics.json") + output_metrics = { + "model_type": args.model_type, + "model_name": model_name, + "training_time_seconds": training_time, + "avg_inference_time_seconds": inference_time, + "test_metrics": test_metrics, + "best_epoch": best_epoch + 1, + "best_val_f1": best_val_f1, + } + + with open(metrics_path, "w") as f: + json.dump(output_metrics, f, indent=2) + + # Log the metrics file + mlflow.log_artifact(metrics_path) + + +def main(): + parser = argparse.ArgumentParser( + description="Train and evaluate transformer-based text classifiers" + ) + + # Required parameters + parser.add_argument( + "--model_type", + type=str, + required=True, + choices=list(AVAILABLE_MODELS.keys()) + ["other"], + help="Type of model to use (bert, roberta, xlm, or other)", + ) + parser.add_argument( + "--data_dir", + type=str, + required=True, + help="Path to the directory containing the data files", + ) + parser.add_argument( + "--output_dir", + type=str, + required=True, + help="Path to save model checkpoints and outputs", + ) + + # Optional parameters + parser.add_argument( + "--model_name", + type=str, + default=None, + help="Full model name (if not using a predefined type)", + ) + parser.add_argument( + "--mlflow_tracking_uri", + type=str, + default="mlruns", + help="URI for MLflow tracking server", + ) + parser.add_argument( + "--num_epochs", type=int, default=5, help="Number of training epochs" + ) + parser.add_argument( + "--batch_size", + type=int, + default=16, + help="Batch size for training and evaluation", + ) + parser.add_argument( + "--learning_rate", type=float, default=2e-5, help="Learning rate" + ) + parser.add_argument("--weight_decay", type=float, default=0.01, help="Weight decay") + parser.add_argument( + "--warmup_ratio", + type=float, + default=0.1, + help="Ratio of training steps for LR warmup", + ) + parser.add_argument( + "--max_seq_length", + type=int, + default=512, + help="Maximum sequence length for tokenization", + ) + parser.add_argument( + "--seed", type=int, default=52, help="Random seed for reproducibility" + ) + parser.add_argument( + "--no_cuda", action="store_true", help="Disable CUDA even if available" + ) + + args = parser.parse_args() + + # Create output directory if it doesn't exist + os.makedirs(args.output_dir, exist_ok=True) + + # Update model_name if provided + if args.model_name and args.model_type == "other": + AVAILABLE_MODELS["other"] = args.model_name + + train_and_evaluate(args) + + +if __name__ == "__main__": + main() diff --git a/experiments/base_model_training/scripts/utils.py b/experiments/base_model_training/scripts/utils.py new file mode 100644 index 00000000..c8a26a65 --- /dev/null +++ b/experiments/base_model_training/scripts/utils.py @@ -0,0 +1,642 @@ +import os +import random +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +from sklearn.model_selection import train_test_split +from sklearn.metrics import ( + accuracy_score, + precision_recall_fscore_support, + confusion_matrix, + roc_curve, + precision_recall_curve, + auc, + roc_auc_score, + average_precision_score, +) +import torch +from transformers import set_seed +import mlflow +import json +import time +from datetime import datetime + + +def set_random_seeds(seed_val=42): + """ + Set random seeds for reproducibility across Python, NumPy and PyTorch. + + Args: + seed_val (int): The seed value to use + """ + random.seed(seed_val) + np.random.seed(seed_val) + torch.manual_seed(seed_val) + torch.cuda.manual_seed_all(seed_val) + set_seed(seed_val) + # Set deterministic behavior for reproducibility + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + +def preprocess_data(data_path, text_col="text", label_col="label", save_path=None): + """ + Preprocess the data from a CSV or JSON file. + + Args: + data_path (str): Path to the data file + text_col (str): Name of the column containing the text + label_col (str): Name of the column containing the labels + save_path (str, optional): Path to save the preprocessed data + + Returns: + pd.DataFrame: Preprocessed dataframe + """ + # Load the data + if data_path.endswith(".csv"): + df = pd.read_csv(data_path) + elif data_path.endswith(".json"): + df = pd.read_json(data_path, lines=True) + else: + raise ValueError(f"Unsupported file format: {data_path}") + + # Verify columns exist + if text_col not in df.columns: + raise ValueError(f"Text column '{text_col}' not found in data") + if label_col not in df.columns: + raise ValueError(f"Label column '{label_col}' not found in data") + + # Basic preprocessing + df = df.dropna(subset=[text_col, label_col]) + + # Ensure labels are numeric + if not pd.api.types.is_numeric_dtype(df[label_col]): + # If labels are strings, convert to integers + label_map = {label: i for i, label in enumerate(df[label_col].unique())} + df[label_col] = df[label_col].map(label_map) + + # Save the label mapping for reference + if save_path: + label_map_path = os.path.join(os.path.dirname(save_path), "label_map.json") + with open(label_map_path, "w") as f: + json.dump(label_map, f, indent=2) + + # Save preprocessed data if a path is provided + if save_path: + os.makedirs(os.path.dirname(save_path), exist_ok=True) + if save_path.endswith(".csv"): + df.to_csv(save_path, index=False) + elif save_path.endswith(".json"): + df.to_json(save_path, orient="records", lines=True) + + return df + + +def split_data( + df, + text_col="text", + label_col="label", + test_size=0.2, + val_size=0.1, + stratify=True, + random_state=42, + save_dir=None, +): + """ + Split the data into train, validation, and test sets. + """ + actual_val_size = val_size / (1 - test_size) + + stratify_col = df[label_col] if stratify else None + train_val_df, test_df = train_test_split( + df, test_size=test_size, random_state=random_state, stratify=stratify_col + ) + + stratify_col = train_val_df[label_col] if stratify else None + train_df, val_df = train_test_split( + train_val_df, + test_size=actual_val_size, + random_state=random_state, + stratify=stratify_col, + ) + + if save_dir: + os.makedirs(save_dir, exist_ok=True) + train_df.to_csv(os.path.join(save_dir, "train.csv"), index=False) + val_df.to_csv(os.path.join(save_dir, "val.csv"), index=False) + test_df.to_csv(os.path.join(save_dir, "test.csv"), index=False) + + split_info = { + "total_samples": len(df), + "train_samples": len(train_df), + "val_samples": len(val_df), + "test_samples": len(test_df), + "test_size": test_size, + "val_size": val_size, + "stratify": stratify, + "random_state": random_state, + "date_created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + + with open(os.path.join(save_dir, "split_info.json"), "w") as f: + json.dump(split_info, f, indent=2) + + return train_df, val_df, test_df + + +def compute_metrics(y_true, y_pred, y_proba=None, average="weighted"): + """ + Compute classification metrics. + + """ + metrics = {} + + # Basic classification metrics + metrics["accuracy"] = accuracy_score(y_true, y_pred) + + precision, recall, f1, _ = precision_recall_fscore_support( + y_true, y_pred, average=average + ) + + metrics["precision"] = precision + metrics["recall"] = recall + metrics["f1"] = f1 + + # Confusion matrix + cm = confusion_matrix(y_true, y_pred) + metrics["confusion_matrix"] = cm + + # Calculate per-class metrics + class_precision, class_recall, class_f1, _ = precision_recall_fscore_support( + y_true, y_pred, average=None + ) + + # Add class metrics + num_classes = len(np.unique(y_true)) + for i in range(num_classes): + metrics[f"class_{i}_precision"] = class_precision[i] + metrics[f"class_{i}_recall"] = class_recall[i] + metrics[f"class_{i}_f1"] = class_f1[i] + + # ROC and PR curve metrics if probabilities are provided + if y_proba is not None: + # For binary classification + if num_classes == 2 and y_proba.shape[1] == 2: + fpr, tpr, _ = roc_curve(y_true, y_proba[:, 1]) + metrics["roc_auc"] = auc(fpr, tpr) + + # Find the FPR at 95% TPR + if any(tpr >= 0.95): + idx_95 = next(i for i, x in enumerate(tpr) if x >= 0.95) + metrics["fpr@95tpr"] = fpr[idx_95] + else: + metrics["fpr@95tpr"] = float("nan") + + # Precision-Recall AUC + precision, recall, _ = precision_recall_curve(y_true, y_proba[:, 1]) + metrics["pr_auc"] = auc(recall, precision) + + # For multi-class classification + else: + try: + # One-hot encode the labels + y_true_onehot = np.zeros((len(y_true), num_classes)) + for i, val in enumerate(y_true): + y_true_onehot[i, val] = 1 + + # Calculate ROC AUC for multi-class + metrics["roc_auc"] = roc_auc_score( + y_true_onehot, y_proba, average=average, multi_class="ovr" + ) + + # Calculate average precision score + metrics["pr_auc"] = average_precision_score( + y_true_onehot, y_proba, average=average + ) + except Exception as e: + print(f"Warning: Could not compute ROC/PR metrics: {str(e)}") + + return metrics + + +def plot_confusion_matrix( + cm, + class_names=None, + normalize=False, + title="Confusion Matrix", + save_path=None, + figsize=(10, 8), + cmap="Blues", +): + """ + Plot a confusion matrix. + + """ + if normalize: + cm = cm.astype("float") / cm.sum(axis=1)[:, np.newaxis] + fmt = ".2f" + else: + fmt = "d" + + if class_names is None: + class_names = [f"Class {i}" for i in range(cm.shape[0])] + + plt.figure(figsize=figsize) + sns.heatmap( + cm, + annot=True, + fmt=fmt, + cmap=cmap, + xticklabels=class_names, + yticklabels=class_names, + ) + plt.xlabel("Predicted") + plt.ylabel("True") + plt.title(title) + plt.tight_layout() + + if save_path: + plt.savefig(save_path) + + return plt.gcf() + + +def plot_roc_curve(fpr, tpr, roc_auc, save_path=None, figsize=(8, 6)): + """ + Plot a ROC curve + """ + plt.figure(figsize=figsize) + plt.plot(fpr, tpr, lw=2, label=f"ROC curve (area = {roc_auc:.2f})") + plt.plot([0, 1], [0, 1], "k--", lw=2) + plt.xlim([0.0, 1.0]) + plt.ylim([0.0, 1.05]) + plt.xlabel("False Positive Rate") + plt.ylabel("True Positive Rate") + plt.title("Receiver Operating Characteristic (ROC) Curve") + plt.legend(loc="lower right") + + if save_path: + plt.savefig(save_path) + + return plt.gcf() + + +def plot_precision_recall_curve( + precision, recall, average_precision, save_path=None, figsize=(8, 6) +): + """ + Plot a precision-recall curve + """ + plt.figure(figsize=figsize) + plt.plot(recall, precision, lw=2, label=f"PR curve (AP = {average_precision:.2f})") + plt.xlim([0.0, 1.0]) + plt.ylim([0.0, 1.05]) + plt.xlabel("Recall") + plt.ylabel("Precision") + plt.title("Precision-Recall Curve") + plt.legend(loc="lower left") + + if save_path: + plt.savefig(save_path) + + return plt.gcf() + + +def log_plots_to_mlflow(metrics, class_names=None, output_dir=None): + """ + Create and log plots to MLflow based on computed metrics + """ + if output_dir: + os.makedirs(output_dir, exist_ok=True) + + # Plot confusion matrix + if "confusion_matrix" in metrics: + cm = metrics["confusion_matrix"] + cm_path = ( + os.path.join(output_dir, "confusion_matrix.png") if output_dir else None + ) + plot_confusion_matrix(cm, class_names=class_names, save_path=cm_path) + + if cm_path: + mlflow.log_artifact(cm_path) + + # Plot ROC curve for binary classification + if "roc_curve" in metrics: + fpr = metrics["roc_curve"]["fpr"] + tpr = metrics["roc_curve"]["tpr"] + roc_auc = metrics["roc_auc"] + + roc_path = os.path.join(output_dir, "roc_curve.png") if output_dir else None + plot_roc_curve(fpr, tpr, roc_auc, save_path=roc_path) + + if roc_path: + mlflow.log_artifact(roc_path) + + # Plot precision-recall curve + if "pr_curve" in metrics: + precision = metrics["pr_curve"]["precision"] + recall = metrics["pr_curve"]["recall"] + avg_precision = metrics["pr_auc"] + + pr_path = os.path.join(output_dir, "pr_curve.png") if output_dir else None + plot_precision_recall_curve(precision, recall, avg_precision, save_path=pr_path) + + if pr_path: + mlflow.log_artifact(pr_path) + + +def measure_model_size(model): + """ + Measure the size of a PyTorch model in MB. + + Args: + model (torch.nn.Module): The model to measure + + Returns: + float: Size of the model in MB + """ + model_size = 0 + for param in model.parameters(): + model_size += param.nelement() * param.element_size() + + # Convert to MB + model_size_mb = model_size / (1024 * 1024) + return model_size_mb + + +def measure_inference_speed(model, sample_input, device, num_runs=100, warm_up=10): + """ + Measure the inference speed of a model + """ + model.eval() + + # Move inputs to the appropriate device + input_ids = sample_input["input_ids"].to(device) + attention_mask = sample_input["attention_mask"].to(device) + + # Warm-up runs + with torch.no_grad(): + for _ in range(warm_up): + _ = model(input_ids=input_ids, attention_mask=attention_mask) + + # Measure inference time + start_time = time.time() + with torch.no_grad(): + for _ in range(num_runs): + _ = model(input_ids=input_ids, attention_mask=attention_mask) + end_time = time.time() + + avg_time = (end_time - start_time) / num_runs + return avg_time + + +def log_model_metadata(model, tokenizer, config=None, output_dir=None): + """ + Log model metadata to a file and MLflow + """ + if output_dir: + os.makedirs(output_dir, exist_ok=True) + + # Collect model metadata + model_info = { + "model_type": model.__class__.__name__, + "model_parameters": sum(p.numel() for p in model.parameters()), + "trainable_parameters": sum( + p.numel() for p in model.parameters() if p.requires_grad + ), + "tokenizer_type": tokenizer.__class__.__name__, + "vocab_size": len(tokenizer), + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + + # Add any additional config + if config: + model_info.update(config) + + # Save metadata to file + if output_dir: + metadata_path = os.path.join(output_dir, "model_metadata.json") + with open(metadata_path, "w") as f: + json.dump(model_info, f, indent=2) + + # Log to MLflow + mlflow.log_dict(model_info, "model_metadata.json") + + return model_info + + +def analyze_dataset(data_path, text_col="text", label_col="label", output_dir=None): + """ + Analyze a dataset and generate statistics. + + Args: + data_path (str): Path to the dataset + text_col (str): Name of the column containing the text + label_col (str): Name of the column containing the labels + output_dir (str, optional): Directory to save the analysis results + + Returns: + dict: Dataset statistics + """ + # Load the data + if data_path.endswith(".csv"): + df = pd.read_csv(data_path) + elif data_path.endswith(".json"): + df = pd.read_json(data_path, lines=True) + else: + raise ValueError(f"Unsupported file format: {data_path}") + + # Basic dataset statistics + stats = { + "num_samples": len(df), + "num_classes": len(df[label_col].unique()), + "class_distribution": df[label_col].value_counts().to_dict(), + "avg_text_length": df[text_col].str.len().mean(), + "min_text_length": df[text_col].str.len().min(), + "max_text_length": df[text_col].str.len().max(), + "median_text_length": df[text_col].str.len().median(), + } + + # Plot class distribution + if output_dir: + os.makedirs(output_dir, exist_ok=True) + + # Class distribution plot + plt.figure(figsize=(10, 6)) + sns.countplot( + y=df[label_col].astype(str), + order=df[label_col].value_counts().index.astype(str), + ) + plt.title("Class Distribution") + plt.xlabel("Count") + plt.ylabel("Class") + plt.tight_layout() + + class_dist_path = os.path.join(output_dir, "class_distribution.png") + plt.savefig(class_dist_path) + plt.close() + + # Text length distribution + plt.figure(figsize=(10, 6)) + sns.histplot(df[text_col].str.len(), bins=50) + plt.title("Text Length Distribution") + plt.xlabel("Text Length (characters)") + plt.ylabel("Count") + plt.tight_layout() + + text_len_path = os.path.join(output_dir, "text_length_distribution.png") + plt.savefig(text_len_path) + plt.close() + + # Save statistics to a JSON file + stats_path = os.path.join(output_dir, "dataset_stats.json") + with open(stats_path, "w") as f: + # Convert any non-serializable values (like numpy.int64) to standard Python types + stats_serializable = { + k: v if isinstance(v, (str, int, float, bool, list, dict)) else int(v) + for k, v in stats.items() + } + json.dump(stats_serializable, f, indent=2) + + # Log to MLflow if active + try: + mlflow.log_artifact(class_dist_path) + mlflow.log_artifact(text_len_path) + mlflow.log_dict(stats, "dataset_stats.json") + except Exception as e: + print(f"Warning: Could not log dataset analysis to MLflow: {str(e)}") + # MLflow might nt be active + + return stats + + +def format_time(seconds): + """ + Format time in seconds to a human-readable string. + + Args: + seconds (float): Time in seconds + + Returns: + str: Formatted time string + """ + if seconds < 1e-3: # Less than a millisecond + return f"{seconds * 1e6:.2f} µs" + elif seconds < 1: # Less than a second + return f"{seconds * 1e3:.2f} ms" + elif seconds < 60: # Less than a minute + return f"{seconds:.2f} s" + elif seconds < 3600: # Less than an hour + minutes = int(seconds // 60) + secs = seconds % 60 + return f"{minutes}m {secs:.2f}s" + else: # Hours or more + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = seconds % 60 + return f"{hours}h {minutes}m {secs:.2f}s" + + +def compare_models(metrics_list, model_names=None, output_dir=None): + """ + Compare multiple models based on their metrics + """ + if model_names is None: + model_names = [f"Model {i + 1}" for i in range(len(metrics_list))] + + if len(model_names) != len(metrics_list): + raise ValueError( + "Number of model names must match number of metric dictionaries" + ) + + # Extract common metrics for comparison + common_metrics = [ + "accuracy", + "precision", + "recall", + "f1", + "roc_auc", + "pr_auc", + "fpr@95tpr", + "avg_inference_time_seconds", + "training_time_seconds", + ] + + # Create comparison dictionary + comparison = {name: {} for name in model_names} + + for i, (name, metrics) in enumerate(zip(model_names, metrics_list)): + for metric in common_metrics: + if metric in metrics: + comparison[name][metric] = metrics[metric] + # Handle nested metrics + elif "test_metrics" in metrics and metric in metrics["test_metrics"]: + comparison[name][metric] = metrics["test_metrics"][metric] + + # Convert to DataFrame for easier comparison + comp_df = pd.DataFrame(comparison).T + + # Format time metrics if present + time_cols = [col for col in comp_df.columns if "time" in col] + for col in time_cols: + if col in comp_df.columns: + comp_df[f"{col}_formatted"] = comp_df[col].apply(format_time) + + # Save comparison to CSV and JSON + if output_dir: + os.makedirs(output_dir, exist_ok=True) + + # Save as CSV + csv_path = os.path.join(output_dir, "model_comparison.csv") + comp_df.to_csv(csv_path) + + # Save as JSON + json_path = os.path.join(output_dir, "model_comparison.json") + comp_df.to_json(json_path, orient="index", indent=2) + + # Create comparison plots + for metric in common_metrics: + if metric in comp_df.columns: + plt.figure(figsize=(10, 6)) + + # Skip time metrics for bar plots (they're often on different scales) + if "time" in metric: + continue + + # Create bar plot + ax = sns.barplot(x=comp_df.index, y=comp_df[metric]) + plt.title(f"Comparison of {metric}") + plt.ylabel(metric) + plt.xlabel("Model") + + # Add value labels on top of each bar + for i, v in enumerate(comp_df[metric]): + ax.text(i, v, f"{v:.4f}", ha="center", va="bottom") + + plt.xticks(rotation=45) + plt.tight_layout() + + # Save the plot + plot_path = os.path.join(output_dir, f"compare_{metric}.png") + plt.savefig(plot_path) + plt.close() + + # Log to MLflow if active + try: + mlflow.log_artifact(plot_path) + except Exception as e: + print( + f"Warning: Could not log plot {plot_path} to MLflow: {str(e)}" + ) + # MLflow might not be active + + # Log to MLflow if active + try: + mlflow.log_artifact(csv_path) + mlflow.log_artifact(json_path) + except Exception as e: + print(f"Warning: Could not log comparison files to MLflow: {str(e)}") + # MLflow might not be active + + return comp_df diff --git a/experiments/ood_detection/README.md b/experiments/ood_detection/README.md new file mode 100644 index 00000000..e69de29b diff --git a/experiments/ood_detection/__init__.py b/experiments/ood_detection/__init__.py new file mode 100644 index 00000000..d1695ac1 --- /dev/null +++ b/experiments/ood_detection/__init__.py @@ -0,0 +1,2 @@ +# OOD Detection Framework +__version__ = "1.0.0" diff --git a/experiments/ood_detection/config/__init__.py b/experiments/ood_detection/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/experiments/ood_detection/config/config.py b/experiments/ood_detection/config/config.py new file mode 100644 index 00000000..47a93f54 --- /dev/null +++ b/experiments/ood_detection/config/config.py @@ -0,0 +1,143 @@ +""" +Configuration file for the OOD detection experiments. +""" + +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class ModelConfig: + """Base configuration for all models.""" + + name: str + model_type: str + hidden_dims: List[int] = None + dropout_rate: float = 0.1 + learning_rate: float = 1e-4 + batch_size: int = 32 + epochs: int = 10 + max_seq_length: int = 512 + pretrained_model: str = "xlm-roberta-base" + seed: int = 42 + + +@dataclass +class SNGPConfig(ModelConfig): + """Configuration for SNGP model.""" + + model_type: str = "sngp" + spec_norm_bound: float = 0.9 + gp_hidden_dim: int = 1024 + gp_scale_random_features: bool = True + gp_cov_momentum: float = -1.0 # No momentum, reset covariance each epoch + gp_cov_ridge_penalty: float = 1.0 + + +@dataclass +class EnergyConfig(ModelConfig): + """Configuration for Energy-based OOD detection.""" + + model_type: str = "energy" + energy_temp: float = 1.0 + + +@dataclass +class SNGPEnergyConfig(SNGPConfig, EnergyConfig): + """Configuration for combined SNGP and Energy-based OOD detection.""" + + model_type: str = "sngp_energy" + alpha: float = 0.5 # Weight for combining SNGP and Energy scores (0-1) + + +@dataclass +class OODClassConfig(ModelConfig): + """Configuration for OOD as a class approach.""" + + model_type: str = "ood_class" + # Percentage of data to use as synthetic OOD + synthetic_ood_ratio: float = 0.2 + + +@dataclass +class SoftmaxConfig(ModelConfig): + """Configuration for Softmax threshold OOD detection.""" + + model_type: str = "softmax" + # Temperature scaling for softmax calibration + temperature: float = 1.0 + # Whether to use entropy instead of max probability + use_entropy: bool = False + + +@dataclass +class TrainingConfig: + """Configuration for training process.""" + + train_file: str + dev_file: str + test_file: str + ood_test_file: Optional[str] = None + output_dir: str = "outputs" + mlflow_tracking_uri: str = "mlruns" + mlflow_experiment_name: str = "ood_detection" + model_config: ModelConfig = None + num_train_epochs: int = 10 + per_device_train_batch_size: int = 32 + per_device_eval_batch_size: int = 32 + warmup_steps: int = 0 + weight_decay: float = 0.01 + logging_steps: int = 100 + evaluation_strategy: str = "epoch" + save_strategy: str = "epoch" + fp16: bool = False + + +@dataclass +class EvaluationConfig: + """Configuration for evaluation metrics.""" + + metrics: List[str] = None + + def __post_init__(self): + if self.metrics is None: + self.metrics = [ + "accuracy", + "precision", + "recall", + "f1", + "roc_auc", + "pr_auc", + "fpr_at_95_tpr", + "auroc", + "aupr", + "confusion_matrix", + ] + + +# Default configurations +DEFAULT_SNGP_CONFIG = SNGPConfig(name="sngp_model") +DEFAULT_ENERGY_CONFIG = EnergyConfig(name="energy_model") +DEFAULT_SNGP_ENERGY_CONFIG = SNGPEnergyConfig(name="sngp_energy_model") +DEFAULT_OOD_CLASS_CONFIG = OODClassConfig(name="ood_class_model") +DEFAULT_SOFTMAX_CONFIG = SoftmaxConfig(name="softmax_model") + +# Experiment configurations +EXPERIMENT_CONFIGS = { + "sngp": DEFAULT_SNGP_CONFIG, + "energy": DEFAULT_ENERGY_CONFIG, + "sngp_energy": DEFAULT_SNGP_ENERGY_CONFIG, + "ood_class": DEFAULT_OOD_CLASS_CONFIG, + "softmax": DEFAULT_SOFTMAX_CONFIG, +} + +# Training configurations +TRAINING_CONFIG = TrainingConfig( + train_file="data/train.csv", + dev_file="data/dev.csv", + test_file="data/test.csv", + ood_test_file="data/ood_test.csv", +) + +# Evaluation configuration +EVALUATION_CONFIG = EvaluationConfig() diff --git a/experiments/ood_detection/data/__init__.py b/experiments/ood_detection/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/experiments/ood_detection/data/data_converter.py b/experiments/ood_detection/data/data_converter.py new file mode 100644 index 00000000..9ccb306d --- /dev/null +++ b/experiments/ood_detection/data/data_converter.py @@ -0,0 +1,396 @@ +#!/usr/bin/env python3 +""" +CSV Conversion Script for OOD Detection Framework + +This script converts CSV files with turn-by-turn conversation data to the format +required by the OOD detection framework. + +Input format: +conversation_id | turn | speaker | text | agency + +Output format: +conversation | agency + +Usage: + python convert_csv.py input.csv output.csv [--format-style bot_prefix] +""" + +import pandas as pd +import argparse +import sys +import os +from typing import List, Optional +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description="Convert turn-by-turn conversation CSV to OOD detection format" + ) + + parser.add_argument( + "input_file", + type=str, + help="Path to input CSV file with turn-by-turn conversations", + ) + + parser.add_argument( + "output_file", type=str, help="Path to output CSV file in OOD detection format" + ) + + parser.add_argument( + "--format-style", + type=str, + choices=["bot_prefix", "speaker_prefix", "clean"], + default="bot_prefix", + help="How to format the conversation text (default: bot_prefix)", + ) + + parser.add_argument( + "--speaker-mapping", + type=str, + nargs=2, + metavar=("USER_SPEAKER", "BOT_SPEAKER"), + default=["user", "bot"], + help="Mapping for speaker names (default: user bot)", + ) + + parser.add_argument( + "--exclude-conversations", + type=str, + nargs="*", + help="List of conversation IDs to exclude from conversion", + ) + + parser.add_argument( + "--min-turns", + type=int, + default=2, + help="Minimum number of turns required for a conversation (default: 2)", + ) + + parser.add_argument( + "--max-turns", + type=int, + default=None, + help="Maximum number of turns to include per conversation", + ) + + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + + return parser.parse_args() + + +def load_and_validate_data(input_file: str) -> pd.DataFrame: + """ + Load CSV data and validate required columns. + + Args: + input_file: Path to input CSV file + + Returns: + DataFrame with validated data + + Raises: + ValueError: If required columns are missing + FileNotFoundError: If input file doesn't exist + """ + if not os.path.exists(input_file): + raise FileNotFoundError(f"Input file not found: {input_file}") + + logger.info(f"Loading data from {input_file}") + df = pd.read_csv(input_file) + + # Check required columns + required_columns = ["conversation_id", "turn", "speaker", "text", "agency"] + missing_columns = [col for col in required_columns if col not in df.columns] + + if missing_columns: + raise ValueError(f"Missing required columns: {missing_columns}") + + logger.info(f"Loaded {len(df)} rows with columns: {list(df.columns)}") + + # Basic data validation + null_counts = df[required_columns].isnull().sum() + if null_counts.any(): + logger.warning("Found null values:") + for col, count in null_counts.items(): + if count > 0: + logger.warning(f" {col}: {count} null values") + + return df + + +def clean_and_filter_data( + df: pd.DataFrame, + exclude_conversations: Optional[List[str]] = None, + min_turns: int = 2, + max_turns: Optional[int] = None, +) -> pd.DataFrame: + """ + Clean and filter the conversation data. + + Args: + df: Input DataFrame + exclude_conversations: List of conversation IDs to exclude + min_turns: Minimum number of turns required + max_turns: Maximum number of turns per conversation + + Returns: + Cleaned and filtered DataFrame + """ + logger.info("Cleaning and filtering data...") + + # Remove rows with null values in critical columns + df = df.dropna(subset=["conversation_id", "text", "agency"]) + + # Exclude specific conversations if requested + if exclude_conversations: + logger.info(f"Excluding {len(exclude_conversations)} conversations") + df = df[~df["conversation_id"].isin(exclude_conversations)] + + # Clean text data + df["text"] = df["text"].astype(str).str.strip() + df = df[df["text"] != ""] # Remove empty text + + # Sort by conversation_id and turn + df = df.sort_values(["conversation_id", "turn"]) + + # Filter conversations by turn count + conversation_turn_counts = df.groupby("conversation_id").size() + + if min_turns > 1: + valid_conversations = conversation_turn_counts[ + conversation_turn_counts >= min_turns + ].index + df = df[df["conversation_id"].isin(valid_conversations)] + logger.info(f"Filtered to conversations with at least {min_turns} turns") + + if max_turns: + # Keep only the first max_turns for each conversation + df = df.groupby("conversation_id").head(max_turns) + logger.info(f"Limited conversations to maximum {max_turns} turns") + + logger.info( + f"After filtering: {len(df)} rows, {df['conversation_id'].nunique()} conversations" + ) + + return df + + +def format_conversation_text( + conversation_df: pd.DataFrame, format_style: str, speaker_mapping: List[str] +) -> str: + """ + Format conversation turns into a single text string. + + Args: + conversation_df: DataFrame containing turns for a single conversation + format_style: How to format the conversation + speaker_mapping: [user_speaker, bot_speaker] names + + Returns: + Formatted conversation string + """ + user_speaker, bot_speaker = speaker_mapping + turns = [] + + for _, row in conversation_df.iterrows(): + speaker = str(row["speaker"]).lower().strip() + text = str(row["text"]).strip() + + if format_style == "bot_prefix": + # Format: "User: text Bot: text" + if speaker == user_speaker.lower(): + turns.append(f"User: {text}") + elif speaker == bot_speaker.lower(): + turns.append(f"Bot: {text}") + else: + # Handle unknown speakers + turns.append(f"{speaker.title()}: {text}") + + elif format_style == "speaker_prefix": + # Format: "user: text bot: text" + turns.append(f"{speaker}: {text}") + + elif format_style == "clean": + # Format: just the text without speaker prefixes + turns.append(text) + + return " ".join(turns) + + +def convert_conversations( + df: pd.DataFrame, + format_style: str = "bot_prefix", + speaker_mapping: List[str] = ["user", "bot"], +) -> pd.DataFrame: + """ + Convert turn-by-turn conversations to single-row format. + + Args: + df: Input DataFrame with turn-by-turn data + format_style: How to format conversations + speaker_mapping: Speaker name mapping + + Returns: + DataFrame with converted conversations + """ + logger.info("Converting conversations...") + + converted_conversations = [] + + # Group by conversation_id and agency + grouped = df.groupby(["conversation_id", "agency"]) + + for (conv_id, agency), group in grouped: + # Sort by turn number + group = group.sort_values("turn") + + # Format the conversation text + conversation_text = format_conversation_text( + group, format_style, speaker_mapping + ) + + converted_conversations.append( + { + "conversation": conversation_text, + "agency": agency, + "conversation_id": conv_id, # Keep for reference + "num_turns": len(group), + } + ) + + result_df = pd.DataFrame(converted_conversations) + logger.info(f"Converted {len(result_df)} conversations") + + return result_df + + +def save_output(df: pd.DataFrame, output_file: str, include_metadata: bool = False): + """ + Save the converted data to output file. + + Args: + df: Converted DataFrame + output_file: Path to output file + include_metadata: Whether to include metadata columns + """ + # Create output directory if it doesn't exist + output_dir = os.path.dirname(output_file) + if output_dir and not os.path.exists(output_dir): + os.makedirs(output_dir) + + # Select columns for output + if include_metadata: + output_columns = ["conversation", "agency", "conversation_id", "num_turns"] + else: + output_columns = ["conversation", "agency"] + + # Save to CSV + df[output_columns].to_csv(output_file, index=False) + logger.info(f"Saved {len(df)} conversations to {output_file}") + + +def print_sample_conversions(df: pd.DataFrame, num_samples: int = 3): + """Print sample conversions for verification.""" + logger.info(f"\nSample conversions (showing first {num_samples}):") + logger.info("=" * 80) + + for i, row in df.head(num_samples).iterrows(): + logger.info(f"Conversation {i + 1}:") + logger.info(f"Agency: {row['agency']}") + logger.info(f"Text: {row['conversation'][:200]}...") + logger.info(f"Turns: {row.get('num_turns', 'N/A')}") + logger.info("-" * 40) + + +def generate_statistics(original_df: pd.DataFrame, converted_df: pd.DataFrame): + """Generate and display conversion statistics.""" + logger.info("\nConversion Statistics:") + logger.info("=" * 40) + + # Original data stats + orig_conversations = original_df["conversation_id"].nunique() + orig_turns = len(original_df) + orig_agencies = original_df["agency"].nunique() + + logger.info("Original data:") + logger.info(f" Conversations: {orig_conversations}") + logger.info(f" Total turns: {orig_turns}") + logger.info(f" Unique agencies: {orig_agencies}") + logger.info( + f" Average turns per conversation: {orig_turns / orig_conversations:.1f}" + ) + + # Converted data stats + conv_conversations = len(converted_df) + conv_agencies = converted_df["agency"].nunique() + + logger.info("\nConverted data:") + logger.info(f" Conversations: {conv_conversations}") + logger.info(f" Unique agencies: {conv_agencies}") + + if "num_turns" in converted_df.columns: + avg_turns = converted_df["num_turns"].mean() + logger.info(f" Average turns per conversation: {avg_turns:.1f}") + + # Agency distribution + logger.info("\nAgency distribution:") + agency_counts = converted_df["agency"].value_counts() + for agency, count in agency_counts.items(): + logger.info(f" {agency}: {count}") + + +def main(): + """Main conversion function.""" + args = parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + try: + # Load and validate input data + df = load_and_validate_data(args.input_file) + + # Clean and filter data + df = clean_and_filter_data( + df, + exclude_conversations=args.exclude_conversations, + min_turns=args.min_turns, + max_turns=args.max_turns, + ) + + if len(df) == 0: + logger.error("No data remaining after filtering!") + sys.exit(1) + + # Convert conversations + converted_df = convert_conversations( + df, format_style=args.format_style, speaker_mapping=args.speaker_mapping + ) + + # Generate statistics + generate_statistics(df, converted_df) + + # Show sample conversions + if args.verbose: + print_sample_conversions(converted_df) + + # Save output + save_output(converted_df, args.output_file, include_metadata=args.verbose) + + logger.info("\nConversion completed successfully!") + logger.info(f"Output saved to: {args.output_file}") + + except Exception as e: + logger.error(f"Error during conversion: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/experiments/ood_detection/data/data_loader.py b/experiments/ood_detection/data/data_loader.py new file mode 100644 index 00000000..c4149399 --- /dev/null +++ b/experiments/ood_detection/data/data_loader.py @@ -0,0 +1,270 @@ +""" +Data loading and preprocessing for the OOD detection models. +""" + +import pandas as pd +import tensorflow as tf +import numpy as np +from typing import Tuple, List, Dict +from transformers import AutoTokenizer +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class DataLoader: + """Data loader for conversation classification with OOD detection.""" + + def __init__( + self, + tokenizer_name: str = "xlm-roberta-base", + max_seq_length: int = 512, + batch_size: int = 32, + seed: int = 42, + ): + """ + Initialize the data loader. + + Args: + tokenizer_name: Name or path of the tokenizer to use + max_seq_length: Maximum sequence length for tokenization + batch_size: Batch size for training and evaluation + seed: Random seed for reproducibility + """ + self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) + self.max_seq_length = max_seq_length + self.batch_size = batch_size + self.seed = seed + + # Set random seed for reproducibility + tf.random.set_seed(seed) + np.random.seed(seed) + + def load_data( + self, data_path: str, is_training: bool = True, add_ood_class: bool = False + ) -> Tuple[tf.data.Dataset, Dict[str, int], int]: + """ + Load data from a CSV file. + + Args: + data_path: Path to the CSV file + is_training: Whether this is training data + add_ood_class: Whether to add an OOD class to the label mapping + + Returns: + dataset: TensorFlow dataset + label_map: Mapping from label names to IDs + num_examples: Number of examples in the dataset + """ + logger.info(f"Loading data from {data_path}") + + # Load data + df = pd.read_csv(data_path) + + # Check required columns + required_cols = ["conversation", "agency"] + if not all(col in df.columns for col in required_cols): + raise ValueError(f"Data file must contain columns: {required_cols}") + + # Create label mapping + unique_labels = sorted(df["agency"].unique()) + logger.info(f"Found {len(unique_labels)} unique labels: {unique_labels}") + + label_map = {label: i for i, label in enumerate(unique_labels)} + + # Add OOD class if specified + if add_ood_class: + label_map["OOD"] = len(label_map) + + # Convert labels to IDs + label_ids = df["agency"].map(label_map).values + + # Tokenize conversations - ensure they are strings + conversations = df["conversation"].astype(str).values + tokenized_inputs = self._tokenize_text(conversations) + + # Create dataset + dataset = tf.data.Dataset.from_tensor_slices( + ( + { + "input_ids": tokenized_inputs["input_ids"], + "attention_mask": tokenized_inputs["attention_mask"], + }, + label_ids, + ) + ) + + # Prepare dataset for training or evaluation + if is_training: + dataset = dataset.shuffle(buffer_size=len(df), seed=self.seed) + + dataset = dataset.batch(self.batch_size) + dataset = dataset.prefetch(tf.data.AUTOTUNE) + + return dataset, label_map, len(df) + + def load_ood_data( + self, data_path: str, label_map: Dict[str, int] + ) -> tf.data.Dataset: + """ + Load OOD data for evaluation. + + Args: + data_path: Path to the OOD data CSV file + label_map: Mapping from label names to IDs + + Returns: + dataset: TensorFlow dataset + """ + logger.info(f"Loading OOD data from {data_path}") + + # Load data + df = pd.read_csv(data_path) + + # Check required columns + if "conversation" not in df.columns: + raise ValueError("Data file must contain a 'conversation' column") + + # Assign OOD label if present in label map, otherwise use -1 + ood_label = label_map.get("OOD", -1) + labels = np.full(len(df), ood_label) + + # Tokenize conversations - ensure they are strings + conversations = df["conversation"].astype(str).values + tokenized_inputs = self._tokenize_text(conversations) + + # Create dataset + dataset = tf.data.Dataset.from_tensor_slices( + ( + { + "input_ids": tokenized_inputs["input_ids"], + "attention_mask": tokenized_inputs["attention_mask"], + }, + labels, + ) + ) + + dataset = dataset.batch(self.batch_size) + dataset = dataset.prefetch(tf.data.AUTOTUNE) + + return dataset + + def create_synthetic_ood( + self, + train_dataset: tf.data.Dataset, + label_map: Dict[str:int], + ratio: float = 0.2, + ) -> tf.data.Dataset: + """ + Create synthetic OOD examples for training. + + Args: + train_dataset: The original training dataset + ratio: The ratio of synthetic OOD examples to add + + Returns: + dataset: The combined dataset with synthetic OOD examples + """ + logger.info(f"Creating synthetic OOD examples with ratio {ratio}") + + # Convert dataset to numpy arrays + all_data = [] + for features, labels in train_dataset: + for i in range(len(labels)): + input_ids = features["input_ids"][i].numpy() + attention_mask = features["attention_mask"][i].numpy() + label = labels[i].numpy() + all_data.append((input_ids, attention_mask, label)) + + # Calculate number of synthetic examples + num_orig = len(all_data) + num_synth = int(num_orig * ratio) + + # Create synthetic examples by shuffling tokens + synthetic_data = [] + for _ in range(num_synth): + # Randomly select an example + idx = np.random.randint(0, num_orig) + input_ids, attention_mask, _ = all_data[idx] + + # Shuffle the tokens (excluding special tokens) + special_tokens = [ + self.tokenizer.cls_token_id, + self.tokenizer.sep_token_id, + self.tokenizer.pad_token_id, + ] + mask = np.ones_like(input_ids, dtype=bool) + for token_id in special_tokens: + mask = mask & (input_ids != token_id) + + # Get non-special token positions + token_positions = np.where(mask)[0] + + if len(token_positions) > 0: + # Shuffle these positions + np.random.shuffle(token_positions) + shuffled_input_ids = input_ids.copy() + + # Apply shuffling + original_tokens = input_ids[mask] + shuffled_input_ids[mask] = np.random.permutation(original_tokens) + + # Add to synthetic data with OOD label (-1 or the OOD class index) + synthetic_data.append( + (shuffled_input_ids, attention_mask, label_map.get("OOD", -1)) + ) + + # Combine original and synthetic data + combined_data = all_data + synthetic_data + np.random.shuffle(combined_data) + + # Convert back to TensorFlow dataset + def gen(): + for input_ids, attention_mask, label in combined_data: + yield {"input_ids": input_ids, "attention_mask": attention_mask}, label + + # Create dataset with the appropriate shapes and types + input_shape = all_data[0][0].shape + output_dataset = tf.data.Dataset.from_generator( + gen, + output_types=( + {"input_ids": tf.int32, "attention_mask": tf.int32}, + tf.int32, + ), + output_shapes=( + { + "input_ids": tf.TensorShape(input_shape), + "attention_mask": tf.TensorShape(input_shape), + }, + tf.TensorShape([]), + ), + ) + + return output_dataset.batch(self.batch_size).prefetch(tf.data.AUTOTUNE) + + def _tokenize_text(self, texts: List[str]) -> Dict[str, np.ndarray]: + """ + Tokenize a list of texts. + + Args: + texts: List of text strings to tokenize + + Returns: + Dictionary with 'input_ids' and 'attention_mask' + """ + # Ensure all texts are strings and handle any NaN/None values + clean_texts = [] + for text in texts: + if pd.isna(text) or text is None: + clean_texts.append("") + else: + clean_texts.append(str(text)) + + return self.tokenizer( + clean_texts, + padding="max_length", + truncation=True, + max_length=self.max_seq_length, + return_tensors="np", + ) diff --git a/experiments/ood_detection/data/dev.csv b/experiments/ood_detection/data/dev.csv new file mode 100644 index 00000000..5428dc4c --- /dev/null +++ b/experiments/ood_detection/data/dev.csv @@ -0,0 +1,115 @@ +conversation,agency +"user: Tere! Kuidas palju Ukraina kodanikke on nüüd Eestisse saabunud alates 27. veebruarist? +assistant: Tere! Alates 27. veebruarist on Eestisse saabunud kokku 23 886 Ukraina kodanikku ja nende pereliiget.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan uue passi taotleda. Mis ma pean tegema? +assistant: Tere! Noh, uue passi taotlemine on suht lihtne. Alustage kõigepealt leidmaks sobiv teenindus. Palju teeninduskohasid on üle Eesti. Vaadake kindlasti (/et/asukohad/teenindused) kohta, kus teil on mugavam kohapeal abi saada. Peate ka isikut tõendava dokumendi kaasa võtma.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas ma saan lapse välismaalase passi taota? Kas pean minema kohapeale? Kuulsin, et teenindused on olukorraseisu kohta +assistant: Tere! Jah, lapse välismaalase passi saab kindlasti taota. Teeninduses saad seda teha. Kuigi teeninduste asukohad on avaldatud veebilehel (/et/asukohad/teenindused), on oluline, et teada saada, millises teeninduses on hetkel vähem rahvusvaheliste passi taotluste kohta. Kuna piirkondades võib olukord olla erinev, on hea idee enne kohtumist telefoni teel küsida.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et minu ID-kaart on kadunud. Kas ma pean midagi tegema selle kaitse tagamiseks? +assistant: Tere! Jah, väga hea, et mõtled sellele. Kui sa arvad, et ID-kaart on kadunud või varastatud, on oluline kohe sertifikaadid peatada. See aitab varguse korral sinu identiteeti kaitsmata jätta. Kas sa saaksid kasvõi lehel http://www.id.ee vaadata, mis seotud sertifikaate pead peatama?",output_ID.ee +"user: Tere! Ma arvan, et digitaalsed dokumendid on kirsileht... mida see tähendab ja miks need on olulised? +assistant: Tere! Kirsileht on hea võrdlus. Digitaalsed dokumendid on tegelikult elektroonilised tõendid, mis kinnitavad teie identiteeti, näiteks ID-kaart, digi-ID, või e-residentide digi-ID. Nende eesmärk on aidata teiega identifitseerida internetis ja elektroonilistes teenustes. Kas sa arvad, et need on olulised, ja miks?",output_ID.ee +"user: Tere! Ma olen huvitatud amatöörraadiosideks tööloa saamisest. Kuidas see üldiselt toimib? Milliseid liike neid tööloasid on? +assistant: Tere! Amatöörraadiojaama tööloa saamiseks peate esitama taotluse Tarbijakaitse ja Tehnilise Järelevalve Ametile (TTJA). Töölube on mitu liiki. On füüsilisele isikule kuuluv raadiojaam, mis on mõeldud ainuisikuliselt kasutamiseks, juriidilisele isikule või ERAÜ-le/raadioklubile/seltsile kuuluv ühiskasutusega jaam, automaatselt töötav jaam kindlas kohas ja viimaseks, automaatselt töötav jaam, mida kasutatakse antennide häälestamiseks või raadioorienteerumiseks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma tahan teada, kuidas Ida-Harju politseijaoskonda saab. Kus see asub? +assistant: Tere! Ida-Harju politseijaoskond asub aadressil P. Pinna 4, 13615 Tallinn.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud Safety Gate'ist. Mis see täpselt on ja milleks see mõeldud on? +assistant: Tere! Safety Gate on ELi kiire teabevahetussüsteem. See on mõeldud selleks, et aidata ennetada ja piirata tarbijatele ohtlikke tooteid ELi turul. See võimaldab ka jälgida liikmesriikide ametiasutuste tööd turujärelevalve ja jõustamistegevuse osas.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin, et Tarbijavaidluste Komisjon (KKK) on olemas. Mis see komisjon tegelikult on ja mis on selle eesmärk? +assistant: Tere! Tarbijavaidluste komisjon on Tarbijakaitse ja Tehnilise Järelevalve Ameti juures tegutsev organ, mis lahendab tarbijavaidlusi kauplejatega. See on koht, kus sa saad pöörduda, kui sul on probleem mingi ostuga ja sa ei saa kauplejaga ise kokkuleppele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin, et Tarbijakaitse Amet tegeleb elektriohutusega. Mis see täpsemalt tähendab? +assistant: Tere! Tõepoolest, me tegeleme sellega. Elektriohutus tähendab, et elektribeede ja -paigaldiste kasutamisel tuleb tagada inimeste elu, tervist, vara ja keskkonna ohutus. See tähendab, et kõik peab olema korras ja turvaline.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on Rail Balticu kohta paar küsimust. Näiteks, miks on uus raudtee trass läbi Harju-, Rapla- ja Pärnumaa valiti, kui läbi Tartu oleks ka võimalik? +assistant: Tere! Hea, et küsid. Ekspertide analüüs näitas, et Tartu kaudu läheks raudtee rajamine märkimisväärselt kallimaks ja mõjuks rohkem nii loodusele kui inimeste elukorraldust. Seega valiti loodust ja inimkeskkonda säästvam ning lühem trass läbi Harju-, Rapla- ja Pärnumaa. Kas see selgitab asja?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma sain probleeme ID-tarkvara paigaldamisel. Kas teil on mõni idee, mis võiks olla põhjus? +assistant: Tere! Ma saan aru, et teil on raskusi ID-tarkvara paigaldamisega. See on levinum probleem, kui Windows operatsioonisüsteem on tehtud uuendusi. Ilmselt Windowsi registris olevad vaimed vanad ID-tarkvara komponendid ei sobi uute versioonidega. Kas sa tead, mis vea kood oli?",output_ID.ee +"user: Tere! Ma kuulsin midagi ID-kaartide sertifikaatide kohta. Mis juhtus? +assistant: Tere! Jah, 2018. aasta 1. aprillist tunnistati turvanõrkusega ID-kaardi sertifikaadid kehtetuks. See tähendab, et kui sa neid ei uuendanud 31. märtsiks 2018, siis on need sertifikaadid nüüd kasutuselt maha võetud.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma hakkan e-poest ostma ja natuke olen segadus. Kust ma peaksin algama, et end kaitsta? +assistant: Tere! Hea, et küsid. Kõigepealt on oluline teada, kas veebipood kuulub juriidilisele isikule või eraisikule. Vaata, kelle pangakontole raha läheb. Eraisikult ostes ei saa sa tarbijana mingeid seadusest tulenevaid kaitseõigusi, näiteks 14-päevast taganemisõigust. Sa ei saa ka Tarbijakaitse Ameti poole pöörduda probleemide korral.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kuulsin, et e-residensi saamine on muutunud keerulisemaks Venemaa ja Valgevene kodanikele. Kas see on tõsi? +assistant: Jah, see on tõsi. Esmakordsed Venemaa ja Valgevene kodanikud ei saa enam Eesti e-residensi digitaalset isikutunnistust taotleda. See piirang jõustus 11. märtsist 2022.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma sooviks teada, kuidas digiallkirja anda ID.ee rakenduses DigiDoc4 kaudu. Kas see on väga keerukas? +assistant: Tere! Ei ole keerukas! Digiallkirja andmine DigiDoc4-s on üsna lihtne. Alguses pead valima, millega sa allkirja annad – kas ID-kaardiga, mobiil-IDga või Smart-IDga. Kas sul on neist ükski neist olemas?",output_ID.ee +"user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnikajärelevalve Amet (TTJA) tegeleb avalike mänguväljakute ohutusega. Kuidas see täpselt töötab? +assistant: Tere! TTJA peab silma peal, et avalikud mänguväljakud ja seikluspargid oleksid ohutud. Nende rajamine või rekonstrueerimine vajab ehitusloa või projektikohustuslikku tegevust. Omanik vastutab nende tehnilise korrasoleku ja ohutuse eest ning peab lähtuma heast ehitustavast.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma plaanin meresõidutunnistust taotleda. Kuidas ma riigilõivu maksmiseks pean tegema? +assistant: Tere! Riigilõivu tasumiseks pead saama vajaliku info. Saaja on Rahandusministeerium. Viitenumber on 2900082443. Pangakontod on erinevad: SEB: EE891010220034796011, Swedbank: EE932200221023778606, Luminor Bank: EE701700017001577198 ja LHV Pank: EE777700771003813400.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Palun, kas teeksite veebilehel id.ee mingisugustid muutusi privaatsustingimustes? +assistant: Tere! Jah, id.ee privaatsustingimused on uuendatud. Peamiselt on täiendatud teavet selle kohta, kuidas veebilehele külastamisel kogutakse andmeid, ning kuidas kasutatakse küpsiseid. Kas sooviks teada rohkem?",output_ID.ee +"user: Tere! Kuulin midagi piirangutest Venemaa kodanikele Eestisse sisenemisel. Kas see on tõsi ja millest see tuleneb? +assistant: Jah, see on tõsi. Vabariigi valitsuse korraldusega piiratakse alates 19. septembrist 2022 ajutiselt Eestisse sisenemist Venemaa Föderatsiooni kodanikele, kellel on kehtiv lühiajaline Schengeni viisa. Korralduse number on 247 ja leiate selle siit: https://www.riigiteataja.ee/akt/310092022002",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma ei ole päris kindel, kuidas riiklikes e-teenustes toimib autentimine. Kas ma pean iga kord, kui ma mõnda teenust saan, uuesti ennast sisse logima? +assistant: Tere! Jah, päriselt on nii. Riiklikes e-teenustes pead alati end autentima. See tähendab, et pead tõendama, et sa oled see, kes väidad olevat. Autentimiseks on saadaval ID-kaart, Mobiil-ID, Smart-ID ja ka Euroopa Liidu riikide autentimisvahendid. Küll aga toimib see keskse turvalise kanali, Riigi autentimisteenuse kaudu. Kas sa mõtled, millise autentimisvahendiga sa tavaliselt seda teed?",output_ID.ee +"user: Tere! Ma olen kuulnud, et enne laenu või järelmaksu võtmist on oluline midagi kontrollida. Mis on kõige tähtsam, mida ma peaksin teadma? +assistant: Tere! See on väga hea küsimus. Enne kui laenu, järelmaksu või liisingu võtate, peaksite kindlasti erinevate krediidiandjate pakkumisi võrdlema ja küsima teabelehte. Teabelehel on kirjas kõik olulised laenutingimused. Samuti võite lepinguprojekti koopia küsida, et see enne allkirjastamist põhjalikult läbi lugeda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma saan uue mobiil-ID SIM-kaardi. Kas ma pean PIN-koodid täna kohe vahetama? +assistant: Tere! Jah, väga soovitame PIN- ja PUK-koodid välja vahetada kohe. Need on kaasa tulnud SIM-kaardi ümbriselt –kraabitava pinna alt. Nende kinnipidasmine on lubamine, et sa soovid mobiil-ID funktsiooni kasutada. Muidugi on kaasa tulnud kolm koodi: isikut tuvastamiseks, digitaalallkirja andmiseks ja lukku läinud koodide taastamiseks.",output_ID.ee +"user: Tere! Ma olen huvitatud amatöörraadiosidega alustamisest. Kust ma peaksin teadma, millist raadiojaama tööloa liiki mulle vaja oleks? +assistant: Tere! Selleks, et teada, millist tööloa liiki sulle vaja on, oleneb see sellest, kas sa kasutad raadiojaama isiklikult või ühiskasutusena. Füüsilisele isikule sobiks isiklik raadiojaam. Kui saama kasutatakse ühiselt, näiteks raadioklubis, siis sobib ühiskasutusega ARJ.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kuulsin, et Politsei- ja Piirivalveamet annab nõu digiturvalisuse kohta. Millised on kõige olulisemad asjad, mida peaksime enda ja laste turvalisuse tagamiseks silmas pidama? +assistant: Tere! Kõige olulisem on tarkvara uuendada – nii seadmete kui ka veebilehitsejate. Aegunud tarkvara teeb meid kergesti pahavara ohvriks. Lisaks, paroolide turvalisus on oluline! Kasutame pikki, raskesti äraarvatavaid parole, mis sisaldavad suuri ja väikseid tähti, numbreid ja kirjavahemärke.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan teada, mis riigilõivu pean maksma, kui tahan uut Eesti kodaniku passi tellida. Leidsin, et link on politsei.ee lehel, aga ma ei leia täpselt seda infot seal. +assistant: Tere! Jah, see on tavaline küsimus. Eesti kodaniku passi tellimisel on riigilõivu. Sa saad selle tellida e-taotluse kaudu: https://etaotlus.politsei.ee/#/login. Konkreetset riigilõivu summat ma hetkel siin öelda ei suuda, aga see on seotud tellimusega. Vaata politsei.ee lehel olevat riigilõivude loendit: https://www.politsei.ee/et/telli-dokument-selverisse, et täpsed summad näha.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahaksin teada, kuidas ma saan lapsele Eesti passi taotleda. Kus ma pean selleks aja broneerima? +assistant: Tere! Eesti passi taotlemiseks lapsele saad aja broneerida PPA teeninduses. Broneerimiseks kasuta veebilehte https://broneering.politsei.ee/. Kas soovid teada ka, kuidas dokumendi kätte saab?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan teada, kuidas Tarbijakaitse Ametis avaldust menetletakse. Mis juhtub, kui ma avalduse esitan? +assistant: Tere! Hea, et küsid. Pärast avalduse esitamist saad 21 tööpäeva jooksul teate, et sinu avaldus on menetlusse võetud. Seejärel edastab sekretariaat avalduse kauplejale, et ta saaks anda selgitusi ja otsida lahendust. Sa saad sellest täiendavalt teavitatud.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma usun, et RIA DigiDoc mobiilirakendus peaks olema kõig jaoks kasutatav. Kas te saate rääkida sellest, kuidas selle ligipääsetavus on? +assistant: Tere! Jah, seda küsitaksegi. RIA DigiDoc mobiilirakendus on ehitatud WCAG 2.1 AA suuniste järgi, et see oleks kasutatav erinevate puudete inimestele. Kuid tegelikult ei ole see täielikult ligipääsetav. Mida tähendab see tekitamas olevas teates?",output_ID.ee +"user: Tere! Ma ostsin äsja kuldsõrmuka ja ma ei saa aru, mida need märgid sellel tähendavad. Mis need proovi- ja nimemärgid tähendavad? +assistant: Tere! Need märgid on väga olulised. Proovimärk näitab, millist metalli on sõrmuse valmistamisel kasutatud ja selle osa sulamis. Näiteks, kui proov on 585, siis see tähendab, et sõrmes on 58,5% puhast kulda tuhande osas. Nimemärk aga näitab tootjat või maaletoimetajat ja see peab olema registreeritud riiklikus registris. Sa leiad selle registreerimise aadressi siin: http://www.proovikoda.ee/register/otsing.php.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kas te raames arvatakse kiipkaardi lugejad? Olen päritlustega probleeme. +assistant: Tere! Jah, ID.ee kiipkaardilugejad on toetatud. Aga probleemid võivad olla mitmetel põhjustel. Mis konkreetset probleemi teil tekkis? Kas näiteks kaardilugeja ei tööta üldse, või on probleem andmete lugemisel?",output_ID.ee +"user: Tere! Ma olen välismaalane ja mu täisealine laps soovib tulla Eestisse elama minu juurde. Kas mul on võimalik talle elamisluba taotleda? +assistant: Jah, see on võimalik! Elamisluba pereliikmega liitumiseks annab sinule võimaluse, kui oled Eesti kodanik või Eestis elamisloa alusel elav välismaalane, kutsuda oma täisealise lapse Eestisse elama, et ta saaks siin sinuga ühiselt elama asuda.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan teada, palju maksab Eesti kodaniku passi tellimine. Ma nägin midagi selle kohta Politsei- ja Piirivalveameti lehel. +assistant: Tere! Eesti kodaniku passi tellimisel on riigilõiv. Selle kohta saad infot Politsei- ja Piirivalveameti veebilehel, täpsemalt siin: https://www.politsei.ee/et/telli-dokument-selverisse. Veebilehel leiad ka viitenumbri, mis on 2900082443.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mida see tekst peamiselt räägib? +assistant: Tekst räägib peamiselt mõõtevahendite direktiivide ja markeerimise nõuetele vastavuse kohta Euroopa Liidus. Seal on loetletud erinevad direktiivid erinevatele mõõtevahenditele ja selgitatud, kuidas neid markeerida, et näidata nende vastavust standarditele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen lapsevanem ja kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet on pannud üles raudteeohutuse õppematerjale. Mis on selle projekti eesmärk? +assistant: Tere! Jah, see on õige. Projekti „Agamina, väikelaste liiklusohutus“ eesmärgiks on toetada väikelaste vanemaid ja õpetajaid, kes tegelevad laste ohutusega liikluses ning viisaka liikleja kujundamisel. See sobib ka sinule, kui sa soovid koos lapsega raudtee ületamise ohutuse teema arutada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kaotanud oma isikut tõendava dokumendi Jõhvis. Kus ma saaksin politsei juurest abi küsida? +assistant: Tere! Jõhvi politseijaoskond asub aadressil Rahu 38, 41588 Jõhvi. Nad on avatud esmaspäevast reedeni kell 9.00 kuni 17.00.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan teada, kuhu saan oma vanu elektroonikaseadmeid viia. +assistant: Tere! Vanu elektroonikaseadmeid saad anda tagastuspunkti. Vanu majapidamismasinaid on Sul võimalik anda ka linna või valla jäätmete kogumispunktidesse või jäätmejaamadesse, patareisid ja akusid aga panna poodides olevatesse spetsiaalsetesse konteineritesse.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma ei ole päris kindel, mida tarkvaraversiooni toeperiood tähendab ID-tarkvaras. Kas sa saaksid seda selgitada? +assistant: Muidugi! Tarkvaraversiooni toeperiood ID-tarkvaras tähendab, et ID kasutajatugi enam selle versiooni kohta teavet teadmistebaasis ei uuenda. See on vôi siis, kui tarkvara versioon enam ei testi probleemide puhul, ja sellele ei vastueta muutusi. Kas see on selge?",output_ID.ee +"user: Tere! Ma pole kunagi faili krüpteerinud. Kas DigiDoc4 abil saab faili turvaliseks teha? +assistant: Tere! Jah, DigiDoc4 võimaldab just seda. Faili krüpteerimine on oluline, et hoida faili sisu eemale võõrastelt pilgutelt. See on nagu omaette karp, mis on lukustatud. Kas sulle on teada, mis krüpteerimine täpselt on?",output_ID.ee +"user: Tere! Ma kuulsin midagi selle kohta, et elektriseadmete puhul on oluline CE-märk. Mis see tähendab ja millal see on nõutav? +assistant: Tere! CE-märk näitab, et elektriseade vastab Euroopa Liidu ohutuse, tervise ja keskkonna nõuetele. Enamus elektriseadmeid, nagu need, mis kuuluvad LVD, EMCD ja RoHS2 direktiivide alla, peavad selle märgist kandma. Eranditeks on näiteks kõrgepingeseadmed ja elektriarvestid. See tagab, et seadmed on ohutud kasutada ja ei ohusta inimesi, kui neid on paigaldatud ja hooldatud vastavalt juhenditele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et e-poodidest ostmine on mugav, aga natuke ka riskantne. Millest ma peaksin aru saama, kui ostan midagi veebist? +assistant: Tere! On hea, et küsid. E-poodidest ostes on oluline teada, kas müüja on juriidiline isik või eraisik. Eraisikult ostes ei kehti seadusest tulenevad kaitseõigused, näiteks 14-päevane taganemisõigus. Veendu, kas raha läheb firmapanga arvele või eraisiku arvele ja kas ettevõte on äriregistris registreeritud.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma nägin internetis kohta MyID portaali. Mis see on? +assistant: Tere! MyID portaal on SK ID Solutions AS (SK) poolt pakutav veebiportaal. See pakub teavet teie Mobiil-ID, ID-kaardi, Digi-ID ja Smart-ID kontode kohta, mis on SK poolt väljaõitud. See on nagu keskoht, kus saate näha, mida te sellede ID-dega teinud olete.",output_ID.ee +"user: Tere! Ma pean külastama Jõgeva politseijaoskonda. Kas sa tead, kus see asub? +assistant: Tere! Jõgeva politseijaoskond asub aadressil Suur 1, 48306 Jõgeva.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et TTJA järelvalvega teeb meediateenused. Mis see tähendab täpselt? +assistant: Tere! Jah, TTJA järelvalvega teeb meediateenused, aga ainult need, kel on Eestist saadud tegevusluba. Saate nimekirja väljastatud tegevuslubadest siit: /ariklient/ametist/load-ja-riigiloivud/meediateenuste-tegevusluba. Meiega saab pöörduda probleemide või tähelepanekute korral, mis puudutavad meediateenuseid.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kas sa saadada mulle infot selle kohta, kui palju Ukraina kodanikke on Eestisse saabunud alates 27. veebruarist? +assistant: Tere! Jah, ma saan. Alates 27. veebruarist on Eestisse saabunud 23 886 Ukraina kodanikku ja nende pereliiget. Kas sulle huvitab veel midagi?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma elan välismaal ja tahaksin teada, kuidas ma saan Eesti passi taotleda? Mis on kõige lihtsam viis? +assistant: Tere! Välismaal on kõige lihtsam passi taotleda kas iseteeninduses aadressil etaotlus.politsei.ee või Eesti Vabariigi välisesinduses. Kas sa soovid, et ma annaksin sulle nimekirja välisesindustest?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan teada, mida ma pean tegema, kui ma ostan midagi internetist ja hiljem kahetsen ostu. +assistant: Tere! Internetist ostetud asjade puhul on Sul võimalik võlaõiguse seaduse järgi 14 päeva jooksul lepingust taganeda ajast, mil kaup Sinuni jõudis. See tähendab, et Sul on aega kaaluda, kas oled sellega tõesti rahul.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et minu ID-kaart on kadunud. Mis ma peaksin tegema, et mu andmeid kaitsta? +assistant: Tere! See on murettekitav. Kui olete kindel, et ID-kaart on kadunud või varastatud, peaksite koheselt sellega seotud sertifikaadid peatama. See on väga tähtis, et varguse korral peaksite oma identiteeti maksimaalselt kaitsma. Kas teiega on selge, mida sertifikaatide peatamine tähendab? Peatamine tähendab, et sertifikaat ei ole enam kehtiv. Kui teiega on selge, siis võime järjadel selle peatamisega tegeleda.",output_ID.ee +"user: Tere! Ma kuulsin, et e-residentsuse saamine on Venemaa ja Valgevene kodanikele muutunud keerulisemaks. Mis juhtus? +assistant: Tere! See on õige. Alates 11. märtsist 2022 on esmakordselt Venemaa ja Valgevene kodanikest taotlejad jäetud e-residentsuse programmiga liitumise võimalusest ilma. Politsei- ja Piirivalveamet peatas selle võimaluse vastavalt peadirektori korraldusele.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb tõstukite ohutusega. Kas teil on aimu, mitu tõstukit Eestis umbes on? +assistant: Tere! See on hea küsimus. Täpset arvu ei tea keegi, aga valdkonnaga seotud ettevõtted arvavad, et tõstukite arv on üle 100 000, ja võib küündida isegi kuni poole miljoni ukseni. See on üsna palju, ja seega on oluline tagada nende ohutus.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma nägin, et on digitembeldamine. Mis see üldse tähendab ja miks seda vaja on? +assistant: Tere! Digitembeldamine tähendab e-templi abil digitaalallkirja andmist. See on nagu kinnitus, et dokument on pärit selle saatnud asutusest ning seda ei ole muudetud. See aitab kindlustada dokumendi autentseid. Kui sa seda enam ei tea, sa saad abi SI ID lahendustes (SK ID Solutions) tehingu teinud teenust.",output_ID.ee +"user: Tere! Mul on küsimus elamislubade kohta. Kas ma saan Eestis ettevõtluseks elamisloa? +assistant: Tere! Jah, seda saab. Kui soovite Eestis ettevõtluseks elamisloa, siis see on võimalik. See nõuab investeeringut Eestis oma äriühingusse. Mida veel täpsemalt teada peaksite?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin midagi numbrite eraldamisest Tarbijakaitse ja Tehnilise Järelevalve Ameti poolt. Mis see tähendab? +assistant: Tere! See tähendab, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) väljastab numbrilube sideteenuse osutajatele, näiteks mobiili- ja telefonifirmadele. Ka sisuteenuse osutajad, kes kasutavad numbreid teenuste pakkimiseks, saavad loa. Eraseisikutele neid lube aga välja ei antaks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on probleeme ID-kaardi PIN-koodiga. Kas ma saan uue PIN-koodi taotleda? +assistant: Tere! Jah, seda saab. Uue PIN-koodi ümbriku taotlemiseks pead sa pöörduma Politsei- ja Piirivalveameti teenindusse või Eesti Vabariigi välisesindusse. Palun võta kaasa isikut tõendava dokumendi! Kas sa tead, kus on lähim Politsei- ja Piirivalveameti teenindus?",output_ID.ee +"user: Tere! Ma arvan, et e-hääletamine on tore asi, aga ma ei ole päris kindel, kuidas see töötab. Kas võite mulle öelda, kuidas seda täpselt tehakse? +assistant: Tere! Päevake! Jah, e-hääletamine on päris uudne asi. Põhiseisus on see, et e-hääletamine tähendab, et saate hääletada interneti kaudu, ilma kodust lahkumata. Seega, kui teil on ID-kaart või mobiil-ID, on valmis! Algab valimispäeva esmaspäev hommikul kell 9 ja lõpeb laupäeval kell 20. Kas teiega jagatakse täpses põhjalikus juhendites, kuidas seda teha?",output_ID.ee +"user: Tere! Ma tahan meresõidutunnistust taotleda, aga ma ei teagi, kuidas riigilõivu maksta. Kust ma alustan? +assistant: Tere! Riigilõivu tasumiseks on mitu võimalust. Saaja on Rahandusministeerium. Viitenumber on 2900082443. Pangad, kuhu sa selle maksta saad, on SEB, Swedbank, Luminor Bank ja LHV Pank. Nüüd, mis pank sulle kõige mugavam on?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma just lugesin midagi 112 kohta. Millal ma peaksin sellele numbrile helistama? +assistant: Tere! Sa peaks helistama 112 numbrile, kui sa vajad politsei kiiret abi ja soovid teatada korrarikkumisest, kuriteost või liiklusõnnetusest. See on hädaabinumber, nii et kasuta seda olukordades, kus kiiret reageerimist on vaja.",output_Politsei-_ja_Piirivalveamet diff --git a/experiments/ood_detection/data/ood_test.csv b/experiments/ood_detection/data/ood_test.csv new file mode 100644 index 00000000..b53028e8 --- /dev/null +++ b/experiments/ood_detection/data/ood_test.csv @@ -0,0 +1,29 @@ +conversation +"user: Tere! Kas ma saan teada, millal on parim aeg tomateid istutada? assistant: Tere! Tomateid on parim istutada mai keskpaigast alates, kui öökülmade oht on möödas. Enne istutamist tasub mulda hästi ette valmistada ja kompostiga rikastada. user: Aitäh! Kas ma peaksin neid kuidagi toestama ka? assistant: Jah, kindlasti! Tomatitaimed vajavad tugevat tuge, eriti kui nad hakkavad kasvama. Kasuta puitlatte või metallist tuge ja seo taim ettevaatlikult kinni." +"user: Tere! Ma planeerin reisi Lõuna-Eestisse. Mida sa soovitaksid seal vaadata? assistant: Tere! Lõuna-Eestis on palju ilusaid kohti! Kindlasti külasta Otepää loodusparki, Rõuge ööbikuorgu ja Suur Munamäge. Võru järv on samuti väga ilus koht lõõgastumiseks. user: Need kõlavad hästi! Kas seal on ka häid kohti, kus süüa? assistant: Jah! Otepääl on mitu head restorani, näiteks Pühajärve spa juures. Võrus soovitan külastada kohalikke kohvikuid, kus saad proovida traditsioonilist lõunaeesti toitu." +"user: Tere! Ma hakkasin äsja jooksma, aga mul läheb kiiresti hingeldama. Mida ma valesti teen? assistant: Tere! See on algajatele täiesti tavaline. Tõenäoliselt jooksed liiga kiiresti. Proovi aeglasemat temposse - pead saama jooksmise ajal veel rääkida. user: Ah, selge! Kui sageli ma peaksin jooksma minema? assistant: Alusta 2-3 korda nädalas ja lase kehel harjuda. Iga jooksu vahel peaks olema vähemalt üks puhkepäev, et lihased saaksid taastuda." +"user: Tere! Ma tahan õppida kitarri mängima. Kust ma peaksin alustama? assistant: Tere! Suurepärane otsus! Alusta lihtsate akordide õppimisega - C, G, Am ja F on head esimesed akordid. YouTube'is on palju tasuta õpetusi algajatele. user: Kas ma peaksin kohe õige kitarri ostma? assistant: Alguses võid proovida ka odavamat kitarri või laenata sõbralt. Kui oled kindel, et tahad jätkata, siis investeeri kvaliteetsemasse instrumendisse." +"user: Tere! Minu koer haugub pidevalt naabrite peale. Kuidas ma saaksin teda rahustada? assistant: Tere! See on üsna levinud probleem. Proovi kõigepealt aru saada, mis koera häirib - ehk on ta lihtsalt igav või vajab rohkem liikumist? user: Ta saab küll jalutada, aga naabrid käivad õues palju. assistant: Sellisel juhul proovi koera tähelepanu ümber suunata - anna talle mäng või maiused, kui naabrid on väljas. Positiivne treening töötab paremini kui karjumine." +"user: Tere! Ma otsin head raamatut, mida suveks lugeda. Mis sa soovitaksid? assistant: Tere! Mis žanr sind huvitab? Kui tahad midagi kerget, siis võiks proovida Fredrik Backmanit või Lucinda Rileyit. Kui eelistad põnevust, siis Stieg Larsson on alati hea valik. user: Ma armastan ajaloolisi romaane! assistant: Suurepärane! Siis soovitan kindlasti Hilary Manteli 'Wolf Hall' seeriat või Ken Folletti 'Sammaste' triloogiat. Need on võimsad ajaloolised teosed." +"user: Tere! Ma tahan hakata oma kodus joogat praktiseerima. Mida ma vajan? assistant: Tere! Alguses piisab joogamatist ja mugavast riietusest. Laadi alla mõni äpp nagu Daily Yoga või otsi YouTube'ist algajatele mõeldud videoid. user: Kui sageli ma peaksin joogat tegema? assistant: Alusta 2-3 korda nädalas 15-20 minutit korraga. Regulaarsus on olulisem kui pikkus - parem teha lühemalt, aga iga päev." +"user: Tere! Minu taimed kodus närvavad ära. Mis ma valesti teen? assistant: Tere! Kõige sagedasem viga on ülekallatamine. Kontrolli, kas pottides on drenaaž ja kas muld ei ole liiga märg. Eri taimed vajavad ka erinevalt valgust. user: Kuidas ma tean, millal kasta? assistant: Torka sõrm umbes 2-3 cm sügavusele mulda. Kui see on kuiv, siis on aeg kasta. Enamik toataimi eelistab kerget kuivamist kastmiste vahel." +"user: Tere! Ma planeerin järgmiseks nädalavahetuseks telkimisreisi. Mida ma peaksin kindlasti kaasa võtma? assistant: Tere! Põhilistest asjadest: hea telk, magamiskott, isopaat, pealamp ja esmaabikomplekt. Kontrolli ka ilmateadet ja võta vastavalt riideid kaasa. user: Kuidas toitlustamisega? assistant: Võta kaasa lihtsat toitu, mis ei riku - konservid, kuivained, pähklid. Gaasipliidike ja keedunõud on ka vajalikud. Ära unusta ka piisavalt vett!" +"user: Tere! Ma hakkasin õppima prantsuse keelt, aga grammatika on nii keeruline! Mis nõu sa annaksid? assistant: Tere! Prantsuse grammatika on tõesti väljakutsuv! Alusta põhiverbide konjugeerimisest ja ära ürita kõike korraga meelde jätta. Kuula palju prantsuse muusikat ja vaata filme subtiitritega. user: Kas sa soovitaksid mõnda äppi? assistant: Kindlasti! Duolingo on hea alguseks, Babbel on põhjalikum. Anki on suurepärane sõnavara meeldejätmiseks. 15 minutit päevas on piisav." +"user: Tere! Ma tahan alustada aias komposteerimisega. Kuidas ma peaksin sellega alustama? assistant: Tere! Komposteerimine on lihtne! Vaja on segada rohelist materjali (rohuleht, köögiviljajäägid) ja pruuni materjali (kuivad lehed, paber). Hoia kompost niiske, aga mitte märg. user: Kui kaua läheb kompostiks saamist? assistant: Tavaliselt 6-12 kuud, olenevalt tingimustest. Kui kompost on valmis, on see tumedate ja mullalähedase lõhnaga. Regulaarne segamine kiirendab protsessi." +"user: Tere! Ma otsin head kohta, kus õhtul sõpradega aega veeta Tallinnas. Mida sa soovitaksid? assistant: Tere! Oleneb, mida otsite! Kui tahate vaiksemat õhtut, siis Kalamaja piirkonnas on häid kohvikuid ja baare. Kui eelistate tantsu, siis vanadlinnas on palju klube. user: Me tahaksime lihtsalt juttu ajada ja head õlut juua. assistant: Siis soovitan Põhjala Taproomi või Laua tänaval asuvaid pubisid. Seal on hea õlu valik ja rahulik atmosfäer vestluseks." +"user: Tere! Ma hakkasin hommikuti varajaselt ärkama, aga tunnen end päeval väsinuna. Mida teha? assistant: Tere! See võib olla und puudutav probleem. Kas sa lähed ka varem magama? Täiskasvanul on vaja 7-9 tundi und. Proovi ka õhtul ekraane vältida tund aega enne magamaheitu. user: Ma lähen küll varasemalt, aga ikka tunnen end halvasti. assistant: Siis võib probleem olla une kvaliteedis. Hoia magamistuba jahe ja pime, väldi kofeiini pärast lõunat. Kui probleem jätkub, tasub arstiga rääkida." +"user: Tere! Ma tahan õppida pildistama. Mis kaamerat sa soovitaksid algajale? assistant: Tere! Algajale sobib hästi peegelfotoaparaat või hea hääljuhtimisega kaamera telefon. Canon EOS või Nikon D-seeria on head valikud. Objektiivi osas alusta 18-55mm kitiga. user: Kas ma peaksin kohe käsirežiimi õppima? assistant: Esialgu kasuta automaatset režiimi, et harjuda kaameraga. Hiljem proovi õppida ISO, säri ja ava mõisteid. YouTube'is on palju häid õpetusi." +"user: Tere! Ma tahan alustada jõusaalis käimisega, aga ma ei tea, kuidas alustada. assistant: Tere! Suurepärane otsus! Alusta kergete raskustega ja keskendur õigele tehnikale. Esimest korda tasub treeneriga konsulteerida või võtta mõni sissejuhatav trenn. user: Kui sageli ma peaksin käima? assistant: Alguses 2-3 korda nädalas on piisav. Anna lihastele aega taastuda. Keskendun suurte lihasgruppide harjutustele nagu kükid, surved ja tõmbed." +"user: Tere! Minu arvuti on muutunud aeglaseks. Mida ma saaksin ise teha? assistant: Tere! Proovi kõigepealt taaskäivitada arvuti. Seejärel kontrolli, kui palju vaba ruumi kõvakettal on - vähemalt 10-15% peaks vaba olema. Kustuta ka ajutised failid. user: Ma tegin seda, aga ikka on aeglane. assistant: Siis kontrolli, millised programmid käivituvad koos süsteemiga. Vahel käivitub liiga palju programme automaatselt. Viiruskontroll võiks ka kasulik olla." +"user: Tere! Ma tahan õppida uisutatama. Kas see on raske täiskasvanuna? assistant: Tere! Pole kunagi liiga hilja alustada! Täiskasvanuna võib alguses rohkem aega võtta, aga järjepidevusega saad kindlasti selgeks. Alusta avaliku uisutamisega ja võta mõned tunnid. user: Millised uisud ma peaksin ostma? assistant: Alguses võid uisud ka laenutada, et aru saada, kas meeldib. Kui ostad, siis vali mugavad, mis toetavad pahkluid hästi. Spetsialist spordipoes aitab õiget suurust valida." +"user: Tere! Ma tahan hakata koduõlut valmistama. Kuidas alustada? assistant: Tere! Koduõllekeetmine on põnev hobi! Alusta lihtsate komplektidega, kus on juba kõik koostisosad olemas. Vaja on puhasust ja õiget temperatuuri. user: Kui kaua protsess kestab? assistant: Esimene kääritamine võtab umbes 1-2 nädalat, seejärel veel 2-4 nädalat valmimist. Kokku umbes kuu aega esimesest õllest, aga tulemus on seda väärt!" +"user: Tere! Ma otsin head kohta, kus talvel suusatada Eestis. Mida sa soovitaksid? assistant: Tere! Kui on piisavalt lund, siis Otepää on klassika - seal on hästi hooldatud rajad. Haanja on samuti populaarne. Kui tahad mäesuusatamist, siis Kuutsemägi või Väike-Munamägi. user: Kas ma pean oma suusad kaasa võtma? assistant: Enamikes kohtades saab suuski laenutada, seega ei pea tingimata omi kaasa võtma. Aga kui käid sageli, siis enda suusad on mugavam valik." +"user: Tere! Ma tahan õppida tantsu. Mis stiil oleks hea algajale? assistant: Tere! Oleneb, mis sulle meeldib! Salsa on seltskondlik ja lõbus, valss on elegantne ja klassikaline. Hip-hop on energiline ja kaasaegne. Enamik tantsukoole pakub proovitunde. user: Kas ma saan ka üksinda alustada? assistant: Jah, kindlasti! Paljudes kohtades on üksikute registreerimise võimalus ja sind paardatakse kellegagi. Rühmatunnid on samuti head - seal kohtad sarnaselt mõtlevaid inimesi." +"user: Tere! Ma tahan alustada kalastamisega. Mida ma alguses vajan? assistant: Tere! Kalastamine on lõõgastav hobi! Alguses piisab lihtsast konksu ja nööriga varrest, mõnest haagist ja ussikestest. Ära investeeri kohe kallisse varustusesse. user: Kus ma Eestis kalastada võin? assistant: Üldkasutatavates veekogudes võid vabalt õngitseda. Jõgedes ja järvedes nagu Peipsi, Võrtsjärv. Privaatsetesse veekogudesse on vaja luba. Alusta lihtsalt lähedalasuvast järvest!" +"user: Tere! Minu kodu on talvel liiga külm. Kuidas ma saaksin energiat säästa? assistant: Tere! Mitmeid viise on! Tihenda aknad ja uksed, kasuta paksemaid kardinaid, sulge kasutamata toad. Õige riietumine kodus aitab ka - soojad sokid ja kampsun. user: Kas radiaatorite reguleerimine aitab? assistant: Kindlasti! Reguleeri radiaatoreid vastavalt vajadusele - magamistoas võib madalam temperatuur olla. Kasutamata tubades võid kütte täiesti kinni keerata." +"user: Tere! Ma tahan õppida malet mängima. Kust alustada? assistant: Tere! Male on suurepärane mäng! Alusta malendite käikude õppimisest. Chess.com või Lichess on head platvormid, kus saad nii õppida kui ka mängida. user: Kas ma peaksin kohe teiste vastu mängima? assistant: Alusta arvuti vastu lihtsamal tasemel. Kui põhitõed selged, siis hakka teiste algajatega mängima. Ära muretse kaotamiste pärast - nii õpidki!" +"user: Tere! Ma tahaksin hakata joonistama, aga mul pole kunagi eriti osanud. assistant: Tere! Iga inimene saab õppida joonistama! Alusta lihtsatest vormidest - ringid, ruudud, kolmnurgad. Seejärel proovi lihtsaid objekte nagu õunad või tassingud kopeerida. user: Millised vahendid ma vajan? assistant: Alguses piisab tavalisest pliiatsist ja paberist. Hiljem võid proovida erinevaid pliiatseid (2H, HB, 2B) ja kustukummi. Oluline on regulaarne harjutamine, mitte kallid vahendid." +"user: Tere! Ma tahan alustada mesilaspidamisega. Kas see on raske? assistant: Tere! Mesilaspidamine on huvitav, aga nõuab teadmisi ja hoolikust. Soovitan alustada kursusest või kogenud mesipidaja juurest õppimisest. Mesilaselud on väga tundlikud. user: Millal on parim aeg alustada? assistant: Kevad on parim aeg - aprillis või mais, kui lilled hakkavad õitsema. Siis on mesikondadel kõige lihtsam uues kohas alustada ja toitu leida." +"user: Tere! Ma otsin head viisi, kuidas kodus rohkem rohelist tuua. assistant: Tere! Toataimed on suurepärane lahendus! Aloe, roheline draakon ja sanseviera on vastupidavad algajatele. Nad puhastavad ka õhku. Võid ka väikese rohelist müüri luua riiulile. user: Kas kõik taimed vajavad palju valgust? assistant: Ei, mitte kõik! Sanseviera ja ZZ-taim taluvad hästi varjulist kohta. Monstera ja pothos sobivad keskmise valgusega kohtadesse. Iga taime juures on tavaliselt hooldamisjuhend." +"user: Tere! Ma tahan õppida kuduma. Kas see on raske? assistant: Tere! Kudumine pole raske, aga nõuab kannatlikkust! Alusta lihtsatest silmustest - parem- ja pahemsilmus. YouTube'is on häid õpetusi. Esimeseks projektiks sobib sall või lihtne müts. user: Millised vahendid ma vajan? assistant: Alguses piisab päris tavalistest heegelnõeltest ja villasest lõngast. Vali keskmine paksus lõng - ei liiga õhuke ega paks. Müüjad poest oskavad hästi nõu anda!" +"user: Tere! Ma tahan õppida autot juhtima. Kuidas ma peaksin sellega alustama? assistant: Tere! Suurepärane otsus! Alusta autokoolist ja võta mõned teoreetilised tunnid, et põhitõed selgeks saada. Seejärel hakka sõiduõpetajaga praktilisi tunde võtma. user: Kui kaua see tavaliselt aega võtab? assistant: Keskmiselt 20-40 sõidutundi, olenevalt inimesest. Teoreetilise eksami võid teha siis, kui oled valmis, praktiline tuleb hiljem. Oluline on mitte kiirustada ja hoolikalt õppida." \ No newline at end of file diff --git a/experiments/ood_detection/data/test.csv b/experiments/ood_detection/data/test.csv new file mode 100644 index 00000000..5a6b5001 --- /dev/null +++ b/experiments/ood_detection/data/test.csv @@ -0,0 +1,229 @@ +conversation,agency +"user: Tere! Ma ostsin äsja uued jalanõud ja ma ei saa päris alguses aru, mida kõik need sildid tähendavad. Mida ma peaksin seal märkima? +assistant: Tere! Jalanõudel peaksid leidma märkused pealsete, voodri ja talla valmistamisel kasutatud materjalid. Samuti on oluline, et jalatsitel oleks märgistatud nende otstarve – kas need sobivad tänaval kandmiseks või siseruumides kasutamiseks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma tahaksin teada, kuidas ma saan lapsele passi taotleda. Mis on esimene asi, mida ma pean tegema? +assistant: Tere! Esiteks, pead broneerima aja politsei teeninduses. Sa leiad teenindused ja saad aja broneerida siin: https://broneering.politsei.ee/. Või saad dokumendi kaupluses kätte, aga sel juhul pead arvestama lisatasuga.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma sain DigiDoc4 käivitamisel vea, et sertifikaatide usaldusnimekirja uuendamine ebaõnnestus. Mis see tähendab ja mida ma sellega pean tegema? +assistant: Tere! See tähendab, et arvutis, millega DigiDoc4 töötab, ei ole uusima usaldusnimekirja versiooni. See on vajalik allkirjade kontrollimiseks. Selle probleemiga toime tulla, kõigepealt veenduge, et teil on arvutis kõige uuem ID-tarkvara paigaldatud. Saate seda hankida siin: https://www.id.ee/artikkel/paigalda-id-tarkvara/ . Kas te seda juba proovite?",output_ID.ee +"user: Tere! Kas te saate rääkida ID-tarkvara värskendamisest? Mul on küsimusi selle kohta. +assistant: Tere! Muidugi, ma saan. ID-tarkvara värskendamine on väga lihtne. Meie veebilehel on automaatselt kindluses oleva tarkvara uusim versioon. Kas te olete Windows, macOS või Linux kasutaja? See aitab öelda, millist versiooni peaksite alla laadima.",output_ID.ee +"user: Tere! Mulle on vaja lennureisija õigustest rohkem teada. Mis on olulisim asi, mida ma peaksin meeles pidama? +assistant: Tere! Olulisim asi on see, et sul on õigus saada abi lennujaamas, olenemata sinu erivajadusest. See tähendab, et saad abi registreerimisel, pagasiregistreerimisel ning isegi transpordiga väravani jõudmiseks. On oluline teatada abivajadusest teenusepakkujale 48 tundi enne lennu väljumisaega.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Olen kuulnud midagi ühtse laadija kohta. Mis see tähendab ja miks on see oluline? +assistant: Tere! Ühtne laadija tähendab, et paljud seadmed hakkavad kasutama USB-C laadimispesa, mis on juba üha levinum. See on oluline, kuna see standardiseerib laadimist, muudab laadijate ostmist kergemaks ja toetab kiirlaadimise tehnoloogiat.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on palju lugemist, aga ma tahan teada, mida tähendab ""tarbija käendusleping""? Mis see üldse on? +assistant: Tere! Tarbija käendusleping on olukord, kus sa kohustud sõbra või sugulase laenu eest tasuma, kui ta ise seda ei suuda. See annab laenuandjale lisakindluse, et laen tagastatakse. Olenemata sellest, kas tegemist on väikelaenu, järelmaksu, tarbimislaenu või eluasemelaenu puhul. Kas soovid, et selgitan täpsemalt sellega seotud nüansse?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud Tarbijakaitse Ametist, aga ei tea täpselt, mis tarbija õigused on. Mis on kõige olulisem asi, mida tarbija peaks teadma? +assistant: Tere! Oluline on teada, et sul on õigus nõuda ja saada kaup või teenus, mis vastab nõuetele ja on ohutu sinu elule, tervisele ja varale. Samuti on sul õigus saada infot, et teha teadlik valik. See tähendab, et müüjalt peaks alati küsima vajalikku teavet toote või teenuse kohta.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mis õigused on mul kui lennureisija? +assistant: Tere! Lennureisijana on sul mitmeid õigusi. Näiteks, kui lend hiljub või tühistatakse, sul on õigus teavitamist saada ja hüvitist nõuda, samuti saad abi saada lennujaamas, kui sul on erivajadusi. On ka õigusi seoses pagasi transpordi ja transpordivahendi kahjustuse puhul. Soovid, et ma puutuksin mõnele neist detailsemalt lahti?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin midagi hoonete energiatõhususest ja Tarbijakaitse ja Tehnilise Järelevalve Ametist. Mis see tähendab täpselt? +assistant: Tere! Hoonete energiatõhusus tähendab seda, et uutele ja oluliselt rekonstrueeritavatele hoonetele on kehtestatud miinimumnõuded selle kohta, kui palju energiat nad tarbivad. Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib, et neid nõudeid järgitaks. Näiteks, uuelt hoonelt ootatakse, et see oleks energiatõhusam kui varem. Kas sa tahaksid teada, kuidas need nõuded täpsemalt kehtivad?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma lugesin TTJA veebilehel sideturu kohta ja natuke segadus. Mis need hulgiturgude teenused täpsemalt on, mida seal loetletud on? Näiteks kõne lõpetamise hulgiteenus telefonivõrgus - mis see tähendab? +assistant: Tere! Hea, et küsid. Need hulgiturgude teenused on sellised teenused, mida operaatorid pakuvad teistele operaatoritele, mitte otse lõppkasutajale. Näiteks, kõne lõpetamise hulgiteenus telefonivõrgus tähendab, et üks operaator maksab teisele operaatorile selle eest, et tema klient saaks teha kõne teise operaatori klientidele. Sisuliselt on see hulgiturgude teenus, mis võimaldab erinevatel operaatoritel omavahel ühendust tagada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen huvitatud teadma, kuidas Politsei- ja Piirivalveamet pöördumistele vastates minu isikuandmeid kasutab? +assistant: Tere! Pöördumiste lahendamiseks ja teile vastamiseks kasutame teie isikuandmeid. Kui peame teistele asutustele või isikutele järelepärimisi esitama, edastame neile ainult nii palju andmeid, kui on vajalik vastuse koostamiseks. Me ei avalda neid andmeid rohkem kui on tarvis.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma saan DigiDoc4 käivitades veateadet, et sertifikaatide usaldusnimekirja uuendamine ebaõnnestub. Kas teate, mis selle vastu pean tegema? +assistant: Tere! Jah, seda probleemi on mõeldud just sellele vea korral. See tähendab, et DigiDoc4 ei saa oma sertifikaate teiega usaldusväärselt kontrollida. Alustame kõigepealt sellest, et veenduksime, et sul on ID-tarkvara kõige uuem versioon. Võite seda vaadata ja vajadusel paigaldada siin: https://www.id.ee/artikkel/paigalda-id-tarkvara/",output_ID.ee +"user: Tere! Ma sain just elamisloa ettevõtluseks suurinvestorina. Kust ma saan selle elamisloakaardi kätte? +assistant: Õnnitleme! Elamisloakaardi saad kätte isiklikult kas Eesti välisesinduses või Politsei- ja Piirivalveameti teeninduses. Tõenäoliselt märkisid sa seda juba oma tähtajalise elamisloa taotluses, kus said valida sobiva koha.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus elamisloa kohta. Mis juhtub, kui minu elamisluba täisealisele lapsele pereliikme juurde elama asumiseks lõppeb? +assistant: Tere! Kui sinu elamisluba lõppeb, saad Eestisse jääda veel kuni 270 päevaks. Sel perioodil on sul ka võimalus Eestis töötada.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin palju 5G kohta, aga ei saa päris hästi aru, mida see tähendab. Mis on need kolm peamist arengusuunda, mida ITU näeb 5G puhul? +assistant: Tere! ITU näeb 5G puhul kolme peamist arengusuunda: suurendatud andmesidekiirus (eMBB), tagatud ajakriitiline reaalajasüsteemide andmeside (URLLC) ja suurearvuline väikeseadmete kommunikatsioon (mMTC). See tähendab lihtsalt seda, et 5G lubab kiiremaid andmesidegaid, võimaldab reaalajas teenuseid ja toetab rohkemat arvukust seadmete ühenduse kohta.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma tahaksin teada, kuidas ma saan Politsei- ja Piirivalveametilt teavet küsida. Mis on kõige lihtsam viis? +assistant: Tere! Pöördumised PPA poole saab saata e-posti teel või postiaadressil Pärnu mnt 139, 15060 Tallinn. Kui saata e-postiga faile, palun krüpteeri need sertifikaadiga „Politsei- ja Piirivalveamet (PPA) 70008747“ turvalisuse tagamiseks.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma pole kunagi digiallkirjastanud midagi mobiiliga. Kas see on üldse lihtne? +assistant: Tere! Jah, digiallkirjastamine RIA DigiDoc mobiilirakendusega on väga lihtne. See on mõeldud nii ID-kaardi, mobiil-ID kui ka Smart-ID kasutajatele. Pärast lohistamist saab sa sellega lihtsalt dokumente allkirjastada. Kas sul on ID-kaart, mobiil-ID või Smart-ID?",output_ID.ee +"user: Tere! Ma kuulsin, et 112 on hädaabinumber. Aga millal täpselt ma peaksin sellele helistama? +assistant: Tere! Sa peaksid 112-le helistama siis, kui sa vajad politsei kiiret abi ja tahad teatada korrarikkumisest, kuriteost või liiklusõnnetusest. See on number, millele helistada, kui on vaja kiiret sekkumist.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus raudtee ohutuse kohta. Mida peaksin silmas pidama, kui lähen raudteeülesõidu juurde? +assistant: Tere! Oluline on peata kiirust, et vajaduse korral saaksid peatuda. Kontrolli liiklusmärke ja valgusfoore ning vaata ja kuula, et veenduda, et liikumine on ohutu. Enne üleminekut veendu alati, et lähenev raudteesõidukile anda teed.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma imestan, kuidas Politsei- ja Piirivalveametiga (PPA) ühendust saada, kui mul on küsimus? +assistant: Tere! PPA registrikood on 70008747 ja aadress on Pärnu mnt 139, 15060 Tallinn. Sa saad meiega ühendust faksi teel, number on 612 3009. Üldiselt on riigiinfo telefon 1247 ka hea võimalus alustuseks.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas te saate mulle öelda, mis on Smart-ID ja miks seda peaks kasutama? +assistant: Tere! Muidugi. Smart-ID on tasuta nutirakendus, millega saate sisse logida erinevates e-teenustes, kinnitada tehinguid ja allkirjastada dokumende. See on nagu ID-kaart, digi-ID või mobiil-ID, aga see on mugavam kasutada telefoni kaudu. Kas sa tahaksid rohkem teada, kuidas see töötab?",output_ID.ee +"user: Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Ametil on kaart, mis näitab sideteenuste kättesaadavust. Mis see kaart täpselt on ja milleks see mõeldud? +assistant: Tere! Jah, meil on sideteenuste kaart. See näitab, kas aadressil on võimalik interneti teenusega liituda. Kaart põhineb sideoperaatorite andmetel nende tavahinnakirjade alusel. Roheline punkt tähendab, et liitumisvõimalus on, punane aga mitte.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma ei ole päris kindel, kuidas oma ID-kaardi sertifikaate vaadata ja neid arvutis salveida. Kas saate mind selles osas aidata? +assistant: Muidugi! Üsna lihtne on see. Esmalt võid kaardilugeja arvutiga ühendada ja oma ID-kaardi sisse süstada. Seejäna avad DigiDoc4 programmi. Pärast seda ootad, kuni sinu ID-kaardi andmed sisse loetakse. Kas sul on kaardilugeja ja arvuti, mis on valmis kasutama?",output_ID.ee +"user: Tere! Ma saan teada, et Narva arestimajas on mingid reeglid pakikandmise kohta. Kas ma saan sinna saates käimas käibele pakki viia kellelegi? +assistant: Tere! Kahjuks mitte. Arestialustele pakke üldiselt tuua ei saa. On teisi küsimusi selle kohta?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kuulsin, et ID-kaartidega on midagi juhtunud. Mis siis täpselt on juhtunud sertifikaatidega? +assistant: Tere! Jah, 2018. aasta 1. aprillist tunnistati turvanõrkusega ID-kaardi sertifikaadid kehtetuks. See tähendab, et kui sa ei uuendanud neid 31. märtsiks 2018, siis elektrooniliselt ID-kaarti enam ei saa kasutada.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma pole päris kindel, mida täpselt tähendab ‘digitaalne allkiri’. Kas see on sama asjalik kui tavaline kiri allkiri? +assistant: Tere! Hea küsimus! Jah, mõttes on see väga sarnane, aga digitaalse allkirja puhul saab sellega kindluse, et see ei ole lihtsalt võltsitav. Tavaline kiri on mõelnud kui kirjalik kinnitus, kuid digitaalse allkirjaga saab kindlustada, et see on seaduslikult kehtiv ja seda ei saa muuta pärast allkirjastamist. See vastab omakäelisele allkirjale juriidiliselt.",output_ID.ee +"user: Tere! Ma uurisin Politsei ja Piirivalve ameti veebilehel infot lapse kodakondsuse kohta. Kuidas saan teada, millal saan lapse kodakondsustunnistuse? +assistant: Tere! Kui laps saab Eesti kodakondsuse, saadetakse kodakondsustunnistus teile posti teel. Täpne ajakavast sõltub menetluse kiirusest, aga tunniste tulek postiga on tavapärane viis dokumentide edastamine.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen Tarbijakaitse ja Tehnilise Järelevalve Ameti uus praktikant. Räägime natuke raadiosageduste kasutamisest. Mis on EFIS ja milleks seda kasutada saab? +assistant: Tere! EFIS on Frequency Information System – Euroopa Raadiosageduste Informatsiooni Süsteem. See andmebaas võimaldab näha, kuidas konkreetne riik sagedusala (9kHz kuni 275 GHz) kasutab ja võrrelda seda teiste Euroopa riikidega. Samuti leiad sealt infot erinevates riikides kehtestatud nõuetele raadioseadmete kasutamisele ja sageduste jaotustele elektroonilise side teenuste osutamiseks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud CE-märgist, aga ma ei tea päris täpselt, mis see tähendab. Saaksin natuke selgitust? +assistant: Tere! Muidugi. CE-märgist kinnitab tootja või tema volitatud esindaja tootele. See tähendab, et tootja kinnitab, et toode vastab ELi ühtlustatud nõuetele. Mis konkreetsemalt sind CE-märgise kohta huvitab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus lennureisijate õiguste kohta. Mis juhtub, kui mu lend hiljub? +assistant: Tere! Kui sinu lend hiljub, on sul õigus hüvitisele, aga see sõltub hilinemise põhjustust. Kui lend hiljub kolm tundi või rohkem, ja hilinemine on vedaja süül, saad hüvitist. Kui aga hilinemine on erakorraliste asjaolude tõttu (mis on vedaja kontrolli alt väljas), hüvitist ei maksta. Täpsemalt on hüvitise summa olenev lennu pikkusest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ostsin hiljaaegu veebist kaamera Saksamaalt. Mida ma peaksin teadma, kui sellega peaks midagi viga olema? +assistant: Tere! Saksamaalt ostetud kaamera puhul on oluline teada, et EL-i ja Euroopa Majanduspiirkonna riikides (Saksamaa on ka selles piirkonnas) on tarbijatele sarnane kaitsetase. Sul on õigus esitada kaebus defektide kohta vähemalt kahe aasta jooksul alates ostukuupäevast. Tähtis on, et sul oleks ostutšekk või maksekorralduse väljavõte tõendamaks ostu.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma sain trahviteate kiiruskaamerast, aga ma ei saa aru, kas fotot saab ka saada? +assistant: Tere! Põhimõtteliselt jah, saad. Trahviteatega ei saadetata fotot automaatselt. Kuid mootorsõiduki eest vastutav isik saab taotleda fotokoopiat. Seda tuleb teha aga eraldi taotlusega, millele on lisatud digitaal- või tavaallkiri.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et piiri tugevdamiseks on rajatud lõiketraati. Kas see ei mõju metsloomadele? Palju nad sellega hädaldavad? +assistant: Tere! See on väga hea küsimus. Tegelikult on seda küsimust juba arutletud. Mure on küll sees, et lõiketraat võib loomade liikumist piirata. Kuid politsei on Keskkonnaametiga kooskõlastanud, kuidas olukorda lahendada, kui loomad juhtu takerduma.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et mobiil-ID on hea asi, aga kuidas seda saab tellida? Ma ei tea, kust alustada. +assistant: Tere! Päris hea küsimus. Mobiil-ID tellimine on päris lihtne. Seda saate tellida oma mobiilsideoperaatori juures - Telia, Elisa või Tele2. Kõik kolm pakuvad seda serviisi. Palju lihtsam on, kui saad teada, millise operaatori pakub sulle kõige sobivam teenus!",output_ID.ee +"user: Tere! Ma nägin midagi energiamärgistuse kohta, aga ei saa pähe. Mis see tähendab ja miks see oluline on? +assistant: Tere! Energiamärgistus näitab, kui palju energiat seade kulutab. See on oluline, sest see aitab teil valida energiasäästlikumaid tooteid, mis aitab vähendada teie energiakulusid ja ka keskkonna mõju. ELis on kehtestatud ühtsed nõuded mitmele tootegrupile, näiteks külmikutele, pesumasinatele ja televiisioritele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma pole kunagi varem ID tarkvara uuendanud. Miks on see nii oluline? +assistant: Tere! Palju maasõna! ID tarkvara uuendamine on väga oluline turva tõttu ja sellepärast, et see hakkaks paremini töötama. Ühest küljest tagab uuendamine, et sa oled kaitstud uusimaid turvariske vastu. Teisest küljest aitab uuendamine, et tarkvara oleks otstarbeks mugavam kasutada.",output_ID.ee +"user: Tere! Ma arvasin, et ID-kaardiga saab reisida, aga ma pole kindel. Kus ma saan teada, kuhu just saan ID-kaardiga reisida? +assistant: Tere! Vastus on, et ID-kaardiga saab reisida Euroopa Liidu (EL) ja Euroopa Majanduspiirkonna (EMP) liikmesriikidesse. Aga kõige täpsema info saad ReisiTargalt veebilehel: https://reisitargalt.vm.ee/kontrolli-reisidokumente/. Seal näeb, millistesse riikidesse ID-kaardiga reisida on lubatud.",output_ID.ee +"user: Tere! Ma soovin tööle minna Eestis ettevõtluse pidamiseks, aga mul pole hetkel Eestis elamisloa. Kuidas saan selle taotleda? +assistant: Tere! Palju õnne otsusega Eestis ettevõtlust alustada! Elamisluba ettevõtluseks saab taotleda, kui investeerid Eestis oma äriühingusse. Esiteks pead investeerima. Kas sa oled juba seda tegema planeerinud? Ja siis pead kohutama nõutud dokumentidele ja riigilõive.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus defektse toote kohta. Mis on minu õigused, kui kaubal on viga? +assistant: Tere! Kui tootel on viga, on Sul õigus nõuda müüjalt esmalt tasuta parandamist või selle asendamist uue kaubaga. Müüja valib reeglina ise, kas parandada või asendada, kuid saavad kokku leppida. On ka olukorda, kus müüjal ei ole võimalik kaupa parandada või asendada, parandamine ebaõnnestub, müüja ei kõrvalda puudust mõistliku aja jooksul või Sulle tekitatakse põhjendamatuid ebamugavusi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus – kui ma olen jäänud kelmuse ohvriks, kuhu ma peaksin kõigepealt pöörduma? +assistant: Tere! Esimese asjana on väga oluline kohe ühendust võtta oma pangaga. Nad saavad sageli kiiresti reaalseid meetmeid võtma, et vältida lisakahjustusi. Swedbank, SEB, LHV ja Luminor kõik pakuvad selleks oma turvalisuse leheküljed, näiteks Swedbanki oma on siin: https://www.swedbank.ee/private/d2d/ebanking/secureBanking",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin midagi CE-märgise kohta. Mis see tähendab ja kes sellele tootele kinnitada tohib? +assistant: Tere! CE-märgis näitab, et toode vastab EL-i ühtlustatud nõuetele. Selle kinnitada tohib ainult tootja või tema volitatud esindaja. See on nagu kinnitus, et toode on ohutu ja vastab nõuetele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kas ma saan ID-kaardiga reisida Euroopa Liitu? Kas mul peab olema pass? +assistant: Tere! Jah, ID-kaardiga saad reisida Euroopa Liitu ja Euroopa Majanduspiirkonna liikmesriikidesse ilma passi kohta. Pass on aga alati vajalik dokument, sest ID-kaart on ainult reisidokumendina lubatud. Kas sa mõtled täpseid riikseid reegleid? ",output_ID.ee +"user: Tere! Kas ma saan ID-kaardi kasutamiseks oma arvutis ID-tarkvara paigaldata? +assistant: Tere! Jah, seda saab. ID-tarkvara on võimalik paigaldada Windowsi, macOS ja Ubuntu operatsioonisüsteemidel. Kuid on oluline teada, et see ei toeta vanemaid operatsioonisüsteeme. Mis operatsioonisüsteemi sa kasutad?",output_ID.ee +"user: Tere! Ma kuulsin, et Tarbijakaitse Amet on teinud meeleseisundi raudteeohutuse kohta. Mis see tähendab? +assistant: Tere! Jah, meil ongi meeleseisund, mis tuletab meile kõik olulised punktid raudtee ohutuse kohta. Põhimõtteliselt tuletab see meid kõiki hoiatama, et raudteed on ohtlikud piirkonnad, kui eiratakse liikluseeskirju.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus TTJA otsuste kohta. Mis saab, kui ma ei nõustu TTJA otsusega? +assistant: Tere! Kui sa ei nõustu TTJA otsusega, on sul õigus selle vaidlustada. Sul on kaks võimalust: esitada vaie TTJA peadirektorile või esitada kaebus Tallinna Halduskohtule.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma uurisin politseijaoskondi ja nägin Lääne-Harju Kesklinna politseijaoskonna nime. Kus see asub? +assistant: Tere! Lääne-Harju Kesklinna politseijaoskond asub Kolde puiestee 65, 10321 Tallinn.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus. Ma pean taotlema passi lapsele, kes on välismaalane. Kuidas ma riigilõivu maksin? +assistant: Tere! Riigilõivu tasumiseks pead kasutama Rahandusministeeriumi kontot. Saaja on Rahandusministeerium, viitenumber on 2900082443. Pangaülekandeks on saadaval erinevad kontod, näiteks SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor (EE701700017001577198), LHV Pank (EE777700771003813400).",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) jälgib gaasiseadmete ohutust. Kuidas see täpselt toimib? +assistant: Tere! TTJA peab silmas, et gaasiseadmed müügil ei tohiks ohustada inimeste tervist ja turvalisust. See tähendab, et nad peavad olema valmistatud ja paigaldatud nõuetekohaselt, regulaarselt hooldatud ning kasutama ettenähtud otstarbel. TTJA võib peatada ohtliku gaasiseadme levitamise ja kasutamise, kui tekib kahtlus ning on õigus nõuda selle turult kõrvaldamist, kui ekspertiis näitab, et seade ei vasta nõuetele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma tahaksin teada, kuidas Narva arestimajas külastajatele lubatud on asju toimetada. Kas saab sinna pakke saata? +assistant: Tere! Narva arestimajasse pakke ei saa toimetada. Ainult vahistatul on lubatud isiklikult toimetada asju.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahaksin teada, mida ma peaksin silmas pidama, kui ma võtan laenu. Mis on vastutustundliku laenamise põhimõtted? +assistant: Tere! Vastutustundliku laenamise põhimõtted tähendavad, et nii laenuandjad kui ka tarbijad peavad olema ettevaatlikud. Enne laenu, järelmaksu või liisingu võtmist, võta pakkumisi paarilt-kolmelt eri firmalt ja küsi tingimuste võrdlemiseks koos pakkumisega teabelehte. See leht sisaldab kõiki olulisi laenutingimusi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et ma peaksin taotlema alaline elamisõigus, aga ma ei ole päris kindel, kuidas see toimib. Kas Politsei- ja Piirivalveamet seda menetleb? +assistant: Tere! Jah, Politsei- ja Piirivalveamet kindlasti menetleb selle. Nad kontrollivad, et sa oled esitanud kõik vajalikud dokumendid ja et sa täidate elamisõiguse saamiseks nõuetekohased tingimused. Kui mingis punktis puudub midagi, sa saad teada. Kas sul on täna mõni konkreetne dokument, mille kohta sa arvad, et puudub midagi?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen välismaal ja tahaksin teada, kuidas ma saan elamisloakaarti taotleda. On see võimalik? +assistant: Tere! Jah, on võimalik. Välismaal saab elamisloakaarti taotleda erandkorras, kui oled välismaalane, kes ei ole EL liikmesriigi kodanik, sul on kehtiv Eesti elamisluba või elamisõigus ning sinu välismaa elukoht on Eesti rahvastikuregistris registreeritud.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma pole päris kindel, mis need elektroonilised allkirjad on ja miks need nii olulised on Euroopas. Kas sa võiksid seda lihtsamalt seletada? +assistant: Tere! Muidugi. Elektroonilised allkirjad on nagu tavaline allkiri, aga see on tehtud arvutis. Need on olulised, sest need aitavad meil e-tehinguteselt kindlust saada, et tegelikult on see just sina, kes selle allkirjastanud oled. Euroopas on selleks vajalikud reeglid, et nad oleksid kõik maailmas ühesugused.",output_ID.ee +"user: Tere! Ma olen huvitatud Eesti kodakondsuse taotlemisest. Kui ma esitan taotluse, mis siis juhtub järgmisena? +assistant: Tere! Pärast taotluse esitamist edastatakse dokumendid Vabariigi Valitsusele. Nad langetavad otsuse sinu kodakondsuse andmise kohta.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud seiklusparkidest, aga ei tea väga palju. Mis on seiklusparki mineku reeglid, mida peaksin teadma? +assistant: Tere! Seikluspark on tore koht aktiivseks puhkuseks. Enne rajale minekut on oluline tutvuda reeglite ja piirangutega. Näiteks, on oluline hinnata oma võimeid ja tervislikku seisundit, eriti kui kavatsete läbida raskemaid radu. Kas Sa oled huvitatud, et ma rääkiks täpsemalt raskusastmete kohta?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et Tarbijakaitseamet aitab probleemide puhul. Mis on esimene asi, mida ma peaksin tegema, kui mul on probleem mingi ostuga? +assistant: Tere! Absoluutselt, Tarbijakaitseamet on abiks. Kõigepealt peaksite kirjutama kaebusena kirja kauplejale. Kui kaebus ei lahenda probleemi, saate pöörduda tarbijavaidluste komisjoni poole.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kaotasin oma passi ja ma ei tea, kust ma saan uue passi tellida. Kas Põlva politseiosakond saab mulle aidata? +assistant: Tere! Jah, Põlva politseiosakond saab teid aidata. Parim viis passi taotlemiseks on PPA iseteenindus veebilehel: https://www.politsei.ee/et/iseteenindus. Võite ka tulla kohale. Põlva teenindus asub Võru 12, 63308 Põlva.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas ID-kaart on mõni uus dokument Eestis või on see vana? +assistant: Tere! ID-kaart on Eesti Vabariigis esmane isikut tõendav dokument. See on suhteliselt uus, sest selle kasutamine on seadusest tuleneva kohustus.",output_ID.ee +"user: Tere! Ma tahan teada, mis on Eesti kodaniku passi tellimisel riigilõivu suurus? +assistant: Tere! Eesti kodaniku passi riigilõivu suurust saad teada Politsei- ja Piirivalveameti veebilehel. Link on https://www.politsei.ee/et/telli-dokument-selverisse. Täpset summat saad seal vaadata.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma lugesin sellest artikkel kohta tarbijavaidluste komisjoni. Mis on selle komisjoni peamine ülesanne? +assistant: Tere! Tarbijavaidluste komisjoni peamine ülesanne on lahendada vaidlusi tarbija ja ettevõtte vahel, mis puudutavad tarbekaupade või -teenuste ostu-müüki. Nad aitavad leida lahendusi, kui tarbija on rahul teenusega või toodetega.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud Ida-Harju politseijaoskonnast. Kus see asub täpselt? +assistant: Tere! Ida-Harju politseijaoskond asub aadressil P. Pinna 4, 13615 Tallinn.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud, et on võimalik laenu maksepuhkust taotleda. Kuidas see üldiselt toimib? +assistant: Tere! Jah, on õige. Maksepuhkus võimaldab teil laenu tagasimaksmist lühikeseks ajaks peatada, kui teil on raskusi. Üldjuhul antakse see kuni kuueks kuuks, ja sel ajal ei maksa laenu põhiselt, vaid intressi tuleb ikkagi edasi tasuda. Oluline on märkida, et seda tuleks taotleda alles siis, kui sissetuleku vähenemine on probleem ja teised kulud kokku tõmmata ei aita.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulasin rääkimist eIDAS-ist. Kas te võtaksite need mõned e-tehingud käsitlevaid küsimusi vastu? +assistant: Tere! Muidugi, võtan need küsimused vastu. eIDAS ongi Euroopa Liidu määrus, mis on loodud selleks, et lihtsustada piiriülest e-teenuste kasutamist. Kas sa tead, milles see lihtsustamine just seisab? ",output_ID.ee +"user: Tere! Ma olen välisriigi kodanik ja plaanin Eestis äri alustada. Kas mulle on vaja mingit erist elamisluba? +assistant: Tere! Jah, kui sa ei ole Euroopa Liidu riigi, Islandi, Liechtensteini, Norra või Šveitsi kodanik, siis Eestis ettevõtlusega tegelneda on vaja elamisluba ettevõtluseks. See on Politsei- ja Piirivalveameti väljastatav dokument.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas tean, et mobiil-ID on olemas, aga pole päris kindel, mida see tähendab ja kas see on minu jaoks hea? +assistant: Tere! On tore, et huvi on! Mobiil-ID on tegelikult väga hea lahendus, kui vajate digitaalset isikutunnistust. See on tänapäeval mugavam ja turvameelt hoogu andev kui traditsiooniline ID-kaart. Sellega saate viibida eesti.ee-s, internetipankas, või osaleda e-valimistel. Kas teil on küsimusi selle kohta?",output_ID.ee +"user: Tere! Ma olen täiskasvanu ja tahan elamisloakaardi taotlemisega alustada. Kuidas ma riigilõivu maksta saan? +assistant: Tere! Elamisloakaardi taotlemise riigilõivu maksmiseks on mitu võimalust. Saaja on Rahandusministeerium, ja viitenumber on 2900082443. Pangakontod on erinevad: SEB EE891010220034796011, Swedbank EE932200221023778606, Luminor EE701700017001577198 ja LHV Pank EE777700771003813400.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas te saate öelda, kus ma saan RIA DigiDoc mobiilirakenduse uue versiooni alla laadida? +assistant: Muidugi! RIA DigiDoc mobiilirakendus on saadaval nii App Store'ist (https://itunes.apple.com/ee/app/ria-digidoc/id1216104448?mt=8) kui ka Google Play poodist (https://play.google.com/store/apps/details?id=ee.ria.DigiDoc).",output_ID.ee +"user: Tere! Ma olen välisriigi kodanik ja plaanin Eestis äri alustada. Kas ma kindlasti vajan elamisluba selleks? +assistant: Tere! Vaelisriigi kodanikuna jah, enamikul juhtudel vajad. Kui sa ei ole Euroopa Liidu riikide, Islandi, Liechtensteini, Norra või Šveitsi kodanik, siis ettevõtluseks Eestis on vaja elamisluba.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb numeratsiooniga. Mis see täpsemalt tähendab? +assistant: Tere! TTJA väljastab numbrilube sideteenuse osutajatele, ehk sideettevõtjatele, ning ka neile, kes pakuvad muud teenust side teenuse abil, näiteks sisuteenuseid. See tähendab, et nad määravad, kes tohib kasutada erinevaid numbrimudeleid, näiteks telefoninumbreid või mobiilnumbreid.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus TTJA otsuste kohta. Mis teha, kui ma ei nõustu TTJA tehtud otsusega? +assistant: Tere! Kui te ei nõustu TTJA tehtud otsusega, on teil õigus selle vaidlustada. Teil on kaks võimalust: esitada vaie TTJA peadirektorile või esitada kaebus Tallinna Halduskohtule.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on lapsevanematele mingit ohutusraamatut välja andnud. Kas see on tõsi? +assistant: Jah, see on tõsi! Politsei- ja Piirivalveamet ongi valmistanud ohutusraamatu just 1. klassi minevate laste vanematele. See on mõeldud eelkõige lapsevanemate toetamiseks lapse turvalisuse tagamisel.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimusi rahvusvahelise kaitse taotlemise kohta, eriti seoses olukorraga Ukrainas. Kuidas ma saan seda taotleda? +assistant: Tere! Rahvusvahelise kaitse taotlemiseks võite pöörduda nelja kohta: Tallinnas Pärnu mnt 139, Pärnus H. Tammsaare pst 61, Jõhvis Rahu tn 38 või Tartus Riia tee 132. Samuti on saadaval ankeedid Politsei- ja Piirivalveameti veebilehel. Aluseks on põhjendus, et te kardate tagakiusamist Ukrainas rassi, rahvuse, usu, ühiskondliku rühmitusse kuulumise või poliitiliste vaadete tõttu või et tagasipöördumine Ukrainasse kujutab endast tõsist ohtu.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et ma saan ID-kaardiga ilma passita Euroopasse reisida? Kas see on võimalik? +assistant: Tere! Jah, ID-kaardiga saate reisida Euroopa Liitu (EL) ja Euroopa Majanduspiirkonda (EMP) kuuluvad riigid ilma passi kohta. See on võimalik, aga oluline on meeles pidada, et ID-kaart on vajalik reisidokumendina ja EI sobi paljude teiste dokumentidega, nagu näiteks elamisloa kaart või digi-ID.",output_ID.ee +"user: Tere! Ma saan probleeme ID-kaardi kasutamisega. Kas sulle teada on, kuidas kindlustada, et ID-kaart ja lugeja töötavad korralikult? +assistant: Tere! Ma saan aru, et sul on probleeme. Alguses on oluline, et sa veedad hetkel vääris aega ja saad lugema selle artikli: https://www.id.ee/artikkel/paigalda-id-tarkvara. Seega veedad hetkel vääris aega ja saad lugema selle artikli: https://www.id.ee/artikkel/paigalda-id-tarkvara. Siis on oluline, et sinul oleks arvutisse ID-tarkvara paigaldatud, ja vajadusel on ID-kaardi kasutamiseks seadistatud veebibrauserid.",output_ID.ee +"user: Tere! Ma saan probleeme mobiil-IDga allkirjastamisel. Mis võiks olla põhjus? +assistant: Tere! Võib olla mitmeid asja. Sageli on probleem ajutine ja laheneb ise. Palju olgem vaadates veateadet – see sisalda alati vajaliku info probleemi lahendamiseks. Esmalt luge veateade täies ulatuses. Kas sa tead, mis veateadet sa nägid?",output_ID.ee +"user: Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on saanud raha piiriehituseks. Kas see on tõsi? +assistant: Jah, see on tõsi. Politsei- ja Piirivalveamet on saanud toetust piiriehituse rahastamiseks Euroopa Liidu Sisejulgeolekufondi välispiiride ja viisade rahastamisvahendist perioodil 2014-2020.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan ID-tarkvara Ubuntu peale paigaldada. Kuidas ma kõige paremini alustan? +assistant: Tere! Väga hea küsimus. Ubuntu jaoks on kõige mugavam paigaldada ID-tarkvara automaatskripti abil. Alustame sellega: esmalt pead salvestama skripti oma arvuti tahvlkale. Siis avad Terminali ja navigeerid kataloogi, kuhu sa skripti salvestasid. Näiteks, kui sa seda 'Downloads' kataloogi, siis sisestad käskluse `cd /home/kasutajanimi/Downloads` (kasutajanimi asendades sinu kasutajanimega). Skripti käivitamiseks siis `sh install-open-eid.sh` ning parool sisestamisel lihtsalt vajuta edasi.",output_ID.ee +"user: Tere! Kas ma saan Eesti kodakondsuse taotleda, kui ma olen eestkostetav? +assistant: Tere! Jah, võimalik on. Aga on mõned tingimused, mida pead täistõttu täitma. Peamine asi on see, et pead olema vähemalt 15-aastane. Kas te seda teate?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan lapsele ID-kaarti taotleda. Kuidas ma riigilõivu maksta saan? +assistant: Tere! ID-kaardi taotlemisel lapsele riigilõivu tasumisel on mitmeid võimalusi. Kui taotled iseteeninduses, saad riigilõivu tasuda otse sealsamas. Kui aga teed pangaülekande, siis saaja on Rahandusministeerium, viitenumber on 2900082443. Kontopõhjad on erinevate pankade jaoks erinevad: SEB EE891010220034796011, Swedbank EE932200221023778606, Luminor Bank EE701700017001577198, LHV Pank EE777700771003813400.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tean, et on mobiil-ID, aga kas see on nagu tavaline ID-kaart, aga telefoni sees? +assistant: Tere! Jah, täpselt nii! Mobiil-ID on SIM-kaardi põhjal toimiv digitaalne isikutunnistus, mis on samasugune kui tavaline ID-kaart (digi-ID, elamisloakaart) – see on riiklikult garanteeritud. See on lihtsalt kasutuses mobiilseadmega. Kas sulle on oluline teada, kuidas seda kasutada? Kas sa mõtled, kuidas taotleda või kuidas sellega tegelikult midagi teha? ",output_ID.ee +"user: Tere! Olen kuulnud, et mõned tooted on ohtlikud. Kuidas ma saan teada, millised tooted on just praegu probleemiks? +assistant: Tere! EL-is on selleks kiirhoiatussüsteem Safety Gate. Saa tutvuda nädala ülevaatega nende kodulehel https://ec.europa.eu/consumers/consumers_safety/safety_products/rapex/alerts/?event=main.listNotifications&lng=en või kasuta otsingut, kui sul on huvi mingi kindla toote suhtes.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma just sain teada, et meil on uus beebi. Mida me peame nüüd kodakondsuse osas tegema? +assistant: Õnne! Kui teie laps saab Eesti kodakondsuse, siis teile saadetakse posti teel kodakondsustunnistus. See on tähtis dokument! Meil on Politsei- ja Piirivalveamet vastutab selle väljastamise.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus WiFi-seadmete kasutamise kohta. Mis on need piirangud, kui ma näiteks tahaksin kodus selliseid seadmeid kasutada? +assistant: Tere! WiFi-seadmete kasutamisel on oluline teada, et erinevad sagedusribad on reguleeritud erinevate piirangutega. Näiteks 2,4 GHz sagedusel on lubatud võimsus kuni 100 mW ja selle kasutamine ei ole piiratud. Täpsem info on leitav määruse „Raadiosageduste kasutamise tingimused ja tehnilised nõuded sagedusloast vabastatud raadioseadmetele“ lisast 6.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et lennusõitjatel on teatud õigused. Mis need on? +assistant: Tere! Jah, lennusõitjatel on mitmeid õigusi. Näiteks, kui lennud hiljenevad või tühistatakse, on teil õigus teavitus saada, hüvitist nõuda ja abi saada tagasipöörduse korral. Lisaks on teil õigus saada abi lennujaamas kui teil on erivajadused.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma ei ole päris kindel, mis on eIDAS. Kas saate mulle sellest natuke selgeks teha? See on midagi, mis seotakse ID.ee? +assistant: Tere! Jah, eIDAS on Euroopa Liidu määrus, mis seotakse ID.ee-ga. Lihtsasti öeldud, see on seadus, mis näeb ette, kuidas digiteenuseid ja e-tehinguid turvaliselt identifitseerida ja kinnitada. See aitab inimestel digimaailmas ohksele saada. See määrus jõudis jõule 23. juulil 2014. aastal.",output_ID.ee +"user: Tere! Ma olen kuulnud, et relvaloa kehtivust on võimalik pikendada. Mis siis täpsemalt on sellega seotud, kui ma tahan seda enda puhul teha? +assistant: Tere! Relvaloa kehtivuse pikendamisel on oluline, miks sa relvaloa üldse omad. Kui relva otstarbeks on märgitud jahipidamine või sport, siis täiendavat laskekatset ei pea tegema. Kui aga on märgitud ainult enese ja vara kaitseks, siis pead selleks täiendava laskekatse sooritama.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et mul on ID-kaart ja ma tahan sellega arvutis midagi tegema. Mis ma pean tegema, et see tööle hakkaks? +assistant: Tere! Üsna hea küsimus! Et saaksid ID-kaardi arvutis kasutama, pead paigaldama ID-tarkvara. See tarkvara on sinu arvuti jaoks vajalik, et ID-kaardiga elektroonilisi toiminguid teha. Kas sa kasutad Windowsi, macOS või Linuxi operatsioonisüsteemi?",output_ID.ee +"user: Tere! Ma olen kuulnud, et Politsei- ja Piirivalveamet kasutab minu isikuandmeid, kui ma saan teiega ühendust. Kas see on nii ja millal? +assistant: Tere! Jah, see on õige. Me kasutame isikuandmeid pöördumiste lahendamiseks ja teile vastamiseks. Kui peame pöördumist lahendades küsimusi esitama teistele asutustele, edastame neile ainult need andmed, mis on vastuse koostamiseks tõesti vajalikud. See aitab meil privaatsust kaitsta.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas teeksite mulle infot ID-tarkvara uusima versiooni kohta? +assistant: Tere! Jah, küllap. ID-tarkvara uusimad versioonid on ID.ee lehel saadaval. Meie süte automaatselt teie arvuti operatsioonisüsteemi ja pakub sobiva paketi. On lihtne, peate vaid tarkvara alla lahtima ja paigaldama.",output_ID.ee +"user: Tere! Ma lugesin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb ka kauplejate komisjoni töoga. Kuidas see üldiselt toimib, kui mul on mingi probleem kauplusega? +assistant: Tere! Üldjuhul, kui sul on probleem, esitad avalduse TTJAs. Komisjon annab kauplejale aega oma seisukohti selgitada ja vastata sinu väidetele. See annab neile võimaluse probleemi lahendada enne, kui asi jõuab komisjoni istungini.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) on tellinud mitmeid uuringuid. Kas sa saaksid rääkida neist? +assistant: Tere! Jah, TTJA on tellinud mitmeid uuringuid. Üks olulisi on uuring passiivse lairibataristu rajamise kohta maapiirkondadesse, mille tellis Majandus- ja Kommunikatsiooniministeerium. Lisaks on tehtud uuring Eesti elanike tarbijate õiguste teadlikkusest ja e-kaubanduse turust.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et tean, et ID.ee on olemas, aga ma ei ole päris kindel, kuidas digiallkirjastamist oma veebisüsteemi juurde lisada. Kas teil on näiteks lihtne viis, kuidas seda saaksin teha? +assistant: Tere! Päev! Jah, digiallkirjastamist saab lisada oma veebisüsteemi mitmel viisil. Alustuseks on Riigi allkirjastamisteenus (SiGa) valitsusektori jaoks väga hea võimalus. See on aga tarkvara, mida peab enda serverile paigaldama. Kas teete veebirakendusi väljapoole valitsust?",output_ID.ee +"user: Tere! Ma olen ajakirjanik ja tahaksin teada, kuidas Politsei- ja Piirivalveametiga pressilisti liikmeks saada. Mis on selleks vaja? +assistant: Tere! Liitumine on lihtne. Saate saata e-kirja aadressil, mis on kirjeldatud ametisõnumites. Kirjutage kirja enda nimi, väljaande nimi ja e-posti aadress.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on mõned küsimused selle kohta, mida ma saan teha, kui ma olen veebist midagi ostnud ja ma ei ole sellega rahul. Mis on minu õigused? +assistant: Tere! Kui oled midagi veebist ostnud, on sul õigus lepingust taganeda 14 päeva jooksul alates sellest, kui kaubad sinuni jõudsid. See on nn 14-päevane taganemisõigus. See kehtib ka teenuste puhul, kus taganemisõiguse tähtaeg algab lepingu sõlmimisest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen Tarbijakaitse ja Tehnilise Järelevalve Ameti uus töötaja. Üks minu ülesandete kohta on raadiosageduste kasutamine. Kust ma peaksin alustama, et aru saada, kuidas seda Eestis reguleeritakse? +assistant: Tere! Suurepärane! Alustada saaks Eesti raadiosagedusplaanist. See on meil avaldatud ja saadaval Riigiteataja veebilehel: https://www.riigiteataja.ee/akt/125012019006. See dokument annab kõige olulisema ülevaate sellest, kuidas Eestis raadiosagedusi kasutatakse ja mis on sellega seotud reeglid.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma sain uue ID-kaardi ja koodiümbriku. Kas ma pean kohe PIN- ja PUK-koodid vahetama? +assistant: Tere! Jah, väga hea, et sa sai uue ID-kaardi ja koodiümbriku! Soovitav on PIN- ja PUK-koodid kohe vahetada. See tagab parema turvalisuse. Koodiümbrikus on uusad koodid, ja on hea, kui sa neid kohe uusima kaardi külge kinnitatakse.",output_ID.ee +"user: Tere! Ma kuulsin, et Rail Balticu ehitamine on keeruline projekt. Mis kõige suuremad väljakutsed sellega seoses on olnud? +assistant: Tere! Jah, Rail Balticu ehitamine on tegelikult üsna keeruline. Üks suurim väljakutse on see, et olemasoleva raudtee ümberehitus, näiteks kurvide sirgemaks tegemine või teise rööpapaari lisamine, on tehtud analüüside järgi kallim kui uue trassi ehitamine. Lisaks on oluline arvestada, et Tartu kaudu minev trass ei võimalda kasutada Euroopa ühendamise rahastu toetust samas mahus.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma ei ole päris kindel, mis need sertifikaadid on ja miks on need nii olulised. Kas saaksid seda veidi selgitada? +assistant: Muidugi! Ühest küljest on sertifikaadid nagu digitaalne isikutunnusjoon, mis seob sind sinu digitaalse isikutuvastusvahendiga, näiteks ID-kaardiga või mobiil-ID-ga. See tähendab, et kui sa kasutad e-teenust, kasutatakse seda sertifikaati, et kinnitada, et sa oled see, keda sa väidate olevat. On nagu digitaalne kiri, millega sa kinnitad oma identiteedi. Kuidas sa need tavaliselt kasutad?",output_ID.ee +"user: Tere! Ma saan aru, et RIA DigiDoc on mobiilirakendus. Kas see on täielikult ligipääsetav kõigile kasutajatele, kes seda kasutavad? +assistant: Tere! Jah, RIA DigiDoc on ehitatud vastavalt WCAG 2.1 AA suunetele, mis tähendab, et seda on mõeldud kasutamiseks ka inimestele, kellel on nägemisvõimete puudega või muud liiklusvõimete probleemid. Küll aga on teatud piirkonnad, kus ligipääsetavus pole täielik. Meie teates on detailsemalt kirjas, milles need puudumised on.",output_ID.ee +"user: Tere! Ma plaanin elamisloakaardi taotlemist. Kuidas ma riigilõivu maksta saan? +assistant: Tere! Elamisloakaardi taotlemisel riigilõivu tasumiseks on mitmeid võimalusi. Saaja on Rahandusministeerium. Viitenumber on 2900082443. Pangad, kuhu saad maksta, on SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor Bank (EE701700017001577198) ja LHV Pank (EE777700771003813400).",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen ostmas kuldehit, aga ma ei tea, millele ma peaksin tähelepanu pöörama. Mida peaksin kontrollima? +assistant: Tere! Kuldehit ostes on oluline, et sellel oleksid proovi- ja nimemärgid. Proovimärgis näitab, millist metalli on kasutatud ja selle osa sulamis, näiteks 375, 585 või 916. See märk on tuhandeksüsteemis. Nimemärgis on tootja või maaletoja nimi. See peab olema registreeritud riiklikus registris. Kas sa teadsid seda?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma ostsin hiljuti televiisori, aga see ei tööta korralikult. Kuidas ma peaksin selle kohta kaebust esitama? +assistant: Tere! Esiteks, otsige üles ostutšekk või mingi muu dokument, mis tõendab ostu. See on oluline. Seejärel pöörduge viivitamatult selle poodi, kust televiisori ostsite. Kaebuse esitamine peaks toimuma hiljemalt kahe kuu jooksul alates vea avastamisest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma plaanin elamisloakaarti taotleda. Kus ma teenindusse ma peaksin minema ja mida ma kaasa võtma peaksin? +assistant: Tere! Elamisloakaardi taotlemiseks saad valida sobiva teeninduse kohta Politsei- ja Piirivalveameti veebilehelt. Kindlasti kaasa isikut tõendav dokument. Sa saad aja broneerida veebilehel https://broneering.politsei.ee/ kui soovid.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma mõtlesin, mis see Mobiil-ID on? Kas see on uus ID-kaart? +assistant: Tere! Mobiil-ID on juurdekasv ID-kaardi kasutamisele. See on mobiiltelefoniga kaasasolev vebinimiliselt kindel identifikaator. See annab teile juurdejuurdepääsu e-teenustele, ilma et peaksite ID-kaarti kaasas kandma. Kas te mõtlete, et see on samasugune kui ID-kaart? See on juurdekasv!",output_ID.ee +"user: Tere! Ma olen veidi segadus ID-kaardi taotlemisel. Kuidas liikumisvõimetule inimesele ID-kaart taotleda saab? Mis on kõige lihtsam viis? +assistant: Tere! Liikumisvõimetule inimesele on mitmeid võimalusi ID-kaardi taotlemiseks. Iseteeninduses saab taotleda, kui te olete täiskasvanu või alaealine laps, ja teil on ID-kaart, mobiil-ID või Smart-ID. Seal peate üles laadima foto ja maksma riigilõivu pangalingi kaudu. Kas teil on iseteenindusega kogemust?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tean, et võin taotleda alaline elamisoigust EL-ist pärit inimesena. Aga kuidas see poliisiasutus selle menetletakse? +assistant: Tere! Jah, seda saab teha. Politsei- ja Piirivalveamet kontrollib taotluse. Nad peaksid veenduma, et teil on kõik nõuetekohased dokumendid, ja et teil on täidetud kõik elamisõiguse saamiseks vajalikud tingimused. Küll aga ei pruugiks teil midagi lisaks kuuldagi, kui kõik on korras. Kas te soovite rohkem teada?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on probleem Windowsiga ID tarkvara paigaldamisel. Ilmnes mingi tõrk. Kas te saate mind aidata? +assistant: Tere! Ma saan küll. Palun räägi rohkem sellest tõrkest. Kas sa saad mingi veaõht (koodiga) või veateate? Ja millise ID tarkvara sa paigaldate?",output_ID.ee +"user: Tere! Ma tahan küsida Kuressaare arestimaja kohta. Kas sinna saab pakke tuua? +assistant: Tere! Kuressaare arestimajas ei saa arestalustele pakke tuua. Vahistatule aga lubatakse üks pakk kuus alates 01.08.2020.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TJAA) tegelda koduohutusega. Mis see täpsemalt tähendab? +assistant: Tere! Nõmme, TJAA jälgib, et ehitis oleks ohutu kogu oma eluea jooksul. Põhimõte on, et omanik vastutab ehitise ohutuse eest. Korterelamus on oluline vaheline erinevus reaalosade ja kaasomandi vahel. Mis on sinu jaoks kõige olulisem selles?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et Tarbijakaitseamet kaitseb tarbija õigusi. Aga mida see täpsemalt tähendab? Millised on mu peamised õigused ostjana? +assistant: Tere! Jah, Tarbijakaitseamet on seal, et kaitsta sind tarbijana. Peamiselt sa saad nõuda kaupa või teenust, mis vastab nõuetele, on ohutu ja mida ei ole keelatud omada või kasutada. Samuti on sul õigus saada vajalikku teavet pakutavate kaupade ja teenuste kohta, et sa saaksid teadlikult valida. Ja kui su õigusi rikutakse, saad nõu ja abi Tarbijakaitseametilt!",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et politsei on mõnikord avalikule koosoledule sekkuda. Miks nad seda teevad, ja millistel juhtudel? +assistant: Tere! Jah, see on tõsi. Politsei saab avalisele koosoledule sekkuda, kui arvab, et on olukord, kus koosoleku pidamine ohustab inimelu või seaduslikkust. Näiteks, kui on alust arvata, et kohas, kus koosolek toimub, on olukord, kus saab tõesti kiiresti süütu tekitada. Mida täpsemalt sa küsid?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma nägin seda MyID portaali. Mis see üldse on? Kas see on midagi uut? +assistant: Tere! MyID portaal on uus platvorm, mida pakub SK ID Solutions AS (SK), mobiil-ID, ID-kaardi, Digi-ID ja Smart-ID kontode kohta. See on koht, kus saad teavet nende kontode kohta ja sellest, mida neis tehtud on. Kas sa tahad teada rohkem, kuidas seda kasutada või mis teeb selle eriliseks? ",output_ID.ee +"user: Tere! Mul on küsimusi rõivaste ja jalatste märgistamise kohta. Mis on kõige olulisem, mida ma peaks ostes vaatama? +assistant: Tere! Oluline on vaadata, kas rõival või jalatseid on näidatud toote nimetus, suurus, materjalikoostis (eriti oluline, kui ei ole nahast või karusnahast) ja hooldusjuhised ehk hooldustähistus. Need on põhilised asjad, mis annavad sulle olulist teavet toote kohta.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet diff --git a/experiments/ood_detection/data/train.csv b/experiments/ood_detection/data/train.csv new file mode 100644 index 00000000..0370ac03 --- /dev/null +++ b/experiments/ood_detection/data/train.csv @@ -0,0 +1,791 @@ +conversation,agency +"user: Tere! Ma tahaksin teada, kuidas lapsele passi taotlemine toimib. Kus ma peaksin selle aja broneerima? +assistant: Tere! Lapse passi taotluse menetluse aega saad broneerida Politsei- ja Piirivalveameti teeninduses. Broneeringu võimalus on saadaval aadressil https://broneering.politsei.ee/ või vaata teenindusi aadressil (/et/asukohad/teenindused).",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma just hakkasin tarbija õiguste kohta rohkem huvi tundma. Mis on kõige olulisem asi, mida tarbijana teada tuleks? +assistant: Tere! Kõige olulisem on see, et Sul on õigus nõuda ja saada kaup või teenus, mis vastab nõuetele ja on ohutu Sinu elule, tervisele ja varale. Lisaks peab kaupleja andma Sulle selle kohta vajalikku teavet, et Sa saaksid teadlikult otsuse teha.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus elamisloakaardi taotlemise kohta. Kuidas ma üldiselt sellele protsessile alustan? +assistant: Tere! Elamisloakaardi taotlemiseks pead sul olema sobiv teenindus ja isikut tõendav dokument. Sa saad teeninduses aega broneerida või tulla lihtsalt kohale. Broneeringu sooritamiseks vaata seda linki: https://broneering.politsei.ee/",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma ei ole päris kindel, mis Mobiil-ID on. Kas see on uus ID-kaart nutitelefonile? +assistant: Tere! Jah, Mobiil-ID on täpselt selline – see on digitaalne isikutunnistus, mis on alati sinuga nutiseadmega kaasas. See põhineb SIM-kaardil ja on riikliku garantii all, nagu ka tavaline ID-kaart (digi-ID, elamisloakaart). See on väga sarnane, aga selle saab kasutada just nutiseadmest.",output_ID.ee +"user: Tere! Ma arvan, et digitaalsed dokumendid on nagu tavalised dokumendid, aga elektroonilised. Kas seda täiesti õigesti arvan? +assistant: Tere! Üsna täiesti õigesti! Digitaalsed dokumendid on tegelikult tavalised dokumendid – nagu ID-kaart, elukirja või passi – aga need on kujutatud digitaalselt, elektroniliselt. See tähendab, et neid saab hoida ja kasutada numbriliselt, mitte ainult paberil. Kas sa tahad teada rohkem nende erinevatest tüüpides või sellest, kuidas neid kasutada?",output_ID.ee +"user: Tere! Ma arvan, et mobiil-ID on juurde tulnud. Aga kuidas see töötab? Kas ma pean mingit eraldi seadet kasutama? +assistant: Tere! Jah, mobiil-ID on juurde tulnud ja selle abil saad e-teenustesse sisse logida ilma ID-kaardita. Pead seda üldiselt eraldi seadetega kasutada ei pea - see töötab nii arvutis, tahvelarvutis kui ka telefonis. Kui sa seda kasutad, siis piisab sellest, et sa mäletaksid oma mobiil-ID PIN-koode. Kas sul on kunagi olnud ID-kaart või oskad sellega sisse logida?",output_ID.ee +"user: Tere! Ma lugesin seda artiklit sideteenuste kohta, aga mul on veidi segadus. Mis on kõigepealt oluline teada roamingu kohta? +assistant: Tere! Oluline on teada, et roaming on sideteenus, mis algab sinu koduriigis ja lõppeb teise Euroopa Liidu liikmesriigi telefonil. See on teenus, mille eest tavaliselt tasutakse tegeliku kasutamise põhjal. Artiklis on ka infot selle kohta, kuidas roamingu tasud on piiratud.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulen, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb sideteenuste tarbijate kaitsega. Kuidas nad seda teevad? +assistant: Tere! TTJA reguleerib sideteenuste valdkonda eriseaduse, elektroonilise side seaduse kaudu. Selle seaduse 9. peatükk keskendub just sideteenuste osutamisele lõppkasutajale ja nende õiguste kaitsele. See seab nõudeid näiteks lepingu sõlmimisele, rikete kõrvaldamisele ja lepingu muutmisel.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimusi droonide kasutamise kohta Eestis. Mis on kõige olulisem asi, mida ma peaksin teadma nende kasutamisel, eriti sageduste osas? +assistant: Tere! Droonide puhul on oluline teada, et raadiosagedusi kasutatakse nii drooni juhtimiseks kui ka video edastamiseks. Paljud droonid, mida kasutatakse hobina, ei vaja sagedusluba, kuid need peavad siiski töötama Euroopa standarditele vastavate sageduste piires, mis on määratud Eesti raadiosagedusplaanis (ERSP).",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kas ma saan lapse välismaalase passi taotleda Politsei- ja Piirivalveametist? +assistant: Muidugi! Jah, saab. Aga kõigepealt on oluline teada, kus ja kuidas seda kõige paremini teha. Kui laps on alla 18aastane, siis passi taotlemine toimub tavaliselt Politsei- ja Piirivalveametis. Küll aga on oluline mõelda, kust taotlus kõige paremini vastuvõetakse ja mida sinu lapse puhul tähendab kiireks tegemiseks (kiire kohalviisa).",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma sain uue ID-kaardi, aga ei oska PIN- ja PUK-koodi kasutada. Mis see kõigega kokku tegemist on? +assistant: Tere! Tore, et sa uue ID-kaardi said! PIN- ja PUK-koodid on need, mis sinu ID-kaardiga elektrooniliselt ID-d tõestada panevad. Nii, et ilma neist ei saa ID-kaarti kasutada. Üks asi, mis väga oluline on, on nende turvalisus. Peame need koodid omaette kaardist eemal hoida, et keegi neid ära ei võiks. Kas sa tead, kus need koodid on?",output_ID.ee +"user: Tere! Mul on küsimus – mida tähendab välismaalase passi taotlemine? Mida see üldse on? +assistant: Tere! Välismaalase pass on reisidokument, mille Eesti Vabariik annab välismaalasele. See on nagu reisipass, aga just sinule, kui sa ei ole Eesti kodanik.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan oma relvaloa kehtivust pikendada. Kas ma pean selleks laskekatset tegema? +assistant: Tere! See oleneb relvaloa otstarbest. Kui relvaloa eesmärk on ainult enese ja vara kaitse, siis jah, pead täiendava laskekatse sooritama. Kui aga on märgitud jahipidamine või sport, siis laskekatset ei pea tegema.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus Eesti politsei lipust. Mis on selle peamine värv ja mida see sümboliseerib? +assistant: Tere! Eesti politsei lipu peamine värv on sinine. See värv sümboliseerib rahu ja stabiilsust, sest see on ka üks meie riigilipu värve.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas te saate öelda, kus Rakvere politseijaoskond asub? +assistant: Muidugi! Rakvere politseijaoskond asub aadressil Fr. R. Kreutzwaldi 5a, 44314 Rakvere. See on väga kerge kohas leida, järgige peatus „Teater“ bussid nr 5 või 5A! Kas sulle täpsem info selle kohta vaja on?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib sideturgu. Mis need täpselt on ja mida TTJA seal teeb? +assistant: Tere! Sideturgude all silmitakse erinevaid teenuseid, nagu näiteks kõne lõpetamise hulgateenus telefonivõrgus, mobiiltelefonivõrgus, ning kohalik juurdepääsu hulgateenus või masstoodete keskse juurdepääsu hulgateenus. TTJA peab silmas elektroonilise side turgude arenguid, analüüsib neid ja annab vajadusel ka loa rändlusteenustel lisatasude rakendamiseks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Ma olen kuulnud, et on mingid reeglid, mis piiravad roaming-tasusid. Kas see on tõsi? +assistant: Jah, see on tõsi! Euroopa Liidus on reeglid, mis piiravad, kui palju sinult roaming-teenuste eest küsitakse. Näiteks kõneminuti kohta ei tohi küsitud tasu ületada 0,19 / 0,228 eurot ja SMS-i kohta 0,06 / 0,072 eurot.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma mõtlesin alustada mobiil-ID kasutusega. Kas see on midagi, mis on uus või midagi, millele ma võin minna? +assistant: Tere! Päris tore, et huvitatud olete! Mobiil-ID on kindlasti modernsem ja mugavam lahendus kui vana ID-kaart. See on riiklikult tunnustatud digitaalne isikutunnistus, millega saate juurida erinevaid e-teenuseid. Kas te kujulite, et teate, mis see on?",output_ID.ee +"user: Tere! Ma arvan, et ID-tarkvara on päris oluline. Kas te saate mulle öelda, mis tarkvaraversiooni toeperioodi pikkus tähendab? +assistant: Tere! Jah, seda on oluline mõistata. Tarkvaraversiooni toeperioodi lõpp tähendab, et ID kasutajatugi enam seda versiooni teadmistebaasis enam ei uuenda. Lisaks sellele ei testita enam selles versioonis viidatud vigade olemasolu, ja tehnilisi lahenduse muudatusettepanekuid ei vastu võeta.",output_ID.ee +"user: Tere! Ma arvan, et ma tean, et digiallkirjastatud failid on ühel kujul, aga kas .bdoc ja .asice on ühesellegi sarnaselt? Kas need on täpselt sama asi? +assistant: Tere! Jah, see on väga hea küsimus. Peamijärjekorda on see, et mõlemad on digitaalsed allkirjad, aga nad on tehniliselt erinevad. .bdoc oli varem kasutatud formaat, kuid .asice on praegune standard. Pidev muutmine on selleks, et sobiks rahvusvaheliselt paremini. Kas sul on mõni konkreetne küsimus, millele sa vastust vajad?",output_ID.ee +"user: Tere! Olen kuulnud, et Tarbijakaitseamet on uuringuid teinud. Mis neid puudutab kõige viimati? +assistant: Tere! Jah, viimase aja uuringuid on mitmeid. Näiteks on valminud uuring, mis annab hinnangu 1 Gbit/s kiirust võimaldava passiivse lairibataristu rajamise maksumus maapiirkondades. Samuti on EKI teinud uuringu tarbijate teadlikkusest nende õigustest veebiküsitlusega.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Sain palju informatsiooni laenude kohta. Kuidas ma saan aru, mida ma peaksin tegema, kui ma hakkasin laenumaksetega hiljaks jääma? +assistant: Tere! Kui oled hätta laenumaksetega, on oluline hakata lahendusi otsima enne, kui võlgu jääd. Korrektse maksekäitumisega kliendil on lihtsam laenuandjaga kompromisse leida. Kas sa soovid, et ma rääkiks sellest, milliseid võimalusi pangad üldjuhul pakuvad?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mida tähendab kemikaal Tarbijakaitse ja Tehnilise Järelevalve Ameti definitsiooni järgi? +assistant: Tere! Tarbijakaitse ja Tehnilise Järelevalve Ameti definitsiooni järgi on kemikaal mistahes aine või segu, mis sisaldab või tekitab üht või enamat toimeainet ja mille eesmärk on kahjulike organismide hävitamine, tõrjumine, kahjutuks muutmine või võitlemine nendega muul viisil kui puhtfüüsikaliste toimingutega. See võib olla ka näiteks puhastamiseks ettenähtud pindaktiivseid aineid sisaldav aine.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et e-ostmine on mugav, aga ka riskantne. Mis peab silmas pidama, kui tellin midagi veebipoest? +assistant: Tere! Täiesti õige, mugavus käib käpa sees riskiga. Kõigepealt on oluline teada, kas veebipood kuulub juriidilisele isikule või eraisikule. Vaata, kelle pangakontole raha läheb ja kas ettevõte on registreeritud äriregistris. Eraisikult ostes ei kehti sulle seadusest tulenev kaitse, näiteks 14-päevane taganemisõigus.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kas mul on vaja sertifikaate ID-kaardi jaoks? Ma ei ole kindel. +assistant: Tere! Jah, ID-kaardi kasutamiseks on vajalikud sertifikaadid. Need garanteerivad teie identiteedi elektroonilises keskkonnas. Need annavad võimaluse sisse logida e-teenustesse ja digitaalseid allkirju anduda. Kas te olete varem sertifikaate kasutanud?",output_ID.ee +"user: Tere! Ma olen huvitatud elektriseadmete kohta teadmise saamisest, eriti seoses Tarbijakaitse ja Tehnilise Järelevalve Ameti nõuetele. Mis on üldiselt oluline, et elektriseade oleks seaduslikult turul saadaval? +assistant: Tere! Oluline on see, et elektriseade oleks valmistatud Euroopa Liidus kehtivate ohutustehnika tavade kohaselt ja ei tohi ohustada inimeste tervist, kui seda õigesti paigaldatakse ja kasutatakse. Enamus elektriseadmeid peab olema varustatud CE-märgise, kuna need kuuluvad madalpinge-, elektromagnetilise ühilduvuse- ja ohtlike ainete sisaldusdirektiivide alla.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimusi seoses toodete ligipääsetavusega. Kust ma peaksin alustama? +assistant: Tere! Kõigepearem on hea teada, et Eesti seadus toodete ja teenuste ligipääsetavuse kohta tugineb Euroopa Liidu direktiivile. Selle kohta on Eestis koostatud mitmeid määrusi, näiteks määrus nr 46, mis käsitleb ettevõtja ebaproportsionaalse koormuse hindamist. Alustage lugemine eelnimetatud määrusest, kui te olete tootja või teenuse osutaja.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kas te saate öelda, kus ma saan RIA DigiDoc mobiilirakenduse uusima versiooni alla laadida? +assistant: Muidugi! Uusima versiooni saate alla laadida App Store'ist: https://itunes.apple.com/ee/app/ria-digidoc/id1216104448?mt=8 või Google Play Store'ist: https://play.google.com/store/apps/details?id=ee.ria.DigiDoc.",output_ID.ee +"user: Tere! Ma kuulsin midagi raadiosagedustest ja Tarbijakaitse ja Tehnilise Järelevalve Ameti seotusest. Mis see tähendab? +assistant: Tere! Raadiosageduste kasutamist Eestis reguleeribki Tarbijakaitse ja Tehnilise Järelevalve Amet. See tähendab, et nad jälgivad, kuidas me neid sagedusi kasutame, et ei oleks häireid ja kõik toimiks korrektselt. Suur osa selle info on kättesaadav Euroopa infobaasis, nimega EFIS.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen välismaalane ja plaanin Eestis ettevõtlusega alustada. Kas te saaksite mulle rääkida, millised on elamisloa tingimused füüsilisest isikust ettevõtjale (FIE)? +assistant: Tere! Jah, muidugi. Ettevõtluseks elamisluba FIE-le saada, on mitmeid tingimusi. Peamiseks on see, et olete oma ärisse Eestis vähemalt 16 000 eurot investeerinud ja olete registreeritud Eesti Äriregistris. Kas te olete juba Äriregistris registreeritud?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet tegeleb koduohutusega. Mis see täpselt tähendab? +assistant: Tere! Jah, me tegeleme koduohutusega. See tähendab, et me jälgime, et sinu kodu oleks ohutu kasutada – ehitise konstruktsioonid, tehnosüsteemid, kõik see peab olema korras. Ehitise omanik on vastutav selle ohutuse eest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin, et on selline Tarbijavaidluste komisjon. Mis see komisjon tegelikult on ja millal ma saan seda kasutada? +assistant: Tere! Tarbijavaidluste komisjon on Tarbijakaitse ja Tehnilise Järelevalve Ameti juures tegutsev organ. Sa saad seda kasutada siis, kui sul on tekkinud vaidlus kauplejaga ja teinud kaubandusvaidluse lahendamisel jõuetud oled. Komisjon vaatab sinu juhtumit üle ja püüab leida lahenduse.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma ei ole päevalt aru saanud, mis MyID portaal on ja mida ta pakub. Kas saate mulle sellest pisut rääkida? +assistant: Muidugi! MyID portaal on ID.ee poolt pakutav keskoht, kus saab infot teatud digitaalsete identifikaatorite kohta, nagu Mobiil-ID, ID-kaart, Digi-ID ja Smart-ID. Need on kõik sertifikaate ja tehinguid pidav digitaalne identifikaatorid, mis on väljaõnnestatud SK ID Solutions AS (SK) poolt. Kas teiega on oluline teada, mida need identifikaatorid täpselt teevad?",output_ID.ee +"user: Tere! Mul on küsimus teenuse ohutuse kohta. Mis on minu õigused tarbija? +assistant: Tere! Tarbija õigus on see, et teenus peab vastama kehtestatud nõuetele. Kui erinõudeid pole, siis teenuse tavakasutus ei tohi ohustada sinu elu, tervist või vara. Kas sa oled huvitatud teadma, kuidas teenuse pakkuja seda tagab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin, et Tarbijavaidluste Komisjon (KKK) aitab probleemidega, mis kauplejatega tekivad. Kas komisjoni poole pöördumine on tasuta? +assistant: Tere! Jah, on õige. Tarbijavaidluste komisjoni poole pöördumine, avalduse läbivaatamine ja tarbijavaidluse menetlus on täiesti tasuta. Teil ei pea selle eest midagi maksma.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen huvitatud Narva arestimajast. Kas ma saan sinna keegi poeletti tuua? +assistant: Tere! Narva arestimajja ei saa arestalistele pakke tuua. See on oluline reegel, mida tuleb järgida.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan teada, mida ma saan teha, kui mulle keeldutakse viisast. Mis on mu võimalused? +assistant: Tere! Kui Teile keeldutakse viisast, on Teil õigus selle otsuse vaidlustada. Teil on võimalik PPA-le esitada vaideavaldus keelduvast otsusest.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kaotanud oma rahakoti Jõhvis. Kas te teate, kust ma saan politseisse pöörduda? +assistant: Tere! Jõhvi politseijaoskond asub Rahu 38, 41588 Jõhvi. Nad on avatud esmaspäevast reedeni kell 9.00 kuni 17.00.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin palju Rail Balticu kohta. Kas sa saaksid mulle öelda, miks praegune trass kõige kallim variant on? +assistant: Tere! Jah, muidugi. Analüüs näitas, et olemasoleva trassi ümberehitus, nagu kurvide sirgemaks tegemine ja teise rööpapaari lisamine, oleks kallim kui uus trass. Lisaks ei oleks võimalik Euroopa ühendamise rahastu toetust täiendavalt kasutada Tartu ja Valga trassi puhul samas mahus kui uue trassi puhul.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et probleem on ID kaardiga. Ma ei saa seda kasutada. Kas teil on midagi, mis aitab? +assistant: Tere! Jah, meil on ID kaardiga seotud küsimustele vastuseid. Palju täpsemalt, kui probleem on, seda parem on abi. Kas saate palun öelda, mis täpselt probleem on? Kas on viga lugemisega, või ei saa seda üldse kasutada?",output_ID.ee +"user: Tere! Mul on küsimus Tarbijakaitse Ameti kohta. Kuidas ma saan kaebust esitada, kui ostuga on probleeme? +assistant: Tere! Kui Teil on ostuga probleeme, on kõigepealt soovituslik pöörduda otse müüja poole. Esitage kaebus võimalikult kiiresti, aga hiljemalt kahe kuu jooksul alates vea avastamisest. Peaksite olema valmis näitama ostutšeki, lepingu või muu tõend, et ostate kaupu või teenuseid.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et minu ID-tarkvara on vanamatu. Kuidas ma saan selle uuemaks tegema? +assistant: Tere! Päev! Võib olla, et on aeg selle uuemaks tegema. ID-tarkvara versiooninumber annab teada, kui uus tarkvara on. Mida suurem number, seda uuem. Kuidas sa siis selle uuemaks tegema soovid? Sa saad selle lehele laadida: [ID-tarkvara leht] (lahtiselt ID.ee lehelt).",output_ID.ee +"user: Tere! Ma nägin, et Politsei- ja Piirivalveamet on Facebookis. Kas te mulle ökaks, kus on PPA peamine Facebooki leht? +assistant: Tere! Jah, PPA on aktiivne Facebookis. PPA peamine Facebooki leht on https://www.facebook.com/politseijapiirivalveamet/. Seal saab leida palju teavet politseitöö kohta ja küsida küsimusi. Kas sulle see leht huuma?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud, et droonide kasutamine on muutunud üsna levinud. Kas Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib nende kasutamist? +assistant: Jah, küllap! TTJA jälgib droonide kasutamist, eriti seoses raadiosageduste kasutusega. Droonid kasutavad neid sagedusi nii juhtimiseks kui ka video edastamiseks. Oluline on, et need seadmed vastaksid nõuetele, sealhulgas CE-märkise olemasolule.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin Rail Balticu kohta palju juttu. Kus saab infot selle keskkonnamõjude hindamise kohta? +assistant: Tere! Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) avalikustab selliseid teateid. Need on leitavad siit: https://ttja.ee/eraklient/tarbija-oigused/avalikud-teated/keskkonnamoju-hindamisega-seotud-teated",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mulle on veidi segadus reklaamide osas. Kas saaksid mulle selgitada, mida Tarbijakaitse ja Tehnilise Järelevalve Amet reklaamide kohta ütleb? +assistant: Tere! Muidugi. Tarbijakaitse ja Tehnilise Järelevalve Ameti (TTJA) kohaselt peab reklaam tavaliselt kohe nähtavale hüppama, et teada, et see on reklaam. See peab selgelt eristuma teistest teadetest. Samuti peaks reklaamist selgeks saama, kes reklaami tellija on – näiteks nime, kaubamärgi või domeeninihi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma saan aru, et Ubuntu tarkvara paigaldusega seoses ID-kaardi kasutamine on üsna oluline. Kuidas ma kõige paremini ID-tarkvara Ubuntu-sse saan paigaldada? +assistant: Tere! Jah, see on oluline. Parim viis on automaatskripti abil. Sa saad selle siin: https://installer.id.ee/media/install-scripts/install-open-eid.sh. Eesmärk on avada Terminal ja navigeerida faili kataloogi, kuhu see on salvestatud. Näiteks, kui sa salvestasid selle ‘Downloads’ kataloogi, siis käsk on ‘cd /home/kasutajanimi/Downloads’. Pärast seda, skripti käivitamine 'sh install-open-eid.sh' käskuga. Kas sa salvestusid selle faili kuhagi või ei ole sa sellele veel jõudnud?",output_ID.ee +"user: Tere! Ma olen kuulnud, et saab täiskasvanuna Eesti kodakondsust taotlema. Kuidas see menetlus käib umbes? +assistant: Tere! Jah, see on võimalik. Pärast kodakondsuse taotluse esitamist saadetakse dokumendid Vabariigi Valitsusele. Nad teevad otsust, kas kodakondsus antakse.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen välisriigi kodanik ja mul on Eesti elamisluba. Mis on välismaalase pass ja kas mul on selleks õigus? +assistant: Tere! Välismaalase pass on Eesti Vabariigi poolt välismaalasele antav reisidokument. Selleks on sul õigus, kui sul on kehtiv Eesti elamisluba või elamisõigus ning sa suudad tõestada, et sul puudub välisriigi reisidokument ja sa ei saa seda ka hankida.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas te saate öelda, kus asub Rakvere politseijaoskond? +assistant: Muidugi! Rakvere politseijaoskond asub aadressil Fr. R. Kreutzwaldi 5a, 44314 Rakvere. See on väga lihtne kohaks leida!",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et ID tarkvara on jäänud natuke vanaks. Miks on nii oluline, et seda uuendada? +assistant: Tere! On õige jutt. ID tarkvara peab perioodiliselt uuendama, sest see on turvalisuse ja selle tööefektiivsuse tagamine. Peaasi, et tarkvara oleks uusim versioon, et vältida kriitilisi turvariskeid. Kui tarkvara ei ole uusim, ei saa sellega kindlalt digitaalselt allkirjastada või kontrollida juba antud allkirjade kehtivust.",output_ID.ee +"user: Tere! Ma olen kuulnud, et Tarbijakaitseamet menetleb kaebusi kauplejatega tekkinud vaidluste puhul. Kuidas see menetlus üldiselt käib, kui ma esitan avalduse? +assistant: Tere! Jah, see on õige. Kui esitad Tarbijakaitseametile avalduse, saad teavitus 21 tööpäeva jooksul, et avaldus on menetlusse võetud. Seejärel edastatakse avaldus kauplejale, et nad saaksid anda selgitusi ja leida lahendust vaidlusele. Sinule saadetakse ka täiendav teavitus selle kohta.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kas te saate öelda, mis RIA DigiDoc on? +assistant: Tere! RIA DigiDoc on mobiilirakendus, mis on mõeldud ID-kaardi, Smart-ID või mobiil-ID abil dokumente digitaalselt allkirjastama. See on nagu ID-tarkvara, aga nutitelefonis!",output_ID.ee +"user: Tere! Ma sooviksin teada, kuidas ma saaksin Politsei- ja Piirivalveametilt (PPA) mõnda dokumenti saada. Milline on selleks õige viis? +assistant: Tere! Selleks tuleb pöörduda PPA poole e-posti teel või postiaadressil Pärnu mnt 139, 15060, Tallinn. Kui sa saadad faili e-posti teel, palume krüpteerida need sertifikaadi „Politsei- ja Piirivalveamet (PPA) 70008747“ abil turvalisuse tagamiseks.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin midagi tarbijavaidluste komisjoni kohta. Mis see on ja millega saab seal abi? +assistant: Tere! Tarbijavaidluste komisjon on koht, kus saab abi, kui sul on probleeme kauplejaga – näiteks kui sa ei saa oma raha tagasi ostetud toote eest või kui kaupleja ei ole täitnud oma lubadusi. Komisjon aitab vaidlusi lahendada, kui kauplejaga ise kokkulepe saavutada ei õnnestu.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma nägin, et Politsei- ja Piirivalveamet kasutab sotsiaalmeediat. Kas teil on Facebooki leht? +assistant: Jah, meil on! Meie peamine Facebooki leht on https://www.facebook.com/politseijapiirivalveamet/. Meie Põhja, Lääne ja Lõuna prefektuuridele on ka oma Facebooki lehed. Kas sa tahad teada nende lingid?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud, et on oluline kontrollida isikukaitsevahendite ohutust. Kust ma peaksin alustama nende kohta infot leidmiseks? +assistant: Tere! Oluline küsimus. Alustuseks on oluline teada, et isikukaitsevahendite ohutust reguleerib Euroopa Parlamendi ja Nõukogu määrus (EL) 2016/425. See määrus kehtib täismahus alates 21. aprillist 2018. Lisainfot saate ka Eesti Standardimis- ja Akrediteerimiskeskuse koduleheküljelt www.evs.ee, kus on erinevad standardid olenevalt tootekategooriast.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kaotanud oma isikutunnistuse ja tahan teada, kust ma saan selle kohta politseisse pöörduda Jõgeva läheduses. +assistant: Tere! Jõgeva politseijaoskond asub aadressil Suur 1, 48306 Jõgeva. Nad on avatud esmaspäevast reedeni kell 9.00-17.00. Kas saaksite mulle kinnitada, kas see on teile sobiv asukoht?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin midagi ohtlike toodete kohta. Mis see tähendab ja kes sellega tegelda oskab? +assistant: Tere! See tähendab, et mõned tooted võivad tarbijale ohtu kujutada. Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) tegelda sellega. EL-is on ka süsteem nimega Safety Gate, mille kaudu järelevalveasutused vahetavad infot ohtlike toodete kohta, et need turult kiiresti eemaldada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin midagi digiligipääsetavuse kohta Tarbijakaitse ja Tehnilise Järevalve Ametiga. Mis see täpselt tähendab? +assistant: Tere! Digiligipääsetavus tähendab seda, et avaliku sektori digiteenused ja -informatsioon peavad olema kättesaadavad ja kasutatavad kõikidele inimestele, olenemata nende võimekustest. See on Tarbijakaitse ja Tehnilise Järevalve Ameti (TTJA) tähelepanu all, et tagada kõikidele inimestele ligipääsetavad digiteenused.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma nägin Politsei- ja Piirivalveameti veebilehel mingit ligipääsetavuse teatis. Mis see tähendab? +assistant: Tere! Ligipääsetavuse teatis on avalik dokument, mis teavitab kasutajaid veebilehe ligipääsetavuse olukorra kohta. See tähendab, et seal on kirjas, kui hästi on veebileht tehtud nii, et seda saaks kasutada inimestel, kel on näiteks puuete või erinevate vajadustega.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahaksin elamisloakaarti taotleda. Kust ma selleks pöörata saaksin ja mida ma kaasa võtta tuleks? +assistant: Tere! Elamisloakaardi taotlemiseks saad pöörduda Politsei- ja Piirivalveameti teenindusse. Kindlasti võta kaasa isikut tõendav dokument. Soovi korral saad aja ka broneerida nende veebilehel: https://broneering.politsei.ee/",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mis on seikluspark ja milleks see sobilik on? +assistant: Seikluspark on koht aktiivseks ja sportlikuks puhkuseks, kus igaüks saab oma võimeid proovile panna, sõltumata varasemast kogemusest. See sobib inimestele, kes otsivad midagi põnevat ja väljakutvaid tegevusi!",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Olen kuulnud, et Venemaa kodanikele on piirangud Eestisse sisenemisel. Kas see on tõsi ja millest see tingitud on? +assistant: Tere! See on tõsi. Alates 19. septembrist 2022 on Eestisse sisenemine piiratud Venemaa Föderatsiooni kodanikele, kellel on kehtiv Schengeni lühiajaline viisa. See piirang on kehtestatud Vabariigi Valitsuse korraldusega 8. septembri 2022 nr 247 järgi.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud, et piiri tugevdamisel on kasutatud lõiketraati. Kas see ei mõju metsloomadele? On neil raskusi sellega? +assistant: Tere! Jah, see on küllaltki oluline küsimus. Paljud kohalikud ja jahimehed on murelikud. Üldjuhul on väikesed loomad piisavalt paindlikud, et lõiketraadist läbi pääseda. Kuid suuremate loomade puhul on ohtu, et nad sellega kinni jooksevad.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan kaebust esitada Tarbijakaitse ja Tehnilise Järelevalve Ametile. Kust ma alustan? +assistant: Tere! Kõigepealt peaksid pöörduma kaebusega otse kaupleja poole. Seadus kohustab kauplejat vastama sinu kaebusele 15 päeva jooksul. Alles siis, kui kaupleja ei vasta või te ei jõua kokkuleppele, saad pöörduda Tarbijavaidluste komisjoni poole.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Olen kuulnud palju 5G kohta, aga ma ei ole päris kindel, mis see tähendab. Saaksite mulle öelda, mis on 5G põhilised arengusuunad selle materjali järgi? +assistant: Tere! 5G tehnoloogia arenguteks on kolm peamist suunda: suurendatud andmesidekiirus (eMBB), tagatud ajakriitilised reaalajasüsteemid (URLLC) ja suurearvuline väikeseadmete kommunikatsioon (mMTC). Nii et põhimõtteliselt on see kiirem, usaldusväärsem ja suudab rohkem seadmeid korraga toetada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus Politsei- ja Piirivalveameti poole pöördumise kohta. Kuidas ma saan teavet taotleda? +assistant: Tere! Teavet saab taotleda mitmel viisil. Saab esitada teabenõude konkreetse dokumendi saamiseks, selgitustaotluse, kui on vaja täpsemat vastust, või märgukirja, et anda ettepanekuid PPA tööle. Kui soovite tutvuda enda andmetega, saate esitada andmesubjekti päringu.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus rõivaste ja jalatiste kohta. Mis on oluline, mida ma peaksin ostes silmas pidama märgistuse osas? +assistant: Tere! Rõivaste ja jalatiste märgistuselt peaksid kindlasti leidma toote nimetuse, suurustähistuse, materjalikoostise (v.a karusnahast ja nahast rõivastel) ning hooldustähistuse. Samuti on oluline, et materjalide kohta oleks täpne info, eriti kui tegu on naturaalse nahaga või karusnahaga.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on lapsevanematele ohutusraamatu välja andnud. Mis see raamat sisaldab? +assistant: Tere! Jah, see on tõsi. See ohutusraamat on mõeldud eelkõige 1. klassi minevate laste vanematele. See ongi Politsei- ja Piirivalveameti poolt toetav materjal, mis käsitleb lapse turvalise kasvukeskkonna loomist ja lapse ohutust erinevates elualdades. Seega, tegelikult sisaldab see informatsiooni lapse turvalisuse kohta koolitee alguses.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma sain uue mobiil-ID SIM-kaardi. Mitte aru ei saa, mis need PIN-koodid on ja mida ma neilt peaks tegema. +assistant: Tere! Tore, et sa oled uue mobiil-ID SIM-kaardi saanud. Need PIN-koodid on olulised, et sa võiksed mobiil-IDga seotud tehingud ja toimingud teha. Seal on kolm koodi: PIN1, PIN2 ja PUK. PIN1 ja PIN2 on vajalikud, et sisselogida ja tehinguid kinnitada. PUK on vajalik, kui need koodid lukku lähevad. Sa leiad need koodid SIM-kaardi ümbrise alt kraabitava pinna alt.",output_ID.ee +"user: Tere! Ma kuulsin, et kui mulle viisast keeldutakse, siis ma saan sellega midagi teha. Kas ma saan selle otsuse vaidlustada? +assistant: Jah, see on õige. Välismaalasel on õigus vaidlustada viisa andmisest keeldumise, viisa kehtetuks tunnistamise või viisa tühistamise otsust. Sa saad selle kohta vaide esitada Politsei- ja Piirivalveametile (PPA).",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas saaksite mulle öelda, mis on Riigiportaal eesti.ee? +assistant: Muidugi! Riigiportaal eesti.ee on Eesti uus digiregionsümbol. See on üks keskoht, kus saate hallata kõiki oma riigiga seotud infot ühes kohas. See on nagu ühtne keskkaabel, mis koondab palju erinevaid teenuseid ja infot.",output_ID.ee +"user: Tere! Kas sa oskad öelda, milleks mobiil-ID on? +assistant: Tere! Mobiil-ID on digitaalne identifikaator, mida saab kasutada erinevates e-teenustes, näiteks pankades, riigilikus ja muudes võrketeenustes. See võimaldab sul e-teenustes sisse logida ühtse identifikaatori abil, mitte iga kord erineva kasutajanime ja salasõna.",output_ID.ee +"user: Tere! Mis on Akadeemia Vabaühenduse peamine tegevus? +assistant: Vabandage, tekstis ei mainita Akadeemia Vabaühendust. Tekst käsitleb peamiselt mõõtevahendite direktiive ja märgistamist Euroopa Liidus.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Olen just Soomast uue telefoni ostnud. Mis siis, kui see peagi katki läheb? Kas mul on õigused siin Eestis? +assistant: Tere! Soomast ostetud telefoni puhul on hea meel kuulda, et EL ja Euroopa Majanduspiirkonnas (kuhu Soomagi kuulub) on tarbijatele kehtestatud sarnane kaitsetase. Sul on õigus esitada kaebus telefoni defekti kohta vähemalt kahe aasta jooksul alates ostukuupäevast. Oluline on alles hoida ostutšeki või maksekorralduse väljavõte tõendamaks ostu.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et Mobiil-ID on minu jaoks oluline. Kuidas saan aru, kas minu Mobiil-ID on ikkagi kehtiv? +assistant: Tere! Noh, seda saab kindlasti kontrollida. Sul on võimalik seda kontrollida oma mobiilsideoperaatori iseteeninduses. Telia mobiil-ID leiate siin: https://www.telia.ee/era/lisateenused/mobiil-id, Elisa mobiil-ID siin: https://www.elisa.ee/et/eraklient/apid-ja-lisateenused/lisateenused/mobiil-id, ja Tele2 mobiil-ID siin: https://tele2.ee/teenused/mugavusteenused/mobiil-id. Kas sa tead, milline operaator sulle Mobiil-ID andis?",output_ID.ee +"user: Tere! Ma kuulen, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegeleb sideteenuste tarbijate kaitsega. Mis täpselt see siis tähendab? +assistant: Tere! Jah, see on õige. TTJA reguleerib sideteenuste valdkonda, et kaitsta tarbijate õigusi. Näiteks elektroonilise side seadus sätestab, kuidas sideteenuse lepinguid sõlmida ja milliseid nõudeid peab lepingu tingimustele järgima. See puudutab lepingute muutmist, rikete kõrvaldamist ja piiramistgi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimusi Rail Balticu kohta. Kust ma saan teada, kes selle projekti planeerimisel kõige rohkem kaasa tegi? +assistant: Tere! Rail Balticu projekti planeerimisel juhtis laiapõhjaline juhtkomitee, kus olid esindatud Pärnu, Rapla ja Harju maavanemad ning maavalitsuste esindajad. Sinna kuulusid ka Eesti Raudtee, Keskkonnaministeerium, Maa-amet, Tarbijakaitse ja Tehnilise Järelevalve Amet ja Majandus- ja Kommunikatsiooniministeerium.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma lähen relvaloa pikendamist taotlema. Kas ma pean kindlasti selle täiendava laskekatse sooritama? +assistant: Tere! See oleneb relva otstarbetest. Kui relvaloa märgitud otstarbeks on enese ja vara kaitse, siis jah, pead täiendava laskekatse sooritama. Aga kui sinna on märgitud jahipidamine või sport, siis seda ei pea tegema.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas mul on DigiDoc4 rakenduses mingisugused piirangud digiallkirjade kohta? Ja kuidas see ajatempel asja ajab? +assistant: Tere! Jah, piirangud on olemas. Piirang puudutab isiklikuks tarbeks digiallkirjastamist, mis toimuvad läbi DigiDoc4 rakenduse ja RIA DigiDoc mobiilirakenduse. Kuus on ette nähtud tasuta digiallkirju, aga see arv on muutunud ja seda piirangut ravivad ID-kaardi, Digi-ID või Mobiil-ID abil tehtavad allkirjad. Peamaseks on see, et piirang puudutab ainult neid digiallkirju, mis tehtakse just selle rakenduse kaudu, mitte näiteks e-teenustes, nagu internetipankades.",output_ID.ee +"user: Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) teeb avalike mänguväljakute osas kontrolli. Mis on nende peamine vastutus selles küsimuses? +assistant: Tere! TTJA peamine vastutus on järelvalveteenus, mis tagab, et avalikud mänguväljakud on ohutud ja vastavad nõuetele. Omanik on vastutav mänguväljaku tehnilise korrasoleku ja ohutuse eest. See tähendab, et mänguväljakud peavad olema rajatud heast ehitustavast lähtudes ja seal paigaldatud seadmete peavad olema ohutud nende ettenähtud otstarbel.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kuulin, et Eesti piiri ehitustööd käivad. Mis on selle hanke üldine eesmärk? +assistant: Tere! Põhiliselt on eesmärk tugevdada Eesti välispiiri turvalisust. Politsei- ja Piirivalveamet (PPA) on käivitunud mitme etapina jagatud piiriehituse hankega, et tagada piiriturvalisus ka tulevikus.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma saan aru, et teiega on seotud DigiDoc konteinerid. Kas mul on mõni uus teadaolev formaat, mis on ID tarkvaras toetatud? +assistant: Tere! Jah, seda on. DigiDoc konteinerid on ID tarkvaras toetatud erinevates versioonides, aga on oluline teada, et on olemas ka BDOC formaat. See loodi selleks, et asendada Eesti-spetsiifiline DDOC formaat. Kas sulle huvitaks, et teada saada, kuidas see erineb DDOC'st?",output_ID.ee +"user: Tere! Ma saan aru, et DigiDoc4 rakendusega saab faile krüpteerida. Aga kuidas see tegelikult toimib? Ükskord ei ole mul sellega kunagi olnud kogemust. +assistant: Tere! Päevad! Kui teil on mõni fail, mida soetate turvalisemalt edastada, siis DigiDoc4 pakub võimalust neid krüpteerida. See tähendab, et faile saab muuta nii, et neid saab avada ainult need, kellel on selleks luba. Ükskord ei ole mul sellega kunagi olnud kogemust.",output_ID.ee +"user: Tere! Ma sain trahviteate ja seal on mainitud kiiruskaamera. Kas mulle automaatselt saadetakse foto, mille alusel trahv määrati? +assistant: Tere! Ei, trahviteatele ei lisata koopia kiiruskaamera pildist. Kuid, kui sa oled sõiduki eest vastutav isik ja soovid pildi koopiat, saad selle taotleda.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et ma saan aru, et digitembeldamine on midagi seotud digiallkirjadega. Kas sa võiksite selgitada, mis see täpne on ja miks seda on vaja? Kui palju tean? +assistant: Tere! Jah, seda saab öelda nii. Digitembeldamine on seotud digiallkirjade andmisega juriidilistele isikutele. See tähendab, et SK ID Solutions AS pakub e-Templi teenust, mis annab kinnituse sellele, et elektrooniliselt saadetud dokument või muu info pärineb tõepoolest selle saatnud asutusest. See on nagu digitaalne kiri, millele lisatakse kinnitus, et see on pärit sellest, kes selle on saatnud. Kas sa tead, et e-Tempel põhineb kvalifitseeritud e-Templi sertifikaatidel?",output_ID.ee +"user: Tere! Ma nägin, et Politsei- ja Piirivalveamet on Facebookis. Kas teil on palju erinevaid Facebooki lehti? +assistant: Tere! Jah, PPA täidab oma suhtluse strateegiat mitmete Facebooki lehtede kaudu. Peaasi on Põhja, Lääne, Lõuna ja Ida prefektuurid, aga ka PPA end seltsi. Lisaks on olemas PPA fännileht ja Politseikoerte Instagram! Kas sul on mõni leht, mille kohta sa tahad rohkem teada? Näiteks kas sa olid huvitatud Põhja prefektuuri lehest? https://www.facebook.com/pohjaprefektuur",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma imestan, kuidas Politsei- ja Piirivalveamet kasutab minu isikuandmeid, kui ma teieni pöördun. Saaksin selle kohta rohkem teada? +assistant: Tere! Jah, muidugi. Me kasutame teie isikuandmeid pöördumise lahendamiseks ja teile vastamiseks. Kui me peame pöördumise lahendamiseks küsimusi esitama teistele asutustele, siis edastame neile ainult nii palju andmeid, mis on vastuse koostamiseks absolutne vaja. Me püüame minimeerida avaldatava info hulka. Kas teil on selle kohta veel küsimusi?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas te oskate öelda, kus asub Rakvere politseijaoskond? +assistant: Muidugi! Rakvere politseijaoskond asub aadressil Fr. R. Kreutzwaldi 5a, 44314 Rakvere. See on suht mugav, kust bussid nr 5 ja 5A peatuvad.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen välismaalane ja plaanin Eestis ettevõtlusega alustada. Kas ma saan elamisluba, kui olen füüsilisest isikust ettevõtja (FIE)? +assistant: Tere! Jah, FIE-le ettevõtluseks elamisluba antakse. Kuid selleks on mitmed tingimused, mis peavad täidetud olema. Üks oluline tingimus on see, et olete oma ärisse Eestis vähemalt 16 000 eurot investeerinud.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet teeb koduohutusega seoses tööd. Mis see tähendab täpselt? +assistant: Tere! Jah, meil on täpselt nii. Peamiselt tähendab see, et ehitise omanik vastutab selle ohutuse eest. Korterelamutes on oluline eristus: reaalosaks on korter ise ja kaasomandiks on ühiskasutuses olevad konstruktsioonid, näiteks küttesüsteem või trepikoda. Sellega seoses on teatud asjades piirangud. Näiteks, ümberehitamine kaasomandis ei ole lubatud omavoolul.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma ei saa aru, kuidas minu ID-kaardi lugeja korralikult töötada. Kas teil on mõningaid nõuandeid? +assistant: Tere! Ma saan aru. Alustame kõige põhilisega. Enne kui su ID-kaartega mingi probleemi ilmneb, tasub veenduda, et sinu arvutisse on ID-tarkvara paigaldatud. Sa saad selle alla laadida siin: https://www.id.ee/artikkel/paigalda-id-tarkvara. Kas oled seda teinud?",output_ID.ee +"user: Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) järelvalvega gaasiseadmete suhtes. Mis see tähendab tegelikult? +assistant: Tere! See tähendab, et TTJA jälgib, et turul olevad gaasiseadmed ei ohusta inimeste tervist ja turvalisust. Oluline on, et need oleksid valmistatud ja paigaldatud õigesti, regulaarselt hooldatud ja kasutatud ettenähtud otstarbel. TTJA võib peatada ohtliku gaasiseadme levitamise ja kasutamise, kui on kahtlusi selle ohutuses.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud Mobiil-ID kohta, aga ei ole kindel, mis see on ja kuidas see töötab. Kas saaksite seda veidi selgitada? +assistant: Muidugi! Mobiil-ID on elektrooniline isikutuvastussüsteem, mis võimaldab teil sisse logida erinevate e-teenustega teie mobiiltelefoniga. See tähendab, et te ei pea pidevalt kaasas hoida ID-kaarti. Kas te teadate, et Mobiil-ID on turvaline ja seda saab kasutada seadmeüleselt – nutitelefonist, tahvelarvutist või arvutist. Kas soovite rohkem teada selle kohta?",output_ID.ee +"user: Tere! Ma nägin, et DigiDoc4 rakendus on muutunud. Kas ma saan endiselt dokumente digiallkirjastada sellega? +assistant: Tere! Jah, seda saate. Küll aga on piirangud sellel, kui palju digiallkirju te saate kuus teha. Alates DigiDoc tarkvara loomisest on teatud arv tasuta allkirju kuus. See on muutunud paari aasta jooksul suuremaks, kuid see jääb endiselt piiratud.",output_ID.ee +"user: Tere! Ma tahan elamisloakaarti taotleda. Kuidas ma riigilõivu maksta saan? +assistant: Tere! Riigilõivu tasumiseks on mitu võimalust. Saaja on Rahandusministeerium ja viitenumber on 2900082443. Pangad, kus riigilõivu maksta saad, on SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor Bank (EE701700017001577198) ja LHV Pank (EE777700771003813400).",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus Tarbijavaidluste komisjoni kohta. Kas saaksid rääkida, mis see komisjon on? +assistant: Tere! Tarbijavaidluste komisjon on Tarbijakaitse ja Tehnilise Järelevalve Ameti osa, mis lahendab tarbijate vaidlusi kauplejatega. Kui sa ei saa oma probleemiga kauplejaga kokkuleppele, saad avalduse komisjonile esitada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olin just poest mänguasi ostmas pojale, aga nüüd ma mõtlen, mida ma peaks silmas pidama, et see oleks ohutu. Mis on kõige olulisem märk, mida mänguasil peaks olema? +assistant: Tere! See on väga hea, et sa silmas pead ohutust. Kõige olulisem märk mänguasil on CE-märk. See garanteerib, et mänguasi vastab Euroopa Liidu ohutuse ja muude nõuetele. Mänguasi peaks olema sellega märgistatud nähtaval viisil.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Palun, kas saaksid anda mulle ülevaate sellest DigiDoc4 tarkvarast? Kas tead, milliseid operatsioonisüsteeme see toetab? +assistant: Tere! Muidugi. DigiDoc4 on ID-kaartide digitaalne allkirjastusprogramm. See on uus versioon vana DigiDoc3 programmi, mis on mõeldud ID-kaartidega töötamisele. Praegu toetab see Windowsi (uimastest versioonid), macOS (uimastest versioonid) ning Ubuntu'i (uimastest versioonid). Kas saaksid öelda, millist operatsioonisüsteemi sa kasutad, et ma saaksin sulle paremini nõu anda?",output_ID.ee +"user: Tere! Ma arvan, et politsei saab konverentsidel abi pakkuda? Kas on olukord, kus neil on vaja konverentsil lisaks korraldajale veel üht inimest? +assistant: Tere! Jah, seda võib juhtuda. Politsei võib suurendada konverentsil korrapidajate arvu, kui arvab, et määratud korrapidaja ei suuda korraldajat piisavalt aidata. See on oluline, kui arvab, et on võimalik kõrgendatud ohu tekkimine või juba tegu on kõrgendatud ohuga. Kas sa mõtled midagi konkreetset? Näiteks kuidas see tä hakkab toimima?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma ei ole päris kindel, kuidas ID-kaardi PIN-koodi ja PUK-koodi vahetada. Kas on sellega vaja DigiDoc4 programmi? +assistant: Tere! Jah, DigiDoc4 on selle jaoks parim vahend. Soovitame kindlasti kohe PIN- ja PUK-koodid vahetada. Need on olulised, et sa neid meelepäraselt meenutada saaksid. Kas sa oled varem DigiDoc4 programmi kasutanud?",output_ID.ee +"user: Tere! Mul on küsimus kodakondsuse kohta. Ma olen eestkostetav, ja tahaksin teada, millised on need tingimused, et Eesti kodakondsust saan taotleda? +assistant: Tere! Päev vastu! Tegelgelt, eestkostetav saab Eesti kodakondsust taotleda, kui täidab mitmeid tingimusi. Peamised need on: sa pead olema vähemalt 15-aastane, ja sul peab olema pikaajalise elaniku elamisluba või alaline elamisõigus. Kas sa tead, mida see pikaajaline elanik on?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma soovin dokumente digiallkirjastada mobiilirakendusega DigiDoc. Kuidas see kõigepeigem toimub? +assistant: Tere! Päevapeale. Digiallkirjastamine DigiDoc mobiilirakenduses on üsna lihtne. Põhiline on see, et seda saab teha ID-kaardi, mobiil-ID või Smart-ID abil. Kõigepealt tuleb avada rakendus ja sealt menüüst valida 'Allkirjasta'. Kas sa kasutad ID-kaarti, mobiil-ID või Smart-ID-d?",output_ID.ee +"user: Tere! Mis on uus DigiDoc tarkvaras? Saaksin teada, milliseid versioone te saate toetada? +assistant: Tere! Päevääre! DigiDoc tarkvaras on mitmeid uusi versioone ja uuendusi. Viimasesse versiooni (v. 19.7.0.1837) toetame macOS 10.11 ja hiljem. See versioon on uusim ja pakub parima jõudluse ja turvalisuse. Varem toetame ka versioone 18.3 kuni 18.10. Kas sa tahaksid teada, milliseid muudatusi selles versioonis on tehtud?",output_ID.ee +"user: Tere! Ma olen kuulnud, et ehitustoodetega on seoses uusi määruseid. Mis on Tarbijakaitse ja Tehnilise Järelevalve Ameti (TJJA) roll selles osas? +assistant: Tere! Jah, see on õige. TJJA vastutab ehitustoodete turujärelevalve eest Eestis. Nad veenduvad, et tootjad järgivad vastavaid nõudeid ja et ehitustooted on ohutud ja vastavad standarditele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma pole päris kindel, mida tähendab ID-kaardi sertifitseerimine. Kas saaksid sulle teada, mida see täpselt on? +assistant: Tere! Muidugi. ID-kaardi sertifitseerimine on protsess, millega ID-kaardi omanik tõestab oma identiteeti. See tähendab, et võid saada ID-kaardile juurdepääsu ja seda kasutada erinevates eesti avalikus veebis. Sertifitseerimisel kontrollitakse, et sa oled tegelikult see, kes sa väidate olevat. Hetkel on aktuaalne sertifitseerimispoliitika (https://www.id.ee/wp-content/uploads/2024/07/cp_esteid_v2.1.-.pdf), mille alustasime kasutama 16. augustil 2024. Kas sa soovid, et ma selle kohta rohkem täpsemat infot annaks?",output_ID.ee +"user: Tere! Mul on küsimus tarbijakaitse kohta. Mis ma peaksin tegema, kui ostsin toote ja see on defektne? +assistant: Tere! Kui ostsite defektse toote, on Teil seadme poolest õigus nõuda müüjalt esmalt tasuta parandamist või selle asendamist uue tooga. Müüja otsustab reeglina ise, kas ta toote parandab või asendab. Selliseid õigusi on Teil seadusega tagatud, isegi kui müüja ei pakka garantiid.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma tahan lapsele ID-kaarti taotleda. Kuidas riigilõivu maksta? +assistant: Tere! ID-kaardi taotlemisel lapsele on mitmeid võimalusi. Kui taotled iseeningukeskuses, saad riigilõivu koheselt seal tasuda. Kui maksad pangaülekandega, saadaval on erinevad panga kontod. Kas soovid, et ma need kontod loetle?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma lugesin, et Politsei- ja Piirivalveamet avaldab liiklusinfo kohta andmeid. Mis seal täpselt näha on? +assistant: Tere! Politsei- ja Piirivalveameti lehel on ülevaade liiklusõnnetustest, mille puhul on inimesed kannatanud. See tähendab, et seal näidatakse liiklusõnnetuste arvu, kus keegi on saanud vigastusi või vaja on olnud meditsiiniabi.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin Rail Balticu kohta palju juttu. Kuidas Tarbijakaitse ja Tehnilise Järelevalve Amet selle projektiga seotud on? +assistant: Tere! Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) menetleb Rail Balticu projekti puhul ehitus- ja kasutuslubasid, samuti projekteerimistingimusi. Kui ehitisel baseerub riigi eriplaneering, siis on TTJA ehitusloa andjaks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) töötleb isikuandmeid. Kuidas see täpselt käib? +assistant: Tere! Jah, see on õige. TTJA töötleb isikuandmeid, näiteks kui te meile kirjutate või olete mõne menetluse osapoolena. Me töötleme neid erinevates infosüsteemides, sealhulgas ka üleeuroopalistes. Kõigis süsteemides on tagatud isikuandmete kaitse.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma elan välismaal ja tahan teada, kuidas ma saan Eesti passi taotleda. Mis on kõige lemmikum viis seda teha? +assistant: Tere! Välismaal saab Eesti passi taotleda kas iseteeninduses (https://etaotlus.politsei.ee/#/login) või Eesti Vabariigi välisesinduses. Kas sa tead, millises välisesinduses sa soovid dokumendi taotleda?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et Tarbijavaidluste komisjon KKK aitab probleemidega kauplustega. Kas komisjonis kaebuse esitamine on tasuta? +assistant: Tere! Jah, küllap. Kaebuse esitamine Tarbijavaidluste komisjoni menetlusse võtmine on täiesti tasuta. Samuti on avalduse läbivaatamine tasuta.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma nägin ühte reklaami ja mõtlesin, kas Tarbijakaitse Amet kontrollib reklaamide nõuetekohasus? +assistant: Jah, küllap! Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib, et reklaam ei rikkuks seadust. Põhilist nõuet on see, et reklaam peab tavaliselt tähelepanu korral olema selgelt eristatav muust teabest – see peab kohe näha, et tegemist on reklaamiga.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen huvitatud sellest, mida ma pean tegema, kui mul on probleeme elektriarvete või jäätmeveoga. Kus ma saan abi? +assistant: Tere! Kui Sul on probleeme elektriarvete või jäätmeveoga, on mitmeid kohti, kust Sa abi leiad. Elektriarvete osas saad pöörduda TTJA tarbijavaidluste komisjoni poole. Jäätmeveo puhul saad esmalt pöörduda kohaliku omavalitsuse poole, kui probleem ei lahene, saad pöörduda TTJAsse. Mis täpsemalt Sind muretseb – elektriarved või jäätmeveo probleem?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus Politsei- ja Piirivalveameti lipudest. Alustame lihtsast: mis on Politseiameti lipu põhivärv? +assistant: Tere! Politseiameti lipu põhivärv on sinine. See on üks meie riigilipu värve ja sümboliseerib rahu ja stabiilsust.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulen, et on olemas Tarbijavaidluste komisjon. Mis see komisjon täpselt on ja kuidas see tarbijaile aitab? +assistant: Tere! Tarbijavaidluste komisjon on selline koht, kus saab lahendada vaidlusi, mis tekivad tarbija ja kaupleja vahel lepingu tõttu. See on palju lihtsam ja kiirem kui kohtuvaidlus, ja see on nii tarbija kui ka kaupleja jaoks tasuta. Üldjuhul lahendatakse probleem 90 päeva jooksul.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus Tarbijakaitse ja Tehnilise Järelevalve Ameti komisjoni töös. Mis juhtub, kui mul on probleem kauplejaga ja pöördun nende poole? +assistant: Tere! Üldjuhul annab Tarbijakaitse Ameti komisjon kauplejale aega, et ta saaks oma seisukoha kujundada ja vastata sinu väidetele. Neil on võimalik vaadata läbi kõik olulised asjaolud, et saaksite koos lahendust leida.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin, et TTJA järelvalve meediateenuste suhtes on. Mis see tähendab? +assistant: Tere! Jah, TTJA järelkab meediateenuste puhul seda, kas teenuseid osutavad ettevõtted on Eestist saanud tegevusloa. Saate nende nimekirja siit: /ariklient/ametist/load-ja-riigiloivud/meediateenuste-tegevusluba. Mis teid selles osas kõige rohkem huvitab?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma saan aru, et ID-kaardiga seotud sertifikaadid on olulised. Aga kuidas see töötab tegelikult? Miks on need nii olulised? Kas ma saan sellega midagi teha, ilma et ma PIN-koodi tean? +assistant: Tere! Päevas! Nii jah, sertifikaadid on nagu sinu digitaalne võti e-teenustesse. Need seovad sind ID-kaardiga (või teiega liidetud muude digitaalsete dokumentidega) ja lubavad sinule e-teenustes sisse logida ja allkirju anda. Kuigi alati vajavad need PIN-koodi (ja PUK-koodi), on see nii selleks, et vältida ebaseaduslikku kasutamist. Mida sa pead silmas, et ilma PIN-koodita ei saa e-teenustega midagi teha.",output_ID.ee +"user: Tere! Ma olen kuulnud, et Tarbijakaitseamet järelvalvib gaasiseadmete turvalisust. Kuidas see täpsemalt toimib? +assistant: Tere! Jah, TTJA järelvalve eesmärk on tagada, et turul olevad gaasiseadmed ei ohusta inimeste tervist ja turvalisust. See tähendab, et seadmed peavad olema valmistatud ja paigaldatud õigesti, regulaarselt hooldatud ning kasutama ettenähtud otstarbel. TTJA võib peatada ohtliku seadme levitamise ja kasutamise ning vajadusel nõuda selle turult kõrvaldamist, kui ekspertiis näitab, et seade ei vasta nõuetele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma saan aru, et id.ee veebileht kogub külastajate kohta andmeid. Kas on võimalik teada, millist teavet täpselt kogutakse, kui ma veebilehele saja. +assistant: Tere! Jah, seda on võimalik. Kui külastate id.ee veebilehte, kogume teie arvuti või arvutivõrgu internetiaadressi (IP-aadressi), lehekülje, mida külastasite, teie arvuti veebilehitseja ja operatsioonisüsteemi ning külastamise aja. Me ei seota IP-aadressi isikut tuvastava teabega, vaid kogume selle kohta, millist veebilehe osa külastate ja kui kaua seal viibite. See on täiestelt külastusstatistika jaoks.",output_ID.ee +"user: Tere! Ma arvan, et saan nüüd oma ID-kaardi kasutada internetis, aga ma ei ole kindel, kuidas kontrollida, et minu sertifikaadid on kehtivad. Kas on midagi oluline, mida pean teadmas? +assistant: Tere! Päevas! Jah, oma ID-kaardi sertifikaadid on olulised, et seda kasutada. Sa saad sertifikaatide kehtivuse kontrollida DigiDoc4 rakenduses. See on lihtne: avad DigiDoc4, vali menüüst „Minu eID”, ja siis kontrollid sertifikaadi staatust. Palju lihtsam, kui arvatakse!",output_ID.ee +"user: Tere! Kas te oskate öelda, kus Kuressaare politseijaoskonn asub? +assistant: Muidugi! Kuressaare politseijaoskonn asub Transvaali 58, Kuressaare. See on väga mugav koht, sest sinna on lihtne jõuda bussiga. Kas te soovite teada, millise peatuses on see?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas te saate rääkida sellest RIA DigiDoc mobiilirakendusest, mida ID.ee teeb? +assistant: Muidugi! RIA DigiDoc on mobiilirakendus, mis on mõeldud ID-kaardi, Smart-ID ja mobiil-ID abil dokumentide digitaalse allkirjastamisele. See on tasuta saadaval App Store'ist ja Google Play poest. Kas te olete huvitatud teadma, mida sellega saab teha?",output_ID.ee +"user: Tere! Ma kuulen palju telefoni- ja koduuksemüüki kohta. Mis on kõige tähtsam, mida ma peaks teadma, kui mulle heljutakse või kui kellegi koduuksel kohtan? +assistant: Tere! See on hea küsimus. Oluline on teada, et telefoni- ja koduuksemüük on eriti tähelepanuta nõudmise puhul erinev tavalisest poes ostmisest. Peamine on see, et telefoni teel sõlmitud leping on siduv ainult siis, kui kaupleja on edastanud lepingu tingimused kirjalikult ning Sa oled kinnitanud oma tahet kirjalikus vormis. Koduuksemüügi puhul peab kaupleja andma Sulle taganemise juhendi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus elamisloa kohta. Mis juhtub, kui mu elamisluba, mis on mul täisealise lapsega siia pere juurde elama asumise põhjal, lõppeb? +assistant: Tere! Kui sinu elamisluba lõppeb, saad Eestisse jääda veel kuni 270 päeva. Sel ajal on sul ka võimalus Eestis töötada. See on tähtis teada, et saaksid asjad korralikult planeerida.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma planeerin oma maja lähedusse mänguväljaku ehitada, ja see jääb lähedale raudtee rööbastele. Kas ma pean selle kohta kellegi nõusolekut küsima? +assistant: Tere! Jah, te pole päris kindel, aga väga tõenäoline, et peate. See võiks olla seotud raudtee kaitsevööndi reeglitega. Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) on selle valdkonna eest vastutav. Mänguväljak, eriti kui see on piireteta ja seal võib olla palju lapsi, võib tekitada olukordi, kus lapsed ei pane rongile tähelepanu. Seega on oluline kindlaks teha, kas mänguväljak jääb kaitsevööndisse ja siis TTJAlt nõusolekut küsida.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin, et piiripunktides ei ole alati politseinikke. Kas see on tõsi? +assistant: Jah, see on õige. Politseiametnikud ei viibi piiripunktides alaliselt. Piirikontrolli teostamine toimub väljakutsel.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et kasutasin RIA DigiDoc'i mobiilirakendust. Kas teil on teada, kas see on hästi ligipääsetav? Või on mõnes peast kohas probleemid? +assistant: Tere! Jah, RIA DigiDoc mobiilirakendus on mõeldud olema ligipääsetav. Kuid seegi, et see on täielikult hea, on keeruline. Teatetes on kirjas, et osa funktsioonidest ei vasta täielikult WCAG 2.1 AA suunistele. Palju on parameetreid, millele on vaja tähelepanu pöörata. Näiteks on osa väljade kontrastsus väga väike. Kas te seda rakendust just kasutasite ja tekkis teil midagi?",output_ID.ee +"user: Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib meediateenuseid. Mis täpselt see tähendab? Kas nad jälgivad ka välismaa telekanaleid? +assistant: Tere! TTJA jälgib meediateenuseid, kes on saanud tegevusloa Eestist. See tähendab, et nad kontrollivad nende tegutsemist, et see oleks kooskõlas Eesti seadusandlusega. Välismaa telekanaleid nad ei jälgi, kui need pole Eestist loa saanud. Saate nimekirja väljastatud tegevuslubadest siit: /ariklient/ametist/load-ja-riigiloivud/meediateenuste-tegevusluba.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma saan aru, et digiallkirjastamine on tähtis. Aga mis see .bdoc ja .asice vahetus täpselt tähendab? Ma näen ka mõlemat laiendit. +assistant: Tere! Väga hea küsimus. Peen, et täheldasid neid erinevaid laiendeid. Peen, et digiallkirjad on Eestis väga üldlevinud. .bdoc ja .asice on kaks erinevat allkirjaformaati, mida kasutatakse digiallkirjastatud dokumentides. Aluperes kasutati Eestis .bdoc-formaati, aga nüüd on eelistatud .asice.",output_ID.ee +"user: Tere! Ma olen välismaalane ja plaanin Eestis füüsilisest isikust ettevõtjana (FIE) alustada. Kas sa saaksid mulle rääkida, millised on elamisloa tingimused selleks? +assistant: Tere! Jah, muidugi. Elamisluba ettevõtluseks FIE-le on võimalik saada, aga on ka mitmeid tingimusi. Kõigepealt pead Eestisse investeerinud vähemalt 16 000 eurot oma ärisse. Veel pead olema Äriregistris registreeritud FIE-na.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et mul on probleeme oma ID-kaardi PIN ja PUK koodidega. Kas saaksite mind aidata? +assistant: Tere! Muidugi, ma saan teid aidata. Kui te probleeme PIN ja PUK koodidega näete, on oluline need kohe vahetada. Soovitame seda teineteise vastu vahetada kohe - näiteks, kui valitud koodid on meeles, saate need muuta DigiDoc4 rakenduses. Kas te olete juba proovinud PIN koodi muuta DigiDoc4 kaudu?",output_ID.ee +"user: Tere! Ma arvan, et ma pean ID-kaardi kasutama oma arvutis. Mis ma peaksin tegema? +assistant: Tere! Hea küsimus. Kui soovite ID-kaardi oma arvutis kasutada, siis kõigepealt pead ID-tarkvara paigaldama. See on väga oluline, et ID-kaart arvutis töötaks. Kas teil on praegu mingi operatsioonisüsteem arvutis? Näiteks Windows, macOS või Linux?",output_ID.ee +"user: Tere! Ma ei ole päris kindel, mis Mobiil-ID on. Kas see on midagi sarnast ID-kaardile? +assistant: Tere! Jah, Mobiil-ID on täpselt nii nagu ID-kaart, aga see on digitaalne ja see on seotud sinu nutitelefoniga. See on SIM-kaardi põhjal toimiv digitaalne isikutunnistus. Politsei- ja Piirivalveamet väljastab selle sertifikaadid. Kas sa mõtled, mis see täpselt teeb?",output_ID.ee +"user: Tere! Ma ei ole päris kindel, kuidas mobiil-ID töötab. Kas ma pean mingi eraldi seadme hankima, et seda kasutada? +assistant: Tere! Ei pea. Mobiil-ID töötab nii arvutis, tahvelarvutis kui ka mobiiltelefonis. Pea kõik suuremad e-teenused, mis pakuvad ID-kaardiga sisenemise võimalust, toetavad seda. Kui sa tead oma mobiil-ID PIN-koode, siis saad neid kasutada!",output_ID.ee +"user: Tere! Ma olen kuulnud, et Tarbijakaitseamet järelvalve teenuste ohutuse suhtes. Mis see tähendab tegelikult? Mis on minu õigused teenusena kasutades? +assistant: Tere! See tähendab, et teenuseid peab osutatama nii, et need vastaksid kehtestatud nõuetele. Kui erinõudeid ei ole, siis ei tohi teenuse sihipärane kasutamine ohustada sinu elu, tervist või vara. Sul on õigus saada infot teenuse kohta – nii suuliselt kui kirjalikult. Näiteks kasutusjuhendid, lepingud jne. Peamine on, et sa aru saaksid, kuidas teenust kasutada ja millised riskid sellega võivad kaasneda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen huvitatud Politsei- ja Piirivalveameti riigihanketest. Kust ma nende kohta infot leian? +assistant: Tere! Kõik Politsei- ja Piirivalveameti hanketeated ja riigihangete aruanded on kättesaadavad riigihangete registri veebilehelt: https://riigihanked.riik.ee/. Seal leiad infot erinevate hankete kohta.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud, et Eestis on tegemisel muutusi tele- ja raadioringhäälingu osas. Mis on kõige olulisem asi, mida tarbijana tulema peab? +assistant: Tere! Üks olulisem muutus on üleminek täielikult kõrgresolutsiooniga (HD) pildikvaliteedile DVB-T2 standardil. See tähendab, et kui teil on vana teler, mis ei toeta DVB-T2-d, peate ehk hankima uue või lisades digiboksi, et HD kvaliteediga programme näha.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma lugesin, et politsei teab avalike koosolekute kohta palju. Mis siis täpselt avalik koosolek on? +assistant: Tere! Hea, et küsisid. Avalik koosolek on inimeste kogunemine avalikus kohas ühise eesmärgiga. See võib olla näiteks väljaku, pargi, tee või tänava peal toimuva koosoleku, demonstratsiooni, miitingu, piketi, religioosse ürituse või rongkäigu vormis.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud, et raadio ja tele ringhäälingu jaoks on vaja luba. Kust ma alustan? +assistant: Tere! Jah, see on õige. Raadioringhäälingu jaoks pead tavaliselt kõigepealt taotlema raadioteenuse tegevusluba. Saad selle kohta rohkem infot selle lingi kaudu: https://ttja.ee/ariklient/ametist/load-ja-riigiloivud/meediateenuste-tegevusluba",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kas te muldite, kuidas avalikku üritust korraldada Eestis? Aga täpselt Politsei- ja Piirivalveameti poolt. +assistant: Tere! Muidugi, ma saan teid selles aidata. Avaliku ürituse korraldamiseks peate esmalt konsulteerima kohaliku omavalitsusega, kelle piires soovite ürituse korraldada. Nad annavad luba ja hindavad, et üritus ei põhusta probleeme. Nende koduleht on parim koht, et leida nende kontaktid ja taotlusvormi. Kas soovite teada, mis need nõuded kohalikel omavalitsustel on?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma saan aru, et ma saan ID-kaardi abil eesti e-teenuseid kasutada. Aga kust ma saan alustada? Mis on kõige olulisem? +assistant: Tere! Hea küsimus! Alustamine ID-kaardi abil eesti e-teenuste kasutamisega on suhteliselt lihtne. Kõige olulisem asi on sellega, et pead paigaldama ID-kaartide tarkvara oma arvutile. Seda saab alla laadida ja paigaldata ID-kaardi ametlikult veebilehe kaudu: [https://www.id.ee/artikkel/paigalda-id-tarkvara](https://www.id.ee/artikkel/paigalda-id-tarkvara). Kas sa kasutad Windowsi, macOSi, Linuxi või Midigate? See aitab mul soovitada kõige sobivama versiooni.",output_ID.ee +"user: Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on saanud raha piiriehituse jaoks. Kas see on tõsi? +assistant: Jah, see on tõsi! Politsei- ja Piirivalveamet on saanud toetust piiriehituse rahastamiseks Euroopa Liidu Sisejulgeolekufondi välispiiride ja viisade rahastamisvahendist perioodil 2014-2020.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et Tarbijakaitse Amet teeb elektriohutusega seotud asjadel palju tööd. Mis see tähendab tegelikult, kui ma olen tavaline inimene ja elan oma majas? +assistant: Tere! Jah, me teeme. Põhiliselt tähendab see, et me tahame tagada, et teie maja elektripaigaldised on ohutud ja vastavad nõuetele. See tähendab, et kui te kasutate elektriseadmeid, siis peaks see olema tehtud nii, et see ei ohusta teie elu, tervist, vara ega keskkonda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et Tarbijakaitseamet jälgib isikukaitsevahendite ohutust. Kuidas see täpselt toimib? +assistant: Tere! See on õige. Meie amet jälgib isikukaitsevahendite ohutust peamiselt Euroopa Parlamendi ja Nõukogu määruse (EL) 2016/425 alusel. See määrus reguleerib isikukaitsevahendite nõuetele vastavust ja ohutust kogu ELis.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mis on selle dokumendi peamine eesmärk? +assistant: Dokumendi peamine eesmärk on kirjeldada mõõtevahendite direktiive ja märgistamist, mis vastavad Euroopa Liidu õigusaktidele. See hõlmab mitmeid direktiive, mis reguleerivad erinevate mõõtevahendite, nagu elektriarvestid, pikkusmõõdud ja rehvimanomeetrid, standardeid ja kontrolli.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et ma saan nüüd Mobiil-ID'd, aga pole päris kindel. Mis see üldse on ja kuidas seda kasutada? +assistant: Tere! Mobiil-ID on digitaalne identifikaator, mis võimaldab tegeleda mitmete võrguteenustega elektrooniliselt, näiteks sularahata maksed või e-krediit. See on nagu digitaalne isikutunnistus. Seda kasutamiseks vajad Mobiil-ID sertifikaadi, mis on kehtiv 5 aastat. Seda saab kontrollida oma mobiilsideoperaatori iseteeninduses.",output_ID.ee +"user: Tere! Olen kuulnud, et e-residensi saamine on Venemaa ja Valgevene kodanikele muutunud keerulisemaks. Kas see on tõsi? +assistant: Jah, see on tõsi. Alates 11. märtsist 2022 on esmakordselt Venemaa ja Valgevene kodanikele e-residensi taotlusi enam rahuldatud ei saa. See on seotud Ukraina sõjaga ja on osa rahvusvahelistest sanktsioonidest.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus laenude kohta. Ma nägin, et TTJA leheküljel on kirjutatud vastutustundliku laenamise põhimõtted. Mis see tähendab ja kuidas see mind puudutab? +assistant: Tere! Vastutustundliku laenamise põhimõtted tähendavad, et nii laenuandjad kui ka tarbijad peavad teatud reeglitele järgima. Laenuandjad peavad hindama sinu maksevõimet ja pakutama sulle sobivat lahendust, samas pead sina olema aus oma väljaminekute kohta. Põhimõte on, et laen ei tohiks olla sulle üle jõu.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus. Kui saan teada, et minu ID-kaart on kadunud, mida peaks tegema? +assistant: Tere! See on murettekitav. Palun mõelge kõigepealt läbi, kas olete seda äsja kasutanud ja kas olete seda tavapärasest veidi erinevasse kohta jätnud, näiteks kontorilauale. Kui te kindel olete, et see on varguses, siis paneme kohe tegutama.",output_ID.ee +"user: Tere! Ma olen ajakirjanik ja ma huvitatav politsei pressilisti liikmeks saamiseks. Kuidas ma saan liituda? +assistant: Tere! Politsei- ja Piirivalveamet jagab pressilistidega pressiteateid, eelmisel ööpäeval politseis registreeritud sündmuste kokkuvõtteid ja inimkannatanutega liiklusõnnetuste ülevaateid. Liitumissoovi saad saata aadressile, mis on leitav meie veebilehel. Saada kiri, kus märkige palun oma nimi, e-posti aadress ja väljaande nimi.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et mul on küsimus elamisõiguse kohta. Kuidas see üldse menetletakse Politsei- ja Piirivalveametis? +assistant: Tere! Jah, küsimus on väga mõistlik. Menetlemine algab sellega, et Politsei- ja Piirivalveamet kontrollib, et kõik taotluses esitatud dokumendid on nõuetekohased. Peab olema näiteks kehtiv ID kaart. Seejärel kontrollitakse, et sa soodsad tingimused elamisõiguse saamiseks täidetakse. Kui on midagi puuduvat, võtame sind teada.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahaks teada, kas Kuressaare arestimajasse saab saata pakke sugulastele või sõpradele, kes seal viibivad? +assistant: Tere! Kuressaare arestimajasse ei tohi pakke üldse tuua. Arestialustele neid lubatud ei ole.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma saan aru, et ma pean DigiDoc4-i oma arvutis leidma, aga ma ei suuda seda leida. Kas teil on mõni nõuadav? +assistant: Tere! Jah, küllap! Palju õnne, et kontrollisid. Üldiselt paigaldatakse DigiDoc4-i Windowsi arvutitesse automaatselt arvuti töölehele. Pärast paigaldust peaks töölehel olema DigiDoc4 ikoon. Kuidas see tööleht välja näeb? Kas sa näed seal midagi, kuhu sa võiksid hiirega topeltklõpsata?",output_ID.ee +"user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) töötab isikuandmetega. Kuidas see üldiselt käib? +assistant: Tere! See on õige. TTJA töötleb isikuandmeid erinevates infosüsteemides, näiteks kui saame sinult kirja või oled mõne menetluse osaline. Need võivad olla ka tundlikud andmed. Kõik töötlemine toimub kooskõlas isikuandmete kaitse seadusega.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kas saaksite öelda, mida Riigiportaalis eesti.ee saab teha? +assistant: Muidugi! Riigiportaalis eesti.ee on väga palju erinevaid asju. See on keskoht, kus saab hallata oma riigiga seotud infot. Näiteks saab kontrollida digiretsepte, haridusinformaat, vaadata oma elukohaandmeid ja palju muud. Põhimõtteliselt on see keskoht, kust saab juurde kõikvõimalikku infot, mis sinu elukaasale kuuluvaid asju puudutab. Kas saaksite öelda, millest te konkreetselt huvitatud olete?",output_ID.ee +"user: Tere! Ma püüan mobiil-IDga dokumenti allkirjastada, aga saan veateate. Mis ma teha saan? +assistant: Tere! Palun, andke teada, mis veateadet just saate? See aitab meil probleemi paremini mõistata. Kui teate veateate numbrit või tekstisummarit, siis palun jagage seda koos teiega tehtud toiminguga.",output_ID.ee +"user: Tere! Ma kuulsin, et piiripunktis ei ole alati politseinikke. Kas see on tõsi? +assistant: Jah, see on õige. Politsei ametnikud ei viibi piiripunktis alaliselt. Nad teostavad piirikontrolli väljakutsel.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et on oluline teada, kuhu pöörduda, kui satud kelmuse ohvriks. Mis on esimene asi, mida ma peaksin tegema? +assistant: Tere! See on väga hea küsimus. Esiteks, kui sa avastad, et oled pangamakse teinud kelmidele, siis võta kohe ja alati ühendust oma pangaga. Swedbanki lehel on selle kohta täiendav info: https://www.swedbank.ee/private/d2d/ebanking/secureBanking , samuti SEB Pangal: https://www.seb.ee/foorum/kuberkuritegevus-kuidas-end-kaitsta , LHV-l: https://www.lhv.ee/et/turvalisus ja Luminoril: https://luminor.ee/petuskeemid?static=true",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kaotasin oma isikukaarti. Kust ma saan Jõhvi politseijaoskonna aadressi? +assistant: Tere! Jõhvi politseijaoskond asub aadressil Rahu 38, 41588 Jõhvi.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma elan välismaal ja tahan teada, kuidas saan elamisloakaarti taotleda. On see võimalik? +assistant: Tere! Jah, välismaal saab elamisloakaarti taotleda, aga erandkorras. See on võimalik, kui sa ei ole Euroopa Liidu liikmesriigi kodanik, sul on kehtiv Eesti elamisluba või elamisõigus ning sinu välismaa elukoht on registreeritud Eesti rahvastikuregistris. Kas sa vastad neile tingimustele?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas ma saan uue ID-kaardi tellida? +assistant: Tere! Jah, seda on võimalik. Uue ID-kaardi saab taotleda Politsei- ja Piirivalveametist. See on kõikidele Eesti kodanikele kohustuslik isikutõendav dokument. Kui saad uue ID-kaardi, saad ka koos sellega koodiümbriku, millesse on trükieritud turvakoode - PIN ja PUK koodid. Küll aga, see on oluline, et teadaksid, et uue ID-kaardi saab tellida ka kauplustest alates jaanuari 2023. Kas sa oled pärituusama ID-kaardi tellimisel kindlamalt huvitatud?",output_ID.ee +"user: Tere! Mul on küsimusi hoonete energiatõhususe kohta. Mis on Tarbijakaitse ja Tehnilise Järevalve Ameti (TTJA) roll selles küsimuses? +assistant: Tere! TTJA järelvalvab hoonete energiatõhususe miinimumnõuete täitmist. See tähendab, et nad tagavad, et uued ja rekonstrueeritud hooned vastavad kehtivatele standarditele. Nad määravad ka suurettevõtetele energiaaudiidikohustuse täitmist.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma tahan teada, kuidas toimib Eesti kodakondsuse taotlus täiskasvanule. Mis juhtub siis, kui ma olen kodakondsuse taotluse esitanud? +assistant: Tere! Pärast kodakondsuse taotluse esitamist edastatakse kõik dokumendid kuue kuu jooksul Vabariigi Valitsusele. Nad teevad otsust, kas sulle kodakondsus antakse.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen üsna uus Tarbijakaitse ja Tehnilise Järelevalve Ameti tööle ja ma kuulen palju raadiosageduste kasutamisest. Mis see tähendab ja miks on see ametile oluline? +assistant: Tere! See on hea küsimus. Raadiosageduste kasutamine tähendab lihtsalt seda, kuidas erinevad seadmed – näiteks raadiod, mobiiltelefonid ja WiFi-ruuterid – kasutavad õhuvõrku signaali edastamiseks. Tarbijakaitse ja Tehnilise Järelevalve Amet (TJJA) on sellega seotud, et tagada, et need seadmed ei häiri teineteist ja et kasutatakse sagedusi vastavalt kehtivatele reeglitele ja planeerimisele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et ma saan aru, et me räägime ID-kaardi sertifikaatidest. Aga kuidas need sertifikaadid üldse töötavad? Miks on need vajalikud? +assistant: Tere! Jah, täiesti! Sertifikaadid on olulised ID-kaardi, elamisloakaardi, digi-ID ja diplomaatilise isikutunnistuse kasutamisel e-teenustes. Need on nagu digitaalne võti, mis võimaldab teil kindlustada oma identiteeti veebis. Need kinnitavad, et te olete selle dokumendi omanik.",output_ID.ee +"user: Mis on tarbijavaidluste komisjoni ülesanne? +assistant: Tarbijavaidluste komisjoni ülesanne on lahendada vaidlusi tarbijate ja ettevõtjate vahel, mis on tekkinud tarbimise seadusest või muudest seadusest tulenevate kohustuste rikkumisest. See on viis tarbijale vaidluse lahendamiseks kohtuvälise menetluse kaudu.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et pürotehnika kasutamisega on teatud reeglid. Kas sa saaksid mulle rääkida millestki olulisest? +assistant: Tere! Jah, kindlasti. Oluline on teada, et pürotehniliste toodete kasutamisel tuleb arvestada avalikku korda. Näiteks, F4- ja T2-kategooria pürotehnikat tohib kasutada ainult pürotehnik ise. Kas sa teadsid seda?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulen, et Tarbijakaitseametis on mingi Tarbijavaidluste komisjon. Mis see komisjon teeb? +assistant: Tere! Jah, Tarbijavaidluste komisjon on Tarbijakaitse ja Tehnilise Järelevalve Ameti juures. See komisjon vahendab Sinu vaidlusi kauplejaga, kui Te ei ole saanud sellega ise lahendust leida.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma tahan teada, mis see raudtee kaitsevöönd täpselt on ja miks see üldse on oluline? +assistant: Tere! Raudtee kaitsevöönd on tsoon raudtee läheduses, mis on mõeldud tagama, et raudtee saab toimida häireteta ja et raudteelt lähtuvad ohtud, näiteks müra või vibratsioon, ei kahjustaks elanikke. See on ka see, et tagada liiklussaated on nähtavad ja et midagi ei takista raudteeohutust.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma ei ole päris kindel, kuidas riiklikes e-teenustes autentimine töötab. Kas ma pean iga kord, kui ma mõnda e-teenust kasutan, ise uuesti sisse logima? +assistant: Tere! See on väga hea küsimus. Põhjaosalt jah, igas e-teenuses pead peaaegu alati uuesti sisse logima. Küll aga on olemas võimalus, et kui sa oled juba ühe e-teenuse kaudu sisse logitud, siis saad sama sisselogimiskättelisuse ka teistes e-teenustes kasutada. Seda ei pea iga kord uuesti tegema.",output_ID.ee +"user: Tere! Ma kuulsin midagi piirkondliku politseitöö kohta. Mis see tähendab? +assistant: Tere! Piirkondlik politseitöö on politseitöö vorm, mis keskendub probleemide lahendamisele konkreetses piirkonnas ja tugineb kogukonnakesksetele põhimõtetele. See tähendab, et politsei püüab lähemalt tulla kogukonnale ja arvestada selle vajadusi.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et Digidoc-failid on minu tööle tegelikult väga olulised. Aga ma ei ole päris kindel kõigest, mis seal sees on. Kas saate mulle põgusalt öelda, milliseid eri formaate seal üldse on? +assistant: Tere! Päev! Muidugi, ma saan. Digidoc-failides on mitut erinevat formaati. Üsna paljudes ID-tarkvara versioonides toetatakse BDOC, CDOC ja ASICE formaate. Lisaks on olemas veel ENCDOC-XML, mis on krüpteeritud DigiDoc formaat. Kas te raatsite täpsemalt mõelda, millise formaadi kohta te soovite rohkem teada?",output_ID.ee +"user: Tere! Ma olen huvitatud lapsele Eesti kodakondsuse taotlemisest. Kuidas see menetlus üldiselt toimib? +assistant: Tere! Põhiliselt on nii, et kui te esitate taotluse, siis Politsei- ja Piirivalveamet edastab dokumendid kolme kuu jooksul Vabariigi Valitsusele otsustamiseks. See on selle taotluse esialgne menetlus.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kahtlustan, et minu autoga on midagi viga. Kas ma saan selle kohta infot Lääne-Harju Kesklinna politseijaoskonna käest? +assistant: Tere! Politseijaoskond ise teab tegelikult vähe sellest. Autovariante saaksite küsida näiteks liiklusregistrist. Lääne-Harju Kesklinna politseijaoskond asub Kolde pst 65, Tallinn, ja on avatud esmaspäevast reedeni kell 9.00-17.00.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus reklaamide kohta. Kas Tarbijakaitse Ameti poolt on reklaamidele mingid reeglid kehtestatud? +assistant: Jah, on küll! Reklaamidele on kehtestatud mitmeid reegleid. Näiteks, reklaam peab tavalise tähelepanu korral selgelt eristuma muust teabest – see peaks kohe aru saada, et tegemist on reklaamiga. Samuti peab selgeks saama, kes reklaami tellija on – näiteks firma nimi või domeeninimi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma tahan lapsele ID-kaarti taotleda. Kuidas ma selle riigilõivu kõige paremini maksta saan? +assistant: Tere! ID-kaardi taotlemisel iseteeninduses saad riigilõivu koheselt tasuda, pole vaja eraldi pangaülekannet. Kui aga soovid pangast maksta, siis saaja on Rahandusministeerium, viitenumber on 2900082443. Selgitusse märgi „ID-kaardi taotlemine“ ja lapse nimi, isikukood või sünniaeg. Kas soovid, et ma sulle pangakontode numbreid ütleksin?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et DigiDoc4 on uus programm, millega saab failid turvaliseks teha. Kas saaksite palun rääkida, kuidas see täpselt toimib? +assistant: Tere! Päev vastu ja jah, DigiDoc4 on just selline programm. See aitab krüpteerida dokumente, et neid ei pääseta võõraste käed. Põhiseadme on see, et need failid saavad avada ainult need, kellele selleks luba antud on. Kas teadaksite, kust alustada?",output_ID.ee +"user: Tere! Ma saan probleeme ID-kaardi kasutamisega veebis. Mida ma peaksin tegema? +assistant: Tere! Ma saan aru, et sul on raskusi ID-kaardi kasutamisega veebis. Palju õnne, see on levinud probleem. Lühidalt, kõigepealt kontrollige, et veebilehitsejas on lubatud ID-kaardi sertifikaatide kasutamine. Kas sul on mõeldud Chrome, Edge, Firefox või mõni teine brauser?",output_ID.ee +"user: Tere! Ma tahan küsida Kuressaare arestimajast. Kas sinna saab pakke toimetada? +assistant: Tere! Kuressaare arestimajja ei tohi arestalustele pakke tuua. Vahistatule on aga lubatud üks pakk kalendrikuus.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud, et on olemas ebaausad kauplemisvõtted. Mis need täpselt on ja miks need on keelatud? +assistant: Tere! Ebaausad kauplemisvõtted on need, mis sind tarbijana eksitavad või on su suhtes agressiivsed. Need on Euroopa Liidus keelustatud. Eksitav on, kui müüja annab sulle valeinfot kauba või teenuse kohta, või esitab õiget infot viisil, mis võib sind eksitada. Agressiivne on, kui müüja piirab sinu valikuvabadust, ahistades või mõjutades sind liigselt ostma.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et ma saan aru, et DigiDoc on seotud dokumentide digitaalse allkirjusega. Kuidas see aga konkreetselt PDF-idega või teiste failidega hakkab? +assistant: Tere! Jah, õigesti. DigiDoc on oluline osa ID-tarkvara, mis käsitleb digitaalset allkirjastamist. See on seotud dokumentide vormingutega. Tegelikult on DigiDoc konteiner formaatidega seotud, mis pakuvad erinevaid digitaalseid allkirju. Praegune oluline vorming on BDOC, mille eesmärk on olla rahvusvahema standarditele vastav. Kas sa tahad teada rohkem BDOC kohta?",output_ID.ee +"user: Tere! Ma olen tootja ja uuenen selle kohta, kuidas tooted ligipääsetavaks muutmine toimib. Saaksite mulle alguses selgeks teha, mis on sellega seotud peamised seadused? +assistant: Tere! Muidugi. Peamiseks seaduseks on Toodete ja teenuste ligipääsetavuse seadus, mis kohaldatakse paljudele toodetele ja teenustele. Lisaks on oluline arvestada määruseid nr 46 ja 47, mis täpsustavad ebaproportsionaalse koormuse hindamist ja teavitamise korra. Üheskoos nendad need reguleerivad ligipääsetavuse nõuete järgimist Eestis.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et kasutatud auto ostmine võib olla riskantne. Mis on kõige olulisem, mida ma peaksin silmas pidama, kui ma kavatsen kasutatud autot osta? +assistant: Tere! See on väga hea küsimus. Kõige olulisem on olla tähelepanelik ja veenduda, et sa tead, keda sa ostate. Vaata äriregistrist, kas autot müüb era- või juriidiline isik. Eraisiku käest osta on odavam, aga eraisikutevahelisi vaidlusi lahendab vaid kohus. Kui ostate firmalt, saad infot Krediidiinfo kodulehelt ja äriregistrist.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma just saime Digi-ID ja ma natuke muretsen, mida teha, kui kaart kaotaks. Mida ma peaks esimesena tegema? +assistant: Tere! See on täiesti normaalne. Esiteks, kui kaart on kadunud või varastatud, helista kohe numbrile 677 3377, et sertifikaadid peatada. See on oluline, et keegi ei saaks sinu nimele toiminguid teha.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma mõtlen, et Smart-ID peaks olema hea, aga kuidas seda kasutada? Kas see on sama, mis ID-kaart? +assistant: Tere! Päevas! Jah, Smart-ID pakub palju samu võimalusi nagu ID-kaart ja mobiil-ID. Sa saad sellega e-teenustesse sisse logida, pangaülekandeid kinnitada ja isegi dokumente allkirjastada. See on kindlasti mugav alternatiiv. Kas sulle on teada, millise e-teenusega sa seda kasutada soovid?",output_ID.ee +"user: Tere! Ma mõtlen, kuidas Eestisse ettevõtlasena elama jääda. Kas ma saan elamisluba ettevõtluseks? +assistant: Tere! Jah, seda on võimalik. Ettevõtluseks elamisluba saamiseks pead investeerima Eestisse oma äriühingusse. Kas sa juba tead, millisega äri tegeleda või on sul äriidee veel arenduses?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Ametil on komisjonid, mis lahendavad vaidlusi kauplejatega. Kuidas see üldiselt töötab? Mis juhtub, kui mul on probleem ostuga? +assistant: Tere! Jah, meil on tarbijavaidlustekomisjon. Üldjuhul, kui teil on probleem ostuga, esitate avalduse. Amet annab kauplejale aja, et ta saaks oma seisukohti kujundada ja vastata väidetele. See annab võimaluse leida lahendus enne komisjoni istungit.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on plaanil avalik üritus korraldada, ja mul on palju küsimusi. Alustaksime põhjaosas. Kuidas ma peaksin seda kõigepealt kohalikele omavalitsustele teavitama? +assistant: Tere! See on väga hea, et alustate etteval valmist. Avaliku ürituse korraldamiseks peab teid kõigepealt konsulteerima kohaliku omavalitsusega. Kuigi eelnimetatud üritus ei ole koosolek, tuleb sellest teatada kohalikule omavalitsusele, kelle piires soovite ürituse korraldada. Palun külastage oma kohaliku omavalitsuse kodulehte, et leida taotlusvorm ja lisateave. Nad hindavad taotlust ja vajadusel kaasavad Politsei- ja Piirivalveameti, Päästeameti ja/või Tarbijakaitse ja Tehnilise Järelevalve Ameti loa kooskõlastamisse.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud, et Tarbijavaidluste komisjon on olemas. Mis on selle komisjoni eesmärk ja kuidas see töötab? +assistant: Tere! Tarbijavaidluste komisjon on loodud selleks, et lahendada tarbija ja kaupleja vahel tekkinud lepingust tulenevaid vaidlusi lihtsamini, kiiren ja odavamalt kui kohtus. Võrreldes kohtuvaitlusega on see protsess mugav ning nõuab vähem aega ja raha.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mida Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tähendab ehitiste ohutuse osas? +assistant: Tere! TTJA järelvalvega on seotud ehitiste kasutamisele ja korrashoidmisele kehtivad nõuded, mis tulenevad seadusest, heast tavast või ehitise kasutus- ja hooldusjuhendist. Omanik peab tagama, et ehitis vastab kõikidele nõuetele. Meie soov on kaardistada ohtlikud ehitised ja kohustada omanikke neid parandama.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma lugesin midagi tarbija õiguste kohta, aga ma ei ole kindel, kust alustada, kui mul on probleemiga kauplus. Mis on esimene asi, mida ma peaksin tegema? +assistant: Tere! Kui teil on probleem kauplusega, on esimene asi, mida peaksite tegema, kirjutada neile kirja. Selgitage kirjas probleem ja millist lahendust te soovite. See on oluline samm, enne kui jätkate edasiste meetmete juurde.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen ostnud poodist televiisori, aga see ei tööta nii, nagu peaks. Kust ma peaksin selle kohta kaebust esitama? +assistant: Tere! Esiteks, pöörduge otse müüja poole. On hea, kui teil on ostutšekk või leping alles, need on kaebuse esitamisel väga kasulikud. Peakski kaebuse esitama hiljemalt kahe kuu jooksul alates vea avastamisest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen ostmas uus käevõru, aga ma ei tea, millele peaksin tähelepanu pöörama, et see oleks kvaliteetne ja vastaks õigetele standarditele. Mis on Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) poolt antud juhised selle kohta? +assistant: Tere! See on hea küsimus. TTJA poolt on oluline, et müüdaval käevõrul oleks proovi- ja nimemärgis. Proovimärgis näitab, millist metalli on kasutatud ja selle osa sulamis, näiteks kuld 375 või hõbe 925. Nimemärgiks on märk, mis peab olema riiklikus registris registreeritud. Kontrollimärgistega tooted on ka eelistatud, näiteks lõvi kuju Eestis.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen välismaalane ja mul on küsimusi viisa kehtetuks tunnistamise kohta. Kui ma saan selle otsuse vaidlustada? +assistant: Tere! Jah, teil on õigus vaidlustada viisa andmisest keeldumise, viisa kehtetuks tunnistamise või viisa tühistamise otsust. Teil on selleks 30 päeva aega alates otsuse teada saamisest.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen huvitatud, kus Paides asub Kesk-Eesti politseijaoskond? +assistant: Tere! Kesk-Eesti politseijaoskond Paides asub aadressil Tallinna 12, 72720 Paide.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma pole päris kindel, mida ehitamine seadustiku mõistes tähendab. Saaksid seda mulle selgeks teha? +assistant: Tere! Ehitamine seadustiku mõistes hõlmab mitmeid tegevusi: ehitise püstitamist, rajamist, paigaldamist, lammutamist ja muid tegevusi, mille tulemusena ehitis tekib või muutuvad selle füüsikalised omadused. Ehitusloa või ehitusteatis on teatud juhtudel nõutav.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et minu tüli tüdruk pole pärit Eestist. Ta küsib, kas ta saab Eesti kodakondsust, sest ta on siin elamine luba saanud. Mida ta peab tegema? +assistant: Tere! Jah, seda on võimalik. Arvatavasti sa mõtled, mida ta peab tegema, et Eesti kodakondsus taotleda. Alguspunkt on see, et ta peab olema vähemalt 15-aastane. Kas ta on selline?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma ei ole pärijadega tuttav elektrooniliste allkirjadega. Mis see eIDAS asi üldse on ja miks see oluline on? +assistant: Tere! EIDAS on Euroopa Liidus kehtiv määrus, mis on loodud, et elektrooniliste teenuste kasutamine oleks lihtsam ja ühtlasem kogu Euroopas. Põhidepõhimõte on see, et erinevate riikide allkirjad oleksid üksteisel põhjal võrdsed. Kasutajad saavad siis ka üle piiri elektroonilisi teenuseid kasutada, näiteks sularaha laenu taotlemisel. Kas sulle see idee üldse mõistab?",output_ID.ee +"user: Tere! Ma kuulsin midagi piirkondlikust politseitööst. Mis see tähendab täpselt? +assistant: Tere! Piirkondlik politseitöö tähendab, et politseinikud keskenduvad konkreetses piirkonnas probleemide lahendamisele ja kogukonna vajaduste täitmisele. See on üks politsei töövorme, mis tugineb kogukonnakeskse lähenemise põhimõtetele.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on tähtajaline elamisluba töötamiseks Eestis ja see lõppeb peatselt. Kuidas ma saan seda pikendada? +assistant: Tere! Elamisluba töötamiseks saab pikendada. Peate taotlema pikendamist vähemalt 2 kuud enne praeguse elamisloa lõppemist. Taotluse menetlemisel esitage nõutavad dokumendid ja tasuge riigilõiv. Kui esitate taotluse õigeaegselt, saate vastuse hiljemalt 10 päeva enne kehtiva loa lõppemist.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud, et Politsei- ja Piirivalveamet on oluline riigiasutus. Milline on PPA registrikood ja aadress? +assistant: Tere! Jah, see on õige. PPA registrikood on 70008747 ja nende aadress on Pärnu mnt 139, 15060 Tallinn.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus teenuse ohutuse kohta. Mis on tarbija õigused teenuse osutajal? +assistant: Tere! Põhiliselt on sul õigus oodata, et teenus vastab kehtestatud nõuetele. Ja kui erinõudeid ei ole, ei tohi teenus seada ohtu sinu elu, tervist või vara. See tähendab, et teenusepakkuja peab teenuse ohutuse tagama.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma jätsin oma ID-kaardi kaduma. Kas ma pean ka sellega seotud sertifikaadid peatama? Ja kuidas seda teen? +assistant: Tere! Jah, kindlasti! Kui olete kindel, et teie ID-kaart on kadunud või varastatud, siis peate sellega seotud sertifikaadid peatama, et varguse korral oleks teie identiteet paremini kaitstud. Seda saab teha telefoni teel või Politsei- ja Piirivalveameti teeninduses. Mida tegelikult peame tegema, on nii, et sertifikaat muutub mittesobivaks. Kas teiega oleks mugav telefoni teel küsida, kuidas see toimib?",output_ID.ee +"user: Tere! Mul on küsimus elamisloakaardi kohta. Kust ma selle saan, kui mul on elamisluba ettevõtluseks suurinvestorina? +assistant: Tere! Elamisloakaardi saad kätte isiklikult Eesti välisesinduses või Politsei- ja Piirivalveameti teeninduses. Sa pead valima selle koha, mille märkisid oma tähtajalise elamisloa taotlusesse.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kaotanud oma isikut tõendava dokumendi. Kus ma saaksin Jõgeva politseioskonda leida? +assistant: Tere! Jõgeva politseioskond asub aadressil Suur 1, postikoht 48306 Jõgeva. See on kõige lähedasem koht, kus sa saad oma dokumendi kaotamise kohta avalduse esitada.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et digiallkirjad on nagu tavaline kiri, aga digitaalselt. Kas .bdoc ja .asice faile saab mõlemad käsitleda samamoodi? +assistant: Tere! Tegelikult on see pisuvõrd küsimus. Need faile saab üldjuhul käsitleda samamoodi, aga see, kuidas need digiallkirjutega liidetakse, on oluline erinevus. .bdoc faile kasutati varem veel, nüüdseks on .asice domineeriv formaat. Kas sa tahaksid rohkem teada, kuidas need faile erinevad?",output_ID.ee +"user: Tere! Mul on küsimus elamisloa pikendamise kohta. Ma õpin siin Eestis ja mu elamisluba lõppeb peagi. Millal ma peaksin selle pikendamist taotlema? +assistant: Tere! Elamisloa pikendamist õppimiseks peaksid saama esitada vähemalt 2 kuud enne kehtiva elamisloa lõppemist. See tagab, et saaksid vastuse õigeaegselt.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud midagi ühtse laadija kohta. Mis see tähendab ja miks see muutub oluliseks? +assistant: Tere! Ühtne laadija tähendab, et paljudele seadmetele, näiteks mobiiltelefonidele, tahvelarvutitele ja sülearvutitele tuleb kasutada USB-C laadimispesa. Selle eesmärk on standardiseerida laadimist ja vähendada erinevate laadijate kasutamise vajadust.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus elamisõiguse kohta. Kuidas täpselt seda taotletakse? Kas on mingi formaalnev protsess? +assistant: Tere! Jah, elamisõiguse taotlus on menetlus. Politsei- ja Piirivalveamet kontrollib, et sa oled esitanud kõik tarvilikud dokumendid. See tähendab, näiteks isikutunnusraamat, kinnituse leping või muu dokument, mis kinnitab teie suhteid. Peamaksud on näiteks dokumentatsioon, mis kinnitab, et te elate koos ja et teil on leping kinnitab teineteise toetust.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma ei saa ID-kaardiga veebilehtedel eesti.ee keskkonnas toimida, ja mul on raskusi. Mis ma peaks tegema? +assistant: Tere! Üsna sagedasti tekib just see probleem. Palju õnne, et teadsid, et sinuga on asi. Peame siis veendutama, et veebilehitseja on ID-kaardi sertifikaatidega õigesti seadistatud. Alustame kõige lihtsamast – kontrolli, et veebilehitsejas on Web eID laiendus lubatud. Kas sa kasutad Chrome, Edge või Midigate?",output_ID.ee +"user: Tere! Ma kaotanud oma digi-ID kaardi. Mida ma peaksin tegema? +assistant: Oh, see on kahjuks mitteideaalne olukord. Esimene asi, mida pead tegema, on helistada numbril 677 3377, et sertifikaadid peatada. See takistab kellegi muu kasutamas su digi-IDd. Seejärel saad minna Politsei- ja Piirivalveameti teenindusse või Eesti Vabariigi välisesindusse, et kas sertifikaadid taas aktiveerida, kui leidsid dokumendi, või taotle uut dokumenti.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimusi ID-kaardi kasutamise kohta. Mis on olulisim asi, mida ma pean meeles pidama, et seda turvaliselt kasutada? +assistant: Tere! Olulisim asi on kindlasti hoia oma PIN-koode salajas ja turvalises kohas. Võimaluse korral õpi need pähe, et sa ei peaks PIN-koodi üles kirjutama ja seda edastama. Ära anna ka oma ID-kaarti kellegi teise kätte!",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma nägin ID.ee saidikus kohta Digidoc failivorminguid. Kas saaksite öelda, mis need BDOC ja CDOC kujulised failid täna Eestis üldse tähendavad? +assistant: Tere! Muidugi. Põhjaosas Eestis kasutatakse praeguses enamus digitaalallkirjade puhul BDOC-faili, mis on varem tuntud kui Eesti-spetsiifiline LT-TM formaat. CDOC on seevastu laiendus, mille abil eristatakse krüpteeritud DigiDoc vormingus olevaid faile. Pea silmass sellele, et uus versioon kasutab automaatselt .asice faili asemel.",output_ID.ee +"user: Tere! Ma kuulsin, et nüüdisaegselt saan oma telefoninumbrit ühe operaatori juurest teise juurde liikuda. Kuidas see üldse toimib? +assistant: Tere! See on õige, numbri liikuvus on võimalik. Põhimõtteliselt on kaks võimalust. Sa saad volitada uut operaatorit lõpetama sinu praegune leping endisega. Või siis, kui operaatoritel on kokkulepe, saab koheselt sõlmitud tingimuslik liitumisleping ja antakse uus SIM-kaart. See numbri liikuvus jõustub alles hiljem.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma lugesin TTJA lehel midagi isikuandmete töötlemise kohta, aga see oli natuke keeruline. Ühesõnas, mida te TTJA-s isikuandmete kohta teete? +assistant: Tere! TTJA-s kogub töötades teavet inimestest, näiteks kui kirjutate meile või olete menetluse osapool. Me töötleme neid andmeid erinevates infosüsteemides, sh üleeuroopalistes, kus need on kaitstud. Põhiliselt täidame avalike ülesannete täitmiseks ja asjade lahendamiseks vajalikku mahut.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimusi selle kohta, kuidas mu täisealisele lapsele, kes elab välismaal, elamisluba Eestisse saada. Mis see üldse tähendab, elamisluba pereliikmega liitumiseks? +assistant: Tere! Elamisluba pereliikmega liitumiseks annab sinule võimaluse kutsuda oma välismaalast täisealist last Eestisse elama, kui oled Eesti kodanik või Eestis elamisloa alusel elav välismaalane. See lubab tal siin elada sinuga lähedasena või koos sinuga Eestisse kolida.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan lapsele passi taotleda, aga ei tea, kuidas riigilõivu maksta. Kust ma selle maksta saan? +assistant: Tere! Riigilõivu saad tasuda mitme panga kaudu. Saaja on Rahandusministeerium. Kontonumbrid on: SEB EE891010220034796011, Swedbank EE932200221023778606, Luminor Bank EE701700017001577198 ja LHV Pank EE777700771003813400.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen Euroopa Liidu kodanik ja plaanin Eestisse kolida. Mis on mu võimalused siin elamiseks? +assistant: Tere! Euroopa Liidu kodanikuna on sul Eestis kolmel võimalusel: ajutine viibimine (kuni 3 kuud), tähtajaline elamine (kuni 5 aastat) ja alaline elamine. Mida sa soovid Eestis teha ja kui kauaks sa plaanid jääda?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma lugesin mingit teksti tarbija õigustest, aga see oli üsna pikk. Mis on üldjuhul esimene koht, kuhu tarbija peaks pöörduma, kui tal on probleem mingi ettevõttega? +assistant: Tere! Kui teil on probleem ettevõttega, on tarbijaõiguste puhul hea koht alustuseks tarbijavaidluste komisjon. Nad aitavad lahendada vaidlusi tarbijate ja ettevõtete vahel.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus politsei piirkondliku töö kohta. Mis see tähendab ja kuidas see erineb tavalisest politseist? +assistant: Tere! Piirkondlik politseitöö on tegevus, mis keskendub konkreetses piirkonnas olevate kogukonna vajaduste lahendamisele. See tugineb kogukonnakesksele politseitööle ja eesmärgiks on lähendada politseid kogukonnale, leida piirkonna turvalisust ohustavaid probleeme ning arendada koostööd asutuste ja kohalike omavalitsustega. See erineb tavalisest politseist selle poolest, et fookus on probleemide ennetamisel ja laiemaid lahendusi otsimisel, mitte ainult reageerimisel sündmustele.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus elektriturvalisuse kohta. Mida peaks silmas pidama, et kodu kasutamisel oleks kõik ohutu? +assistant: Tere! Oluline on tagada, et elektriseadmed ja -paigaldised on korras ja vastavad nõuetele. Elektriseadet võib kasutada alles pärast kontrolli, et see on tehniliselt korras ja ohutu. Peamiselt tasub meeles pidada, et elektritöid ei tohiks ise teha, vaid pöörduda pädeva elektriala isiku poole.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma lugesin, et Politsei- ja Piirivalveamet avaldab liiklusinfo kohta andmeid. Mis seal täpselt näha saab? +assistant: Tere! Politsei- ja Piirivalveameti veebilehel ongi ülevaade liiklusõnnetustest, kus on inimesed saanud vigastusi. Seal on info liiklusõnnetuste kohta, millega seoses on inimkannatanuid olnud.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et Smart-ID tundub päris hea asi. Kuidas sellega saab e-teenuste juurde sisse logida? +assistant: Tere! Päris hea mõte, et huvitatud olete! Smart-ID on nutirakendus, millega saate e-teenustesse sisse logida ja tehinguid kinnitada. See on nagu ID-kaart või mobiil-ID, aga seda saab kasutada ka telefoni kaudu. Peaasi, et selle jaoks on konto olemas.",output_ID.ee +"user: Tere! Ma olen kuulnud Digi-ID taotlemisest, aga ma ei tea, kust ma alustama peaksin. Mis on esimene asi, mida ma peaksin tegema? +assistant: Tere! Digi-ID taotlemiseks peate esmalt pöörduma Politsei- ja Piirivalveameti teenindusse. Seal saate taotlusprotsessi käima panna. Täpsem info asukohad on saadaval aadressil https://www.politsei.ee/et/asukohad/teenindused.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud, et politsei teeb piiri ehitamist. Kas sa saaksid rääkida selle kohta midagi? +assistant: Tere! Jah, see on õige. Politsei- ja Piirivalveamet (PPA) on käivitanud projekti Eesti välispiiri taristu väljaehitamine. Alustati kagupiiri ehitamisega. Ühes etapis ehitatakse 23,5 kilomeetrit piiri Võrumaal, alates kolmikpunktist kuni Luhamaa piiripunktini.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas ma saan RIA DigiDoc äpi uue versiooni alla laadida? Mis tasu? +assistant: Tere! Jah, RIA DigiDoc mobiilirakenduse uusim versioon on alati saadaval App Store'ist (https://itunes.apple.com/ee/app/ria-digidoc/id1216104448?mt=8) ja Google Play store'ist (https://play.google.com/store/apps/details?id=ee.ria.DigiDoc). See on tasuta!",output_ID.ee +"user: Tere! Olen kuulnud, et piiri tugevdamisel kasutatakse lõiketraati. Kas see ei mõju metsloomadele? Mida politsei selleks teeb? +assistant: Tere! See on väga hea küsimus. Jah, lõiketraadi rajamine võib mõjutada metsloomade liikumist. Politsei on teadlik sellest probleemist ja on Keskkonnaametiga konsulteerinud, kuidas sellega tegeleda. Üldiselt on väiksemad loomad suutelised lõiketraadist läbi pääsema, kuid suuremad loomad võivad takerduda.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TJAT) tegeleb digiligipääsetavusega. Mis see täpselt tähendab? +assistant: Tere! Digiligipääsetavus tähendab, et digisisu ja -teenused on kättesaadavad ja kasutatavad kõigile, sealhulgas inimestele, kel on puueteid. TJAT järelvalvega seoses on see oluline, et avaliku sektori teenused oleksid digitaalselt kättesaadavad kõigile kodanikele. Võite tutvuda selle teema kohta rohkem TJAT poolt loodud koolitustest – nendest on mitu osa YouTube'is.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kaotasin oma rahakoti ja tahan teada, kust ma saan politseisse minna, kui elan Lääne-Harjus. Kas te teate, kus Lääne-Harju Kesklinna politseijaoskond asub? +assistant: Tere! Jah, Lääne-Harju Kesklinna politseijaoskond asub Kolde puiestee 65, 10321 Tallinn. See on Lääne-Harju piirkonna politseijaoskond.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et DigiDoc4 on uus tarkvara. Kas saaksite mulle öelda, mida see teeb ja kuidas seda kasutada? +assistant: Tere! Jah, DigiDoc4 on ID-kaardi ja Smart-ID omanikele ettenähtud tarkvara digiallkirjade andmisel. Peamisi selleks on kasutada digiallkirju dokumentidele. Selle abil saate luua usaldusväärseid digiallkirju. Peamised erinevad on ajatempel ja kehtivuskinnitusteenus, mis kinnitavad digiallkirja autentseks ja ajakohaks.",output_ID.ee +"user: Tere! Ma kuulsin, et droonide kasutamine on muutunud keerulisemaks. Mis mulle selle kohta teada peaks olema Tarbijakaitse ja Tehnilise Järelevalve Ameti poolt? +assistant: Tere! Jah, droonide kasutamisel on oluliselt rohkem regulatsioone. Peamine asi on see, et droonide juhtimiseks kasutatavad raadiosagedused peavad olema nõuetele vastavad. See tähendab, et droonil peab olema CE-märgistus ja vastavusdeklaratsioon.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen välisriigi kodanik ja plaanin Eestis ettevõtlusega alustada. Kas mulle on vaja elamisluba? +assistant: Tere! Jah, kui sa ei ole Euroopa Liidu, Islandi, Liechtensteini, Norra või Šveitsi kodanik, siis Eestis ettevõtlusega tegelemiseks on sulle vaja elamisluba.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma ei suuda DigiDoc4 programmi oma arvutis leida, kui ma ID kaardi kasutada tahan. Kuidas ma seda tegema saan? +assistant: Tere! Helge, küta. DigiDoc4 programmi leidmiseks on erinevad viisad sõltuvalt teie arvuti operatsioonisüsteemist. Kas te kasutate Windowsit, macOS või Linuxi? Kui te Windowsit kasutate, siis programmi ikoon peaks automaatselt arvuti töölauale paigaldatud olema. Kas te seda näete?",output_ID.ee +"user: Tere! Mul on küsimus selle energiamärgise kohta. Mis see tähendab, et toodete energiamärgistus on Euroopa Liidus ühtne? +assistant: Tere! See tähendab, et erinevate tootegruppide, näiteks külmkappide või pesumasinate, toodete energia tarbimise nõuetele ja märgistusele on kehtestatud ühtsed reeglid. See tagab, et märgistuse vorm on võimalikult sarnane ja selgelt nähtav kõikides riikides. Näiteks, kui näete A-klassi külmkappi, teate, et see on ühesugult energiasäästlik ka teistes EL riikides.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma saan aru, et ID-kaardi PIN-koodid võivad kaduda. Kuidas saan uue PIN-koodi taotleda? +assistant: Tere! Jah, see on võimalik. Uue PIN-koodi taotlemiseks pead pöörduma Politsei- ja Piirivalveameti teenindusse või Eesti Vabariigi välisesindusse. On oluline kaasa võtta isikut tõendav dokument. Kas sa tead, kus on lähim Politsei- ja Piirivalveameti asukoht?",output_ID.ee +"user: Tere! Ma huvitasin end Politsei- ja Piirivalveameti riigihanketest. Kust ma nende kohta infot leian? +assistant: Tere! Kõik Politsei- ja Piirivalveameti hanketeated ja riigihangete aruanded on avalikult kättesaadavad riigihangete registri veebilehel: https://riigihanked.riik.ee/. Seal leiad kõik vajalikud andmed.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tahan teada, mida ma pean tegema, kui mul on probleem ostetud toega. Alguses kas peaksin pöörduma ise müüja poole? +assistant: Tere! Õige küsimus. Enne kui Tarbijakaitseametisse pöörduda, pead kõigepealt pöörduma kaebusega müüja poole. Müüja on kohustatud sinule vastama 15 päeva jooksul.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimusi tõstukite ohutuse kohta. Kas teil on aimu, kui palju tõstukse on Eestis kasutusel? +assistant: Tere! See on keeruline küsimus. Täpset arvu ei teata, kuid ettevõtted arvavad, et neid on rohkem kui 100 000 ja võib-olla isegi kuni pool miljonit. Seadmete ohutuse tagamine on seega suur ülesanne.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) teeb tööd sideteenuste puhul. Mis on nende peamine roll selles valdkonnas? +assistant: Tere! TTJA peamine roll on tagada tarbijate õiguste kaitse sideteenuste valdkonnas. Selleks reguleerib TTJA sideteenuste osutamist, lähtudes elektroonilise side seadusest. See seadus reguleerib lepingu sõlmimist, rikete kõrvaldamist, piiramist ja lepingu muutmisi/lõpetamist.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus elamisloakaardi kohta. Kust ma selle saan, kui mul on suurinvestori elamisluba? +assistant: Tere! Suurinvestori elamisluba andes saad elamisloakaardi kas Eesti välisesindusest või Politsei- ja Piirivalveameti teenindusest. Mis asukoht oli märgitud sinu tähtajalise elamisloa taotluses?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma just sain ID-kaardi ja tahan teada, mida ma pean sellega seoses meeles pidama. Mis on kõige olulisem? +assistant: Tere! Kõige olulisem on hoia PIN-koode salajas ja turvalises kohas. Ära anna neid kellegi teise kätte ja proovi need pähe õppida, kui võimalik.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen ID-kaardi kasutamiseks DigiDoc4 programmi installinud, aga ei suuda seda oma arvutis leida. Kas teil on mõni nõu hulgimisel? +assistant: Tere! Päev vastu! Jah, seda on lihtne leida. Kui sa oled ID-kaardi kasutamiseks DigiDoc4 programmi juba installinud, siis DigiDoc4 ikoon peaks arvuti töölaual olema. Kui seda ei ole, siis tee topeltklõps sellel, millel on 'D' tähe all nimega DigiDoc4. Kas sa leiad selle ikooni?",output_ID.ee +"user: Tere! Olen kuulnud, et digiturvalisus on oluline. Kust ma peaksin alustama enda ja laste turvalisuse tagamise osas? +assistant: Tere! See on väga hea küsimus. Alustage tarkvara uuendamisest kõikides seadmetes – telefonis, tabletis, arvutis. Aegunud tarkvaras on turvalisi auke, mida pahavara võib ära kasutada. See on lihtne, aga oluline samm!,",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma just kahtlen, kuidas Paides asuva Kesk-Eesti politseijaoskonna leida. Kas te teate selle aadressi? +assistant: Tere! Kesk-Eesti politseijaoskond Paides asub aadressil Tallinna 12, 72720 Paide.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul oli probleeme ID tarkvara paigaldamisega. Sai teateid, et paigaldus läks tõrkeid. Kuidas ma saan sellest aru ja mida peaks tegema? +assistant: Tere! Üsna sagedased probleemid. Selle peamiseks põhjuseks on see, et Windows registreis on vana ID tarkvara komponentide kirjeid, mis uue versiooni ei nõua. Vaevalt paigaldus lõpeb, täna on see probleem seotud Windowsi operatsioonisüsteemi uuendusega. Kas sulle kuvati mingisugune veateade? Kui võisa seda koodi või veateadet meelde tuua, oleks see väga hea.",output_ID.ee +"user: Tere! Ma ei ole päris kindel, miks minu ID tarkvara on uuendamine soovitatud. Mis see tähtsusetu on? +assistant: Tere! Kui sul on ID tarkvara uuendamine soovitatud, see on hea märk – see tähendab, et sinu seade on turvalisemaks ja tööefektiivsemaks ümber tehtud. Peamiselt on uuendused seotud turvariskide vähendamisega või selleks, et tarkvara töötaks paremini teiste programmidega. Kas sulle on teada, mis ID tarkvara on?",output_ID.ee +"user: Tere! Olen kuulnud, et Politsei- ja Piirivalveamet on lapsevanematele mingit ohutusraamatut valmistanud. Mis see raamat on ja kellele see mõeldud? +assistant: Tere! Jah, meil ongi olemas Ohutusraamat lapsevanematele, mis on mõeldud eelkõige 1. klassi minevate laste vanematele. See ongi materjal, mis toetab lapsevanemaid lapse elu kaitsmisel ja turvalise kasvukeskkonna loomisel.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus. Mis on oluline, kui tellin midagi telefoni teel? Pean ikka sellele kohustusatud olema? +assistant: Tere! Telefoni teel tellimisel on oluline, et kaupleja oleks sulle kinnitanud lepingu püsival andmekandjal (nt e-kirja) ja sina ise olema kinnitanud oma tahet ka selles vormis. Kui seda ei juhtu, sa ei ole selle lepingu poolt kohustatud. Muidu võib olla keeruline jätta lepingust loobuda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma nägin, et ID.ee juures on sertifitseerimiskorra teatud muudatusi. Kas saaksite öelda, milline on uus sertifitseerimiskorra versioon praegu keesingil? Vaatan, et need muutuvad sageli... +assistant: Tere! Jah, on. Praegune sertifitseerimiskorra versioon on v2.1. See jõustus 16. augustil 2024. Olete õigesti tähendanud, et need muutused on suhteloolised. Kas teiega on juttu sellest, kuidas see muutmine mõjutab ID-kaardi omanikke? See on uusim versioon.",output_ID.ee +"user: Tere! Olen kuulnud palju 5G kohta, aga ma ei saa päris hästi aru, mida see tähendab. Mis on need kolm peamist arengusuunda, mida Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) mainib? +assistant: Tere! TTJA kohaselt on kolm peamist arengusuunda 5G tehnoloogias: suurendatud andmesidekiirus (eMBB), tagatud ajakriitiline reaalajasüsteemide andmeside (URLLC) ja suurearvuline väikeseadmete kommunikatsioon (mMTC). See tähendab, et 5G lubab palju kiiret internetti, usaldusväärseid ühendusi reaalajas rakendusteks ja toetab paljusid seadmeid, mis on ühendatud võrku.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et Politsei- ja Piirivalveamet teeb palju riigihankeid. Kust ma saaksin infot nende hankete kohta leida? +assistant: Tere! Jah, meil on riigihankeid mitmeid. Kõik avalikud hanketeated ja aruanded on leitavad riigihangete registri veebilehel: https://riigihanked.riik.ee/ . Seal leiad kogu vajaliku info.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et tahaksin teada rohkem elamisõigusest, eriti kui ma soovin elada peresega, kelle kohta on juba elamisõigus tehtud taotlus. Kuidas see menetlus toimub? +assistant: Tere! Noh, elamisõiguse menetlemine on algusest lõpuni suhtelooline. Politsei- ja Piirivalveamet (PPA) kontrollib, et taotlus on tehtud nõuetekohaselt ja et kõik elamisõiguse saamiseks vajalikud tingimused on täidetud. See tähendab, et kontrollime näiteks isikutunnistust ja muu dokumentatsiooni. Kui on midagi puudu või vaja rohkem infot, siis teeme teiega ühendust.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mida pean silmas pidama, kui ostan midagi internetis? +assistant: Tere! Internetis ostes on oluline teada, et Sul on õigus lepingust taganeda 14 päeva jooksul, alates päevast, mil kaubad Sinuni jõuavad. See on seadusega garanteeritud võimalus, et Sa saaksid toote tagasi anda, kui see ei vasta Sinu ootustele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mulle tekkis küsimus kiiruskaamera fotode kohta. Kas trahviteatega kaasneb ka foto koopia? +assistant: Tere! Ei, trahviteatega fotokoopiat ei lisata. Aga kui sa oled sõiduki eest vastutav isik ja soovid fotot, mille alusel liiklusrikkumine tuvastati, siis saad selle taotleda.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus elamisloa pikendamise kohta. Ma õpin Eestis ja mu elamisluba õppimiseks lõppeb pea olemas. Mis ma pean tegema? +assistant: Tere! Elamisloa pikendamiseks õppimiseks pead saama taotluse esitada vähemalt 2 kuud enne kehtiva loa lõppemist. Pead esitama mitmeid dokumente ja tasuma riigilõivu. Kas sa soovid, et ma mainiksin täpsemalt, milliseid dokumente vaja on?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et ID-tarkvara on minu jaoks väga oluline. Aga mis see ‘toeperioodi pikkus’ tähendab? Ja miks see on nii tähtis? +assistant: Tere! Päev vastu on ja ma saan aru, et see on uus. ‘Toeperioodi pikkus’ ID-tarkvara kontekstis tähendab seda, kui kaua ID-tarkvaraversioon on aktiivne ja et selle versiooni kohta pakutakse toetust. Kui versioon on ‘elatsükli lõppenud’, enam ei ole selle versiooni kohta ID kasutajatugi (teadmistebaasis), ei testita sellega seotud vigade leidimist ja vastu ei võeta tegelikult mitte mitte mitte midagi - ei uut tehnilist lahendust, ei funktsiooni muudatuste taotlusi. Peamine põhjus on turvajärjekorra. Vajalik on alati uusima versiooni kasutada. Kas sulle on plaanis uuendamine?",output_ID.ee +"user: Tere! Ma just lugesin, et 112 on hädaabinumber. Aga millal täpselt ma sellele helistama peaksin? +assistant: Tere! 112-le helistad, kui vajad politsei kiiret abi ja soovid teatada korrarikkumisest, kuriteost või liiklusõnnetusest. See ongi näiteks, kui näed kedagi, kes on mures või vajan abi pärast liiklusõnnetust.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib sideturge. Mis need täpsemalt on? +assistant: Tere! See on väga hea küsimus. Sideturgude puhul TTJA jälgib näiteks kõne lõpetamise hulgiteenust telefonivõrgus või mobiiltelefonivõrgus, kohalikku juurdepääsu hulgiteenust kindlas kohas ja kvaliteetset juurdepääsu hulgiteenust samuti kindlas kohas. See on oluline, et tagada konkurents ja õiged hinnad tarbijatele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma saan aru, et peaks ID-kaardi tarkvara macOS arvutis paigaldama. Kutsutakse seda ID-tarkvara? Ja kuidas ma saan selle alla laadida? +assistant: Tere! Jah, see on täpsasti ID-tarkvara. See koosneb mitmest komponendist. Sa saad selle alla laadida DigiDoc4 rakendusest, mille lingi leiad ID.ee veebilehelt: [https://www.id.ee/artikkel/digidoc4-raakendus]. See on ühedest kolmest komponendist, mida pead macOS arvutile paigaldama.",output_ID.ee +"user: Tere! Mul on uus ID-kaart ja koodiümbrik. Kas ma pean kohe PIN- ja PUK-koodid vahetama? +assistant: Tere! Jah, on väga oluline PIN- ja PUK-koodid kohe uuega vahetada. Kui sa neid uuega vahetamata jätad, ei saa ID-kaarti elektrooniliselt kasutada. Ja kui keegi saadab need koodid võõrasse kätesse, on ka sinu elektrooniline identiteet ohus. Kas sa oskad öelda, miks on see nii?",output_ID.ee +"user: Tere! Ma lugesin midagi ehitiste ohutuse kohta Tarbijakaitse ja Tehnilise Järelevalve Ameti (TTJA) leheküljel. Kuidas ma saan aru, kas minu maja on ohutu? +assistant: Tere! Ehitise ohutus tähendab, et see peab vastama mitmetele nõuetele. Need tulenevad seadustest, heast tavast või ehitise kasutus- ja hooldusjuhendist. Näiteks, maja peab olema mehaaniliselt vastupidav, tuleohutu, hügieeniline ja kasutajate jaoks ohutu – sealhulgas lihtne evakueerida tulekahju korral.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus elamisloa kohta. Mis juhtub, kui mu elamisluba täisealisele lapsele pereliikme juurde elama asumise põhjal lõppeb? +assistant: Tere! Kui sinu elamisluba lõppeb, saad sa Eestisse veel jääda kuni 270 päevaks. Sel ajal on sul ka võimalus Eestis töötada.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus ID-kaardi kohta. Ma tahaks teada, mida ma pean tegema, kui kaotan ID-kaardi. +assistant: Tere! Oluline on kohe tegutseda. Esimene asi, mida peaks tegema, on helistada numbril 677 3377, et sertifikaadid peatada. See takistab kellegi teise oma isik andmetega kuritarvitamist. Kas sa tead, kust seda infot leidsid?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud palju digiturvalisusest, aga ma ei tea, kust ma peaksin alustama. Mis on kõige olulisem asi, mida ma peaksin oma ja laste turvalisuse tagamiseks silmas pidama? +assistant: Tere! Alguspunktiks on kindlasti tarkvara uuendamine kõikides seadmetes – telefonides, arvutites, tahvlitel. Aegunud tarkvara on pahavara jaoks lihtne tee sisse. Veelgi olulisem on luua tugevad paroolid. Need peaksid olema pikad, raskesti äraarvatavad ja erinevad igas kontol. Riigi Infosüsteemide Ametil on selle kohta väga hea lehekülg: https://www.ria.ee/et/kuberturvalisus/nouanded/parooli-loomine.html",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kahtlen, kuidas Paides asuva Kesk-Eesti politseijaoskonna töösüsteem on korraldatud. Saaksid sa mulle natuke infot anda? +assistant: Tere! Kesk-Eesti politseijaoskond Paides asub Tallinnas 12, postikoht 72720. Nad on avatud esmaspäevast reedeni, kell 9.00 kuni 17.00 igal tööpäeval.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud, et Tarbijakaitseamet saab tarbijaid aitada vaidluste lahendamisel. Kuidas see menetlus üldiselt toimib, kui ma esitan avalduse? +assistant: Tere! Meeldiv kuulata. Pärast avalduse esitamist, saame me sind teavitada 21 tööpäeva jooksul, et menetlus on käivitunud. Seejärel edastame avalduse kauplejale, et nad annaksid selgitusi ja pakkuksid lahendusi. Meiega saad ühendust ka selgituste kohta täiendavalt",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen uus ettevõtja ja tahan teada, milliseid seadusi pean arvestama, kui ma turule viin toote, mis on seotud ligipääsetavusega? +assistant: Tere! Peamiseks seaduseks on Toodete ja teenuste ligipääsetavuse seadus (LPS). Lisaks pead arvestama määrust nr 45, mis käsitleb ebaproportsionaalse koormuse hindamist, ja määrust nr 46, mis sätestab selle hindamise korra. Samuti on oluline tundida direktiivi I. lisa VII jaos ja harmoneeritud standardeid, millega seadus on seotud.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kas te saate öelda, kus Kuressaare politseijaoskond asub? +assistant: Muidugi! Kuressaare politseijaoskond asub Transvaali 58, Kuressaare. See on väga mugav, sest sealt on lihtne bussidega sinna jõuda.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Tahaks infot saada, kuidas lapse välismaalase passi taotleda? +assistant: Tere! Noh, põhiline on see, et lapse välismaalase passi taotlemine toimub teeninduses. Alustame kõigepealt sellega, et leiate sobiva teeninduskoja (nt PPA kontor). Ja siis on vaja isikut tõendava dokumendi, aga lapsele peab ju tohtivõti olema. Kas teil on küllagi selge, kus ja kuidas seda teenindust leida? Kas teadate, kus PPA kontorid asuvad?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin midagi ohtlike toodete kohta. Mis see tähendab ja kes sellega tegelda tohib? +assistant: Tere! See tähendab, et tegemist on toodetega, mis võivad tarbijale ohtu kujutada. Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) ja EL-i kiirhoiatussüsteem Safety Gate jälgivad ja teavitavad selliseid olukordi. Safety Gate abil vahetavad järelevalveasutused infot ohtlike kaupade kohta, et need võimalikult kiiresti turult eemaldada.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma soovisin teada, kus Ida-Harju politseijaoskond asub? Kas see on Tallinnas? +assistant: Jah, Ida-Harju politseijaoskond asub Tallinnas, aadressil P. Pinna 4, 13615 Tallinn.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma saan probleeme ID-kaardiga, aga ei ole kindel, kuidas diagnoosi faili genereerida. Kust ma peaksin alustama? +assistant: Tere! Üsna sagedasti juhtubki. Võime teiega sellega kindlasti aidata. Kõige mugavam on kirjutada ID-abile läbi id.ee veebis asuva lingi (https://www.id.ee/artikkel/teavita-meid-probleemist/). Kui tead, milline probleem tekkis, siis saad teada täpsemad juhised. Aga enne, kas tead, et teil on DigiDoc4 ja RIA DigiDoc rakendus ID-kaardi kasutamisele?",output_ID.ee +"user: Tere! Ma olen Euroopa Liidu kodanik ja tahan Eestisse elama tulla. Mis võimalused mul on selleks? +assistant: Tere! Eestisse saab tulla kolmel viisil: ajutine viibimine (kuni 3 kuud), tähtajaline elamine (kuni 5 aastat) või alaline elamine. Mis eesmärgiga ja kui pikaks ajaks sa Eestisse jääda soovid?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas ma saan uue ID-kaardi? Ja kust saan selle taotleda? +assistant: Tere! Jah, uut ID-kaarti saab kindlasti taotleda. Peaasi, et see on kõikidele Eesti kodanikele kohustuslik isikut tõendav dokument. ID-kaardi saab taotleda ainult Politsei- ja Piirivalveametist (PPA). Saada lisa infot nende kodulehel: https://www.politsei.ee/et/juhend/id-kaardi-taotlemine-taeiskasvanule",output_ID.ee +"user: Tere! Ma olen plaanimas pakettreisi ja ma ei ole päris kindel, mis see tähendab. Kuidas ma tean, kas ma olen broneerinud pakettreisi? +assistant: Tere! Pakettreis on tegelikult siis, kui reisikorraldaja on kokku pannud terve paketti reisiteenustest - näiteks lend, hotell ja ekskursioonid. Sa võid pidada seda pakettreisiks, kui leping on sõlmitud ühe müügikohaga ja sa oled nõus maksmise kohta enne maksmist. See kehtib ka siis, kui pakutakse ühtset hinda kõigile teenustele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvasin, et minu ID-kaart on kadunud. Mida ma peaks tegema? +assistant: Tere! See on murettekitav. Enne, siis mõtle kokku, kas olete seda äsja kasutanud ja kas seda võisite just hiljemalt, näiteks kontorilauale jätta. Kui te olete kindel, et kaardi varguses, siis on oluline teguda kohe. Mida edasi sõltub sellest, kui kindel olete kaardi varguses.",output_ID.ee +"user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järelevalve Ametil on kaart sideteenuste kohta. Mis see kaart täpselt näitab? +assistant: Tere! Jah, meil on sideteenuste kaart. See näitab, kus aadressil on võimalus interneti teenusega liituda. Roheline punkt tähendab, et liitumisvõimalus on olemas ja punane, et puudub.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin, et kui mul on probleem ostetud asjaga, saan pöörduda Tarbijakaitse Ametisse. Kust ma alustan? +assistant: Tere! See on õige. Enne Tarbijavaidluste komisjoni pöördumist on oluline, et sa esitada pretensioon otse kauplejale. Nad peavad sellele vastama 15 päeva jooksul. Alles kui kaupleja ei vasta või te ei jõua kokkuleppele, saab komisjoni pöörduda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et ID-kaart on väärt uurida. Kas sa võiksite mulle öelda, mis see üldse on ja milleks see on? +assistant: Muidugi! ID-kaart on Eesti kodaniku ja Eestis püsivalt elava Euroopa Liidu kodaniku jaoks kohustuslik isikut tõendav dokument. See on digitaalne dokument, mis on ka võbilauaõigus, st seda saab kasutada reisidel passi asemel. Kas sa tahad rohkem teada selle kasutusvõimaluste kohta?",output_ID.ee +"user: Tere! Mul on probleeme ID-kaardiga. Kas teil on võimalik aidata? +assistant: Tere! Muidugi aitame. Kõigepealt, palun kirjuta id.ee veebilehele ja „Kirjuta ID-abile“. Seal saad oma probleemist kirjalikult selgitada. See aitab meil sind paremini aidata.",output_ID.ee +"user: Tere! Ma olen kuulnud, et pürotehnika kasutamisel on olulisi reegleid. Mis kõige olulisem, mida ma peaksin teadma? +assistant: Tere! Kõigepealt oluline: F4 ja T2 kategooria pürotehnikat tohib kasutada ainult pürotehnik ise. Samuti on oluline, et saaksite toote ohutult kasutada, seega ostate pürotehnikat ainult ametlikust müügikohast, mitte kellegi käest tänavalt.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus laste mänguasjade ostmise kohta. Mis on kõige olulisem asi, millele peaksin tähelepanu pöörama, et osta ohutu mänguasi? +assistant: Tere! Kõige olulisem on vaadata, kas mänguasi kannab CE-märgist. See märgistus peaks tagama, et mänguasi vastab nõuetele. Aga see ei ole kõik – loe ka müügipakendi soovitusi ja hoiatusi!",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on lend ees ja tahan teada, mida ma peaksin tegema, kui lend hiljub. Mis on mu õigused? +assistant: Tere! Kui lend hiljub, on sul mitmeid õigusi. Need sõltuvad aga sellest, kas lend väljus Eestist või saabus siia kolmandast riigist. Mis riigist su lend väljub?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et digiallkirjastamine on üsna oluline, aga ei suuda päris aru saada, kuidas seda enda e-teenuses realiseerida. Kas saate öelda, milliseid võimalusi on olemas? +assistant: Tere! Muidugi. Digiallkirjastamise realiseerimiseks on mitu võimalikku lahendust, mis sõtuvad valitsusektori ja erasektori lahendustega. Valitsusektori asutused saavad kasutada Riigi allkirjastamisteenust (https://www.ria.ee/riigi-infosusteem/elektrooniline-identiteet-ja-usaldusteenused/digiallkirja-serverteenused) . See pakub ID-kaardi, mobiil-ID, Smart-ID ja e-Templiga digiallkirjastamise tuge. Samuti on olemas mitmed teed, mida saate kasutada erasektoris. Kas teiega oleks huvitav ka uurida erasektori lahendusi?",output_ID.ee +"user: Tere! Ma lugesin midagi Tarbijakaitse Ameti veebilehel ehitiste ohutuse kohta. Mis see tähendab täpselt? +assistant: Tere! Ehitiste ohutus tähendab seda, et ehitis peab olema vastavuses seadusaktide nõuetele ja kasutatav ohutult. See hõlmab mitmeid aspekte nagu mehaaniline vastupidavus, tuleohutus, hügieen, juurdepääsetavus puudega inimestele ja palju muud. Nõuded tulenevad seadusest, ehitise kasutusjuhendist või heast tavast.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on tähtajaline elamisluba töötamiseks Eestis ja see aegub peagi. Kuidas ma saan seda pikendada? +assistant: Tere! Elamisluba töötamiseks saab pikendada. Vähemalt kaks kuud enne loa lõppemist pead sa pikendamise taotluse esitama. Seejärel esita nõutud dokumendid ja tasu riigilõiv. Saad vastuse hiljemalt 10 päeva enne kehtiva loa lõppemist, kui taotlus on õigeaegselt esitatud.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud midagi ebaausate müügivõtete kohta. Mis see tähendab ja miks on see probleemne? +assistant: Tere! Ebaausad müügivõtted on sellised müügitaktikad, mis püüavad sind tarbijana eksitada või survestavad sind ostma. Need on Euroopa Liidus keelatud, sest nad kahjustavad tarbija õigusi ja teevad tehingu keeruliseks ja ebavõrdseks. Näiteks võib müüja anda valeväiteid toote kohta või üritada sind piirata valikuvabaduses.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kaalun seiklusparki minekut. Mida ma peaksin teadma? +assistant: Tere! Seikluspark on tore valik! Enne kui minna, on oluline tutvuda reeglite ja piirangutega, et kogemus positiivne oleks. Kas Sa soovid, et ma rääkiks reeglite ja piirangute kohta või millest muust Sa huvitatud oled?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma saan nüüd ID-kaardi ja Digi-ID, siis saan ka koodiümbriku? Ja mida see koodiümbrik on? +assistant: Tere! Jah, kui teil on ID-kaart või Digi-ID, siis saate ka koodiümbriku. See on kindlik koodiümbrik, mis sisaldab PIN- ja PUK-koodi. Need koodid on vajalikud, et saaksite ID-kaardi ja Digi-ID elektrooniliselt kasutada. See on nagu turvafirma sild, mis võimaldab kaardi turvaliselt kasutada.",output_ID.ee +"user: Tere! Olen Ukraina kodanik ja kardustan tagasipöördumist sinna sõja tõttu. Kas ma saan Eestis taotleda rahvusvahelist kaitset? +assistant: Tere! Jah, sa saad. Rahvusvahelist kaitset võivad taotleda välismaalased, sh Ukraina kodanikud, kes kardavad kodumaal tagakiusamist või kellele ähvardab ohtu tagasipöördumine seoses relvakonfliktiga. See kehtib, kui kardad näiteks surmanuhtlust, piinamist või inimväärikust alandavat kohtlemist.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin Rail Balticu kohta palju, aga ma ei tea, kuidas selle keskkonnamõju kohta infot leida. Kas Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) sellega tegeleb? +assistant: Jah, TTJA tegeleb Rail Balticu keskkonnamõjude hindamisega (KMH). Kõik avalikud teated selle kohta on leitavad TTJA veebilehel, aadressil https://ttja.ee/eraklient/tarbija-oigused/avalikud-teated/keskkonnamoju-hindamisega-seotud-teated.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin midagi raadiosageduste kohta, aga ei saa pähe. Kes need Eestis siis reguleerib? +assistant: Tere! Raadiosageduste kasutamist Eestis reguleerib Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA). Aga Euroopa tasandil on selleks EFIS (EROFrequency Information System).",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma ei taju nii palju, mida need sertifikaadid tähtsuses on. Kas need on nagu minu ID-kaardi versioon internetis? +assistant: Tere! Jah, seda võid nimetada nii. Sertifikaadid on nagu ID-kaardi digitaalne versioon. Nad aitavad sinu identiteeti veebis tõestada. Kui sul on ID-kaart, digi-ID, mobiil-ID või Smart-ID, siis sul on ka nendega seotud sertifikaadid. Need on olulised, et sa võiksid turvaliselt e-teenuseid kasutada.",output_ID.ee +"user: Tere! Kas sa võiksite mulle öelda, mitu Ukraina kodanikku on Eestisse alates 27. veebruarist saabunud? +assistant: Muidugi! Eestisse on alates 27. veebruarist saabunud umbes 23 886 Ukraina kodanikku ja nende pereliiget.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud palju CE-märgist räägitakse, aga ei tea täpselt, mis see tähendab. Mis on CE-märgise üldpõhimõtted? +assistant: Tere! CE-märgise põhimõtted on üsna lihtsad. Põhimõtteliselt kinnitab CE-märgis, et tootja või volitatud esindaja garanteerib, et toode vastab ELi ühtlustatud nõuetele. See tähendab, et toode on ohutu ja vastab kehtivatele standarditele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma mõtlesin alustada mobiil-ID kasutamist, aga ei tea päris midagi selle kohta. Kas seda võtab vana ID-kaardist palju rohkem aega? +assistant: Tere! Väga tore, et huvi on! Ei võta reaalselt nii palju aega, kui tead, kuidas see töötab. Mobiil-ID kasutamine on palju mugavam, sest ei pea kaarti kaasas kanda. Kui vana ID-kaardi puhul on tähtaeg kaarti kanda, siis mobiil-ID on alati kaasas telefoni sees. Kas sa tahaksid teada, milliseid seadmeid sellega kasutada?",output_ID.ee +"user: Tere! Kas sa oskad rääguda mobiil-ID-st? Ma pole sellest midagi kuulnud. +assistant: Tere! Jah, ma oskan. Mobiil-ID on digitaalne identifikaator, mis võimaldab sul kasutada oma nutitelefoniga e-teenuseid, nagu näiteks pangaülekanded või võrreldamatud kohvrid. See on nagu digitaalne võti! Kas sa tahad teada, kuidas see töötab?",output_ID.ee +"user: Tere! Ma olen veemõju mootorpaadi omanik ja ma ei ole kindel, kas mul on vaja raadioluba. Kust ma peaksin alustama? +assistant: Tere! Veemõju mootorpaadi puhul on oluline teada, et kui paat on siseveekogudel ja üle 12 meetri pikkusega, või reisilaev, siis peab see olema varustatud meresagedusalas töötavate raadioside seadmetega. See tähendab, et teil on tõenäoliselt vaja veesõiduki raadioluba. Saate taotluse esitada JVISi kaudu või alla laadida vorm TTJA lehelt ja saata see meile e-posti aadressile info@ttja.ee.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma tahan teada, mis on Tarbijakaitse ja Tehnilise Järelevalve Ameti (TTJA) roll avalike mänguväljakute osas. Mis on nende peamised kohustused? +assistant: Tere! TTJA peab silmas avalike mänguväljakute puhul, et nende rajamine ja rekonstrueerimine on ehitusloa- ja projektikohustuslik tegevus. Lisaks on omanik vastutav mänguväljaku tehnilise korrasoleku ja ohutuse eest. See tähendab, et nad peavad lähtuma heast ehitustavast ja tagama seadmete ohutuse kasutamisel.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin, et Eestis on muutusi tele- ja raadioringhäälingus käsil. Saaksid sa selle kohta midagi rääkida? +assistant: Tere! Jah, muutusi on. Alustame ehk raadioraadioga: kui sa tahad Eestis raadioraadioraadiot teha, pead esmalt taotlema meediateenuse tegevusluba TTJA-st. See on esmane samm. Sa leidmaks rohkem infot, vaata https://ttja.ee/ariklient/ametist/load-ja-riigiloivud/meediateenuste-tegevusluba. Tahad midagi raadioraadioga seoses veel teada?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma just kuulsin midagi numbriloast Tarbijakaitse ja Tehnilise Järelevalve Ameti poolt. Mis see täpselt on? +assistant: Tere! See tähendab, et TTJA väljastab numbriload sideteenuse osutajatele – sideettevõtjatele ja ka nendele, kes pakuvad muud teenust, kasutades sidevõrku. Näiteks, kui sa soovid mobiilnumbriga tegutseda, peab sinu sideettevõtjal olema selleks TTJA numbriluba.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et mul on probleeme, kuidas sisse logida erinevate riiklike e-teenustesse. Kas ma pean iga kord uuesti sisse logima? +assistant: Tere! Päriselt, see on levinud küsimus. Jah, varem oli nii, et iga kord pead sisse logima. Aga nüüd on võimalik kasutada ühte sisselogimist, mis aitab sind säästida. See on võimalik läbi Riigi autentimisteenuse.",output_ID.ee +"user: Tere! Ma nägin Politsei- ja Piirivalveameti veebilehel mingit ligipääsetavuse teatis. Mis see tähendab? +assistant: Tere! See teatis on seadusest tulenev avaldus, mis kinnitab, et Politsei- ja Piirivalveamet püüab oma veebilehte www.politsei.ee (https://www.politsei.ee/) muuta ligipääsetavaks kõigile, kaasa arvatud inimestele, kel on puueteid. See teatis ütleb ka, mis on selle hetke olukord ligipääsetavuse osas.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin midagi digiligipääsetavusest Tarbijakaitse ja Tehnilise Järevalve Ameti poolt. Mis see tähendab? +assistant: Tere! Digiligipääsetavus tähendab, et digitaalsetel sisu ja teenustel peab olema kättesaadav kõikidele inimestele, kaasa arvatud inimestele, kel on puueteid. Tarbijakaitse ja Tehnilise Järevalve Amet (TTJA) tegeleb sellega, et avaliku sektori teenused oleksid digitaalselt kättesaadavad.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et on probleeme ID-kaardi kasutamisega. Kuidas saan abi? +assistant: Tere! Päev vastu. Kui sul on probleeme ID-kaardi või mobiil-ID-ga, on ID.ee veebist hea abi leida. Palju informatsiooni ja vastuseid seal leiad: id.ee. Sa võid ka ID-abikeskusega ühendust võtta läbi nende veebiformi, et saada personaalsemat abi.",output_ID.ee +"user: Tere! Olen ajakirjanik ja tahan teada, kuidas Politsei- ja Piirivalveameti pressilisti liikmeks saada. Kas see on võimalik? +assistant: Tere! Jah, on võimalik. Politsei- ja Piirivalveamet jagab pressilistidele regulaarselt infot. Kui soovid liikmeks saada, saad kirjutada soovi aadressil, mis on antud ametis, ning märkida oma andmed: nimi, e-posti aadress ja väljaande nimi.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma elan välismaal ja tahan teada, kuidas ma saan elamisloakaarti taotleda. Kas see on üldse võimalik? +assistant: Tere! Jah, see on võimalik, aga erandkorras. Sa saad elamisloakaarti taotleda välismaal, kui sa ei ole Euroopa Liidu liikmesriigi kodanik, sul on kehtiv Eesti elamisluba või elamisõigus ja sinu välismaa elukoht on registreeritud rahvastikuregistris. Kas sa vastad nendele tingimustele?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma tegelikult ei mäleta, kuidas Politsei- ja Piirivalveameti lipud välja näevad. Saaksid sa mulle natuke rääkida? +assistant: Tere! Muidugi. Politsei lipu põhiline värv on sinine, mis on ka meie riigilipuvärv ja sümboliseerib rahu ja stabiilsust. Sellel on valge püstitõusnud lõvi, kes hoiab käes väikest Eesti riigivappi. Lipu mõõdud on 105x135 cm ja see on ääristatud kuldsete narmastega.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen plaanimas pakettreisi tellida. Mis asi üldse tähendab ""pakettreis""? +assistant: Tere! Pakettreis tähendab, et reisikorraldaja on pannud kokku terve komplekti reisiteenuseid – näiteks lennud, majutus, toitlustus ja ekskursioonid. Sa saad selle paketi ühe lepinguga tellida. See võib tähendada nii ühte lepingut või ka eraldi lepinguid, kui täidad teatud tingimused.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet on tellinud uuringuid. Mis neid puudutab? +assistant: Tere! Jah, neid on mitmeid. Üks olulisemaid on uuring, mis annab hinnangu 1 Gbit/s kiirust võimaldava passiivse lairibataristu rajamiseks maapiirkondadesse. Samuti on tehtud uuring Eesti elanike teadlikkusest tarbijate õigustest ja veebiküsitlus nende käitumisest probleemide korral.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma soovin digiallkirja anda ID.ee rakenduses. Kuidas ma sellega alustan? +assistant: Tere! Digiallkirja andmine on päris lihtne. Alustada saate kahel viisil: kas ID-kaardi või mobiil-ID abil. Kõigepealt pead tegema kindlaks, millise meetodi te kasutate. Kas teil on ID-kaart või mobiil-ID?",output_ID.ee +"user: Tere! Ma saan probleeme ID-kaardi kasutamisega. Kas teil oleks võimalik öelda, kuidas veenduda, et mu kaardilugeja töökorras on? +assistant: Tere! Päevas! On hea, et sa pöördud abi pidi. Jah, kindlasti! Esimese sammuna on see, et vajad jääd kontrollima, et ID-kaardi ja kaardilugeja on täielikult töökorras. Enne kaardi kasutamist peaks arvutis olema ID-tarkvara paigaldatud (https://www.id.ee/artikkel/paigalda-id-tarkvara). Kas sa oled kindel, et ID-tarkvara on paigaldatud ja ID-kaart on kaardilugejas?",output_ID.ee +"user: Tere! Kuulisin midagi ID-kaartide sertifikaatide kohta. Mis asi see tähendab ja miks see oluline on? +assistant: Tere! Õige, alates 1. aprillist 2018 tunnistati turvanõrkusega ID-kaardi sertifikaadid kehtetuks. See tähendab, et kui sa ei uuendanud oma ID-kaardi sertifikaate 31. märtsiks 2018, siis elektrooniliselt ei saa seda enam kasutada nii nagu varem.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et Politsei- ja Piirivalveameti koduleht on tehtud juurdepääsetavaks. Mis see täpsemalt tähendab? +assistant: Tere! See tähendab, et leht on ehitatud nii, et see vastaks WCAG 2.1 AA suunistele. See tähendab, et oleme kasutanud nii tehnilisi vahendeid kui ka sisu koostamise põhimõtteid, mis aitavad puuetega inimestel kodulehte kasutada. Näiteks pimesed, vaegnägevad, kurtud, vaegkuulvad, inimesed õpiraskustega või kognitiivsete piirangutega – kõik need saavad lehte paremini kasutada.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen Euroopa Liidu kodanik ja plaanin Eestisse kolida. Mis võimalusi mul on siin elamiseks? +assistant: Tere! Euroopa Liidu kodanikuna on sul Eestis kolm võimalust: ajutine viibimine kuni 3 kuud, tähtajaline elamine kuni 5 aastat ja alaline elamine. Millist varianti sa silmas pead?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et saab välismaalase passi taotleda. Mis see täpselt on? +assistant: Tere! Välismaalase pass on Eesti Vabariigi poolt välismaalasele väljastatav reisidokument. See ongi selline pass, mida sa kasutad välismaal kui sul on kehtiv elamisluba või elamisõigus Eestis.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen uudishimulik, kuidas Eesti kodakondsus lapsele antakse. Saabus mulle kiri selle kohta, aga ma ei tea, mida edasi teha. +assistant: Tere! Hästi, siis sa oled saanud kirja selle kohta. Kui lapsele Eesti kodakondsus antakse, siis tavaliselt saadakse selle kohta teada posti teel saadetud kodakondsustunnistuse näol.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud, et kui lend hiljub, on reisijatel teatud õigused. Kuidas ma saan teada, mida need õigused on? +assistant: Tere! See on õige. Euroopa Liidu lennureisijate õigused on määratletud kindlas määruses. Üks viis on tutvuda Euroopa Liidu tarbija nõustamiskeskuse veebilehega, kust leiad infot: [https://consumer.ee/](https://consumer.ee/). Samuti on saadaval Euroopa Liidu veebileht, kus on selgelt kirjeldatud reisijaõigused: [https://europa.eu/youreurope/citizens/travel/passenger-rights/air/index_et.htm](https://europa.eu/youreurope/citizens/travel/passenger-rights/air/index_et.htm). Mis täpselt sind kõige rohkem paksub?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma lähen Eestisse ja tahan lapsele passi taotleda. Kuidas ma riigilõivu maksta saan? +assistant: Tere! Passi taotlemise riigilõivu maksmiseks on mitmeid võimalusi. Saaja on Rahandusministeerium ja viitenumber on 2900082443. Pangad, kust saad maksta, on SEB (EE891010220034796011), Swedbank (EE932200221023778606), Luminor Bank (EE701700017001577198), LHV Pank (EE777700771003813400).",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma arvan, et digitaalsed allkirjad on kirsilehe otsas. Kas saaksid sa mulle selgitada, mis see täpselt tähendab ja kuidas nad erinevad tavalisest allkirjastamisest? +assistant: Tere! Väga hea küsimus. Digitaalse allkirjastamise tähendus on see, et sa saad dokumenti paberivaba allkirjastada. Selleks ei ole vaja dokumendi välja trükkida. Selleks kasutatakse digitaalset isikutuvastusvahendit, nagu ID-kaart. Aga oluline on see, et kõik digitaalsed allkirjad ei ole seaduslikult kehtivad nagu omakäelised allkirjad. Mis täpselt teeb digiallkiri seaduslikult kehtivaks? See on, et kasutaja isik ja sertifikaadi andja taust on kontrollitud, ning allkirja andmise aega on kindlalt fikseeritud.",output_ID.ee +"user: Tere! Olen kuulnud, et Eesti piiri juures on käimas ehitustööd. Mis seal täpselt ehitatakse? +assistant: Tere! Jah, seda on tõesti käimas. Politsei- ja Piirivalveamet (PPA) on käivutanud piiriehituse hanked. Esimene etapp keskendus 23,5 kilomeetri pikkusel piirilõigul Võrumaal, alates kolmikpunktist kuni Luhamaa piiripunktini. Teine etapp on samuti käimas, see hõlmab 39,5 kilomeetrit Tserebi ja Võmmorski külade vahel.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen plaaninud pakettreisi, aga ma ei tea päris täpselt, mida see tähendab. Kuidas ma aru saan, kas mu reis on pakettreis? +assistant: Tere! Pakettreis on siis, kui reisikorraldaja on kokku pannud reisiteenuste kogumi. See tähendab, et sa tegelikult ostsid ühe paketti, mitte eraldi lennupileti ja hotelli. Aga on ka juhtumisi, kus eraldi lepinguid on sõlmitud, ent see on ikkagi pakettreis, kui sa näiteks valisid kõik teenused ühest kohast ja maksad ühtse hinna eest. Kas Sa soovid, et ma selgitan seda täpsemalt? ",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kus ma leian infot liiklusõnnetuste kohta, kus keegi viga sai? +assistant: Tere! Politsei- ja Piirivalveameti lehel on ülevõtte infoga liiklusõnnetuste kohta, kus on registreeritud inimkannatanuid.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on probleeme ID-kaardi kasutamisega. Mida ma võin ette võtta? +assistant: Tere! Ma saan sind aidata. Esmapilgub võta mõned lihtsad küsimused, et paremini aru saada, mis toimub. Kas sa kasutad ID-kaarti esimest korda, või on probleem tekkinud varem? Ja millist ID-kaarti sa kasutad (nt. mille tootja ja mudel)? ",output_ID.ee +"user: Tere! Ma kuulsin, et on kelmidega ohvriks sattumisel, kuhu pöörduda. Mis on kõige tähtsam asi, mida ma kohe teha peaks, kui kahtlustan, et olen kelmidele raha saatnud? +assistant: Tere! Kõige olulisem asi on kohe oma pangaga ühendust võtta. Nad saavad sul pangamakse peatada või tagastada, kui see veel võimalik on. Lisainfot leiad näiteks Swedpanki, SEB Panga, LHV ja Luminori veebisaitidelt.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulen, et Tarbijavaidluste komisjon aitab lahendada probleeme kauplustega. Kuidas see toimib ja kas see on kergem ja odavam kui kohtuasse minek? +assistant: Tere! Jah, see on õige. Tarbijavaidluste komisjon pakubki lihtsamalt, mugavamalt ja kiiremini lahendusi kui kohtuasse minek. Komisjonis saab probleemi lahendada tunduvalt vähem aega ja raha kulutades. Vaidluse lahendamine on nii tarbija kui ka ettevõtja jaoks tasuta ja üldjuhul jõutakse tulemusele umbes 90 päeva jooksul alates menetluse algatamisest.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et olen kuuldud digiallkirjadest, aga ma ei ole kindel, mida need täpsemalt tähendavad ja kuidas need minu e-teenuses kasutamiseks soetada? +assistant: Tere! Palju maas! Digiallkirjad on oluline osa e-teenuste turvalisusest. Neil on võti sellele, et dokumendid ja tehingud on autentse ja usaldusväärse, ning neid ei saa muuta ilma, et teist ei teatsidaks. Paljuski seega on need olulised selleks, et mu e-teenuses toimuvatest asjadest oleks kindel, et on autentse. Arvestades seda, saame rääkida selle kohta rohkem, aga kõigepealt saame aru, kuidas neid üldjuhul saame teostada. Kõige tavalisemad viisid on ID-kaardiga, mobiil-ID või Smart-ID. Kas sa tahaksid teada ühe neist põhjalikumalt?",output_ID.ee +"user: Tere! Ma olen kuulnud, et Tarbijakaitseamet järelleb isikukaitsevahendite ohutust. Mis see tähendab täpsemalt? +assistant: Tere! See tähendab, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TJAT) järelleb, et isikukaitsevahendid vastaksid ohutusnõuetele. Peamiseks seadusandluseks on Euroopa Parlamendi ja Nõukogu määrus (EL) 2016/425.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma sain uue ID-kaardi. Mis on see koodiümbrik, mille kaasa tuli? Kas ma pean seda aktiveerima? +assistant: Tere! Päevad! Uue ID-kaardi puhul sa saad kaasa koodiümbriku, mille sees on PIN1, PIN2 ja PUK koodid. Seda ei pea eraldi aktiveerima, see töötab koheselt.",output_ID.ee +"user: Tere! Ma kuulsin, et Ukrainast saab rahvusvahelist kaitset Eestis taotlema. Kuidas see täpselt töötab? +assistant: Tere! Jah, see on õige. Rahvusvahelist kaitset saab taotlema igaüks, kes kardab Ukrainas tagakiusamist, näiteks rassi, usu või poliitiliste vaadete tõttu. Samuti, kui tagasipöördumine Ukrainasse kujutab endast tõsist ohtu, näiteks surmanuhtluse, piinamise või vägivalla tõttu relvakonflikti tõttu. Mis sind selle osas kõige rohkem huvitab?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma ei saa aru, miks mul ID-kaardiga probleemid on, kui ma internetis teenuste eest sisenen. Mis ma peaks uurima? +assistant: Tere! Olgu. See on üsna tavaline probleem. Esimese sammuks on kindlasti kontrollida, et ID-kaardi ja kaardilugeja on korralikult ühendatud ja töökorras. Kas olete kaardilugeja arvutiga USB-kaabelga ühendanud, ja kui olete, on kaabel korralikult arvutiga luku?",output_ID.ee +"user: Tere! Ma olen Põlva maast ja tahan teada, kuidas saaksin passi taotleda. Mis on kõige lihtsam viis? +assistant: Tere! Kõige turvalisem ja kiireim viis dokumendi taotlemiseks on PPA iseteenindus veebilehel: https://www.politsei.ee/et/iseteenindus. Seal saad kogu protsessi algatada. Kas sa soovid ka digikioski kasutamist kaaluda?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Olen kuulnud Tarbijakaitse Ameti sidekaardist. Mis see kaart täpselt kujutab? +assistant: Tere! Sidekaart näitab, kus on võimalik interneti teenusega liituda. See põhineb sideoperaatorite pakutavatel hindakirjades olevatel andmetel. Roheline punkt tähendab, et saadaval on liitumisvõimalus teatud kiirusega, punane punkt aga, et sellist võimalust praegu pole.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen kuulnud, et raudtee ületamisel on oluline olla eriti tähelepanelik. Mis on kõige olulisem asi, mida ma peaksin meeles pidama, kui ma ületan raudteed autoga? +assistant: Tere! See on väga hea küsimus. Kõigepealt veendu, et sa ei lähene raudteeülesõidukohale, kui raudteesõiduk juba läheneb. See tähendab, et vaata ja kuula hoolikalt. Ja alati anna lähenevale raudteesõidukile teed. Ära ületa foori keelava tulega või suletud tõkkepuu korral.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mis on Web eID? +assistant: Tere! Web eID on uus süsteem, mis võimaldab teid veebis turvaliselt identifitseerida ja oma dokumendid allkirjastada. See tähendab, et saate kasutada oma ID-kaarti või digi-kaarti veebis, ilma seda kaardi enda eemardamata. See on uus arhitektuur selleks, et tagada parem turvalisus.",output_ID.ee +"user: Tere! Ma ei ole väga tuttav e-hääletamisega. Kas te võite mulle öelda, mis see täpselt on? +assistant: Muidugi! E-hääletamine on olukord, kus sa saad hääleda valimistel interneti kaudu, ilma kodust lahkumata. See on väga erandlik asi, et Eesti on esimene riik maailmas, kus see üleriigiliselt valimistel kasutusele võeti! See on Interneti kaudu toimiv hääletamine, mis erineb traditsioonilisest hääletasest.",output_ID.ee +"user: Tere! Ma kuulsin midagi hoonete energiatõhususe kohta, aga ma ei saa päriselt aru, mis see tähendab ja kes sellega tegelevad Eestis? Kas Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) on selles rollis? +assistant: Tere! Jah, Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) tegelikult on seotud hoonete energiatõhususega. Nad järelveevad, et ehitised vastaksid energiatõhususe nõuetele. Põhimõtteliselt tähendab hoonete energiatõhusus seda, et ehitised kasutaksid vähem energiat soojendamiseks, jahutamiseks ja valgustamiseks, samal ajal kui need tagaksid mugava elukeskkonna.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus Mobiil-ID kohta. Kas ma saan teada, kui kaua need sertifikaadid kehtivad? +assistant: Tere! Jah, Mobiil-ID sertifikaadid kehtivad 5 aastat. Kuidas ma saan teada, kas teie enda sertifikaat on aktiivne?",output_ID.ee +"user: Tere! Ma saan aru, et teete asju DigiDociga. Kas teil on infot, milliseid failivorme DigiDoc konteinerid toetavad, eri tarkvaraversioonides? +assistant: Tere! Jah, seda küsimegi. ID-tarkvaras toetatud DigiDoc konteineri formaadid sõltuvad tarkvaraversioonist. Peamised on olnud DDOC, BDOC ja ka mõned vanemad versioonid. Kuidas ma saan sind paremini aidata? Kas sa oled huvitatud mõne konkreetse versiooni kohta?",output_ID.ee +"user: Tere! Ma olen huvitatud amatöörraadiosidest. Kuidas ma saan loa raadiojaama töötamiseks? +assistant: Tere! Selleks peab esitama taotluse Tarbijakaitse ja Tehnilise Järelevalve Ametile (TTJA). Esiteks, on oluline teada, millist tüüpi raadiojaam teil on - kas see on isiklikuks kasutuseks, ühiskasutusraadiojaam, automaatne majakas või ehk raadioorienteerumise majakakompleks?",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma tahan teada, kuidas toimib lapsele Eesti kodakondsuse taotluse menetlus. Mis juhtub pärast taotluse esitamist? +assistant: Tere! Pärast taotluse esitamist edastatakse kõik dokumendid kolme kuu jooksul Vabariigi Valitsusele. Valitsus teeb seejärel otsuse, kas kodakondsus lapsele antakse.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Sa võid mulle teada, milliseid andmeid DigiDoc4 rakendus kasutab ja kes neile andmeid edastab? +assistant: Tere! Loomulikult. DigiDoc4 rakendus kasutab mitmesuguseid andmeid, sealhulja sinu isikukoodi, telefoni numbrit ja aadressi. Need andmed edastatakse erinevate kolleegidele, näiteks Riigi Infosüsteemi Ametile (RIA), SK ID Solutionsi ja ka muudele partneritele, kes osalvad e-allkirjastamisprotsessis. Küll aga on oluline meeles pidada, et RIA kasutab neid andmeid ainult e-allkirjastamiseks ja andmete kontrollimiseks.",output_ID.ee +"user: Tere! Mul on küsimus. Kuidas Eestis raadiosageduste kasutamist reguleeritakse? +assistant: Tere! Raadiosageduste kasutamist Eestis reguleerib seadus, mis tugineb Euroopa Liidu standarditele. Konkreetsemat infot selle kohta saab EFIS andmebaasist, mis on osa Euroopa Raadiosageduste Informatsiooni Süsteemist (EROFrequency Information System).",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Olen kuulnud Safety Gate'ist, aga ei tea, mis see täpselt on. Saaksid natuke selgitada? +assistant: Tere! Safety Gate on ELi kiire teabevahetussüsteem, mille kaudu edastatakse infot ohtlike toodete kohta. Selle abil saab ennetada ja piirata ohtlike toodete jõudmist ELi turule ning tagada, et liikmesriigid jälgiksid toodete ohutust ühtemoodi. See aitab meil teada, milliseid tooteid tuleks vältida ja milliseid kontrollida.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma ei saa oma ID-kaardi PIN-koodi sisse sisestada. Mida peaks tegema? +assistant: Tere! Kui PIN-kood on kolm korda järjest valesti sisestatud, siis ID-kaart selle lukustab turvalisuse huvides. Palju ei parda, see on üsna levinum probleem. Sa saad PIN-koodi ise kiiresti lahti blokeerida – ei tarvitse PUK-koodi kasutada! Kas sul on PUK-koodi olemas, või sooviksime teada, kas sul seda pole?",output_ID.ee +"user: Tere! Ma olen kuulnud, et avalikku koosolekut tuleb politseist registreerida. Mis täpselt see avalik koosolek on? +assistant: Tere! Avalik koosolek on inimeste kogunemine avalikus kohas ühise eesmärgiga, näiteks meeliste väljendamiseks või mõne asja kujundamiseks. See võib olla näiteks väljaku-, pargi-, tee- või tänavakorraldatav kohtumine, demonstratsioon, miiting, pikett, religioosne üritus või rongkäik.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma mõtlesin, et saan nüüd digi-ID kaardi? Kas see on ID-kaardi asendaja? +assistant: Tere! Jah, digi-ID on mõeldud ID-kaardi asendajaks elektroonilises keskkonnas. See on digitaalne dokument, millega saab isikut tõendada ja allkirja anda. See on oluline, et olla turvalisem ja mugavam elektroonilistes teenustes.",output_ID.ee +"user: Tere! Ma saan aru, et see on Politsei- ja Piirivalveameti kontaktinfo? Mis on PPA registrikood? +assistant: Jah, õige! PPA registrikood on 70008747. See on oluline number ametniku jaoks, kui sa seda näiteks vormistama pead.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulin, et Politsei- ja Piirivalveamet haldab piiripunkte. Mis see tähendab tegelikult? +assistant: Tere! See tähendab, et amet vastutab piiripunktide korralduse eest. Näiteks, piiripunkti juures ei ole alaliselt politseinikke, vaid nad on kohal väljakutsel piirikontrolli teostamiseks.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Kas te oskate öelda, kus Kuressaare politseijaoskond asub? +assistant: Tere! Kuressaare politseijaoskond asub aadressil Transvaali 58, Kuressaare.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma lugesin midagi liiklusvoodu kohta, aga olen natuke segadus. Kas sa saaksid mulle selgitada, mis on liiklusvoolu piiramine ELis? +assistant: Tere! Liiklusvoolu piiramine ELis tähendab, et Euroopa Liidus ei tohi arveldada kõnede ja SMS-ide eest erinevalt riikide kaupa. Näiteks, kui te teete kõne teise EL riigi numbril, siis arvelduse summa ei tohiks olla suurem kui kui te oleksite kõnet teinud oma koduriigis. Täpsemalt, kõneminuti kohta 0,19/0,228 eurot, SMS-i kohta 0,06/0,072 eurot (ilma või koos käibemaksuta).",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen liikumisvõimetu ja tahaksin teada, kuidas ma ID-kaarti taotleda saan. Kas mul peab sõrmejäljed andma? +assistant: Tere! Küsimus on hea. Alates 12. eluaastast on ID-kaardi taotlemiseks vaja sõrmejäljed. Kui te olete üle 70-aastane ja teilt on varem sõrmejäljed võetud, siis uut dokumenti saate taotleda ilma uusi sõrmejälgi andmata. Muudel juhtudel, kui sõrmejäljed on vähem kui 11 aastat tagasi hõivatud, ka siis uusi anda ei pea. Kui sõrmejälgede andmine on vajalik, aga te ei saa ise teenindusse tulla, saadame sõrmejälgede hõivamiseks teeninduse teie juurde. Saate meile selleks taotlus ja arstitõendi saata.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen huvitatud, kuidas jäätmed Eestis käsitletakse. Mis on kõige olulisem asi, mida ma selle kohta teada peaks? +assistant: Tere! Jäätmehoolduse korraldamine on kohaliku omavalitsuse ülesanne. Oluline on teada, et kui elad elanikkudega omavalitsuses, pead sa ise jäätmevedaja valima või viima jäätmed prügilasse/jäätmejaama. Elanikkudega omavalitsused peavad korraldama jäätmeveokonkurssi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kuulin midagi piirangute kohta Venemaa kodanikele Eestisse sisenemisel. Kas see on ikka tõene? +assistant: Jah, see on tõene. Alates 2022. aasta 19. septembrist on kehtinud piirangud Eestisse sisenemisel Venemaa Föderatsiooni kodanikele, kel on kehtiv lühiajaline Schengeni viisa. See on seotud Vabariigi Valitsuse korraldusega.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Mul on küsimus. Ostsin hiljuti veidrikas eseme ja see ei tööta nii, nagu peaks. Mis on mu õigused kui see on defektne? +assistant: Tere! Oluline on teada, et tarbija õigus on nõuda kauplejalt esmalt tasuta parandamist või asendamist. See, kumb variant on parem, lepitakse teie vahel kokku. Kuid reeglina otsustab müüja ise, kas asendada toode uuega või parandada seda. See kehtib, kui müüjal ei ole võimalik kaupa parandada või asendada, või kui parandamine/asendamine ei õnnestu, või müüja ei kõrvalda defekti mõistlikus ajas, või kui teile tekitatakse põhjendamatuid ebamugavusi.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Mul on küsimus TTJA otsuste kohta. Mis saab, kui ma ei nõustu TTJA tehtud otsusega? +assistant: Tere! Kui te ei nõustu TTJA tehtud otsusega, on teil kaks võimalust. Esiteks, saate esitada vaide TTJA peadirektorile. Teiseks, saate esitada kaebuse Tallinna Halduskohtule.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma ei saa oma ID-kaardi PIN-koodi lahti blokeerida. Mis ma peaks tegema? Kas see on kurb asi? +assistant: Tere! Ei ole kurb asi, seda saab lahti blokeerida. Kui sa kolm korda järjest valesti PIN-koodi sisestasid, siis ID-kaart seda turvalisuse huvides blokeeris. Sa saad oma PIN-koodi ise kiirelt ja mugavalt lahti blokeerida. Pühkige ID-kaardi PIN-koodi blokeerimise maha. Kuidas ma saan selle täna tegema?",output_ID.ee +"user: Tere! Ma pole kunagi enne mobiil-ID'd kasutanud. Mis see üldse on ja kuidas seda kasutada? +assistant: Tere! Mobili-ID on digitaalne identifikaator, mis võimaldab sul turvaliselt sisse logida erinevate e-teenustesse, näiteks e-posti, pankadesse ja ametiasutuste veebilehtudele. See on nagu digitaalne isikutunnus. Seda saab kasutada ka dokumendi allkirjastamiseks. Alguses on see pisemaks saamiseks eelistatud meetod, mis sobib enamusele inimestele. Kas sa tead, kas sul on juba mobiil-ID?",output_ID.ee +"user: Tere! Ma ostsin pojale uue mänguasja, aga ma ei saa aru, mida ma selle kohta teada peaks kontrollima, et see ohutu oleks? Mida see CE-märk tähendab? +assistant: Tere! See on väga hea küsimus! CE-märk näitab, et mänguasi vastab Euroopa Liidu ohutusnõuetele. See ei garanteeri 100% ohutust, aga see on oluline näitaja. Kontrollige ka, kas mänguasi on sobiva vanusgrupi jaoks. See on oluline, et laps saaks sellest turvaliselt nautida.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin, et muutub midagi ehitustoodete kohta. Mis on kõige olulisem muudatus, mida Tarbijakaitse ja Tehnilise Järelevalve Amet (TJT) on rõhutanud? +assistant: Tere! TJT on rõhutanud, et 2024/3110 määrus asendab 305/2011 määruse. Peamine muudatus on see, et peale toimivuse peab tootja deklareerima ka ehitustoote vastavust ohutus- ja keskkonnanõuetele. See tähendab, et tuleb koostada toimivus- ja vastavusdeklaratsioon selle asemel, mida varem kutsuti lihtsalt toimivusdeklaratsiooniks.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Kas ma saan dokumendid RIA DigiDoc mobiilirakenduses digiallkirjastada? +assistant: Kuigi, jah! Digiallkirjastamine RIA DigiDoc mobiilirakenduses on üsna lihtne. Kas sa kasutad ID-kaarti, mobiil-ID või Smart-ID? Alustame kõige lihtsamaga - kas sa kasutad ID-kaarti? ",output_ID.ee +"user: Tere! Ma tahan Eesti passi taotleda. Kutsun teid abi, palun. +assistant: Tere! Muidugi aitame. Millist passi soovite taotleda – tavapass või kiirkorras?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) jälgib elektriseadmete ohutust. Kuidas see täpsemalt toimib? +assistant: Tere! TTJA jälgib, et elektriseadmed oleksid valmistatud Euroopa Liidu ohutustehnika reeglite järgi ja ei ohustaks kasutajat, kui neid on paigaldatud ja hooldatud õigesti ning kasutusotstarbega vastavuses. Enamuses elektriseadmed peavad olema varustatud CE-märgistega, mis kinnitab nende vastavust direktiividele.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma olen liikumisvõimetu ja tahan ID-kaarti taotleda. Kust ma peaksin alustama? +assistant: Tere! Esimese samuna saad ID-kaarti taotleda kas iseteeninduses, posti teel või kui sind aitab sotsiaaltöötaja. Iseteenindus on lihtsam, kui suudad ise dokumenditaotluse esitada ja tasuda riigilõivu. Posti teel pead täitma ankeedi ja saama dokumendifoto. Kui sind aitab sotsiaaltöötaja, siis on protsess teine.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud midagi avalikest koosolekutest. Mis see täpselt tähendab? +assistant: Tere! Avalik koosolek on oluline mõiste. See tähendab inimeste kogunemist avalikus kohas ühise eesmärgiga oma meelt väljendada või kujundada. Näiteks väljakul, pargis või isegi tänaval toimuv demonstratsioon või miiting kuulub selle alla.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulen palju tõstukite ohutusest. Kas kellelgi on mingit aimu, kui palju tõstukid Eestis üldse on? +assistant: Tere! See on hea küsimus. Täpse arvu ei tea keegi, kuid ettevõtted arvavad, et neid on rohkem kui 100 000, ja võib-olla isegi kuni pool miljonit. Seega on nende ohutuse tagamine päris suur väljakutse.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin, et Tarbijakaitse ja Tehnilise Järevalve Amet on teinud materjale raudteeohutuse teema kohta. Mis see täpsemalt on? +assistant: Tere! Jah, meil on mitmeid materjale, mis on suunatud nii lapsevanematele, õpetajatele kui ka lastele endadele. Põhiliseks eesmärgiks on tekitada teadlikkust raudteeületamise ohudest ja õpetada, kuidas ohutult käituda raudtee läheduses. Nende materjalide hulka kuuluvad õppematerjalid, õppevideod ja töövihikud.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Olen kuulnud, et on olemas ebaausad müügivõtted. Mis see tähendab tarbija jaoks? +assistant: Tere! Ebaausad müügivõtted on sellised müügitehnikad, mis püüavad sind tarbijaid eksitada või survestada ostma. Seadus keelab selliseid võtteid Euroopa Liidus. Need võtted võivad olla kas eksitavad või agressiivsed – mõlemad on probleemsed.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulsin, et Politsei- ja Piirivalveamet on saanud rahastust piiriehituseks. Mis see täpsemalt tähendab? +assistant: Tere! See on õige. PPA on perioodil 2014-2020 saanud toetust Euroopa Liidu Sisejulgeolekufondi välispiiride ja viisade rahastamisvahendist, et rahastada erinevaid piiriehitusprojekte.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Sa ütled, et siin on infot ID-tarkvara kohta. Kõigepealt, mis see ID-tarkvara üldse on? +assistant: Tere! ID-tarkvara on kogum, mis aitab identiteeti tõestada ja digitaalselt dokumente allkirjastada. Siin on osa sellest: DigiDoc4, RIA e-allkirja valideerimisteenus SiVa ning RIA DigiDoc mobiilirakendus. Kõigepealt aga, kas sa tead, mis on identiteedi tõendamine?",output_ID.ee +"user: Tere! Kas ma saan aru, et politsei võtab kohalolekul vastu rolli koosolekute korraldamisel? Kas see on mingi kohustus? +assistant: Tere! Jah, seda saab juhtuda. Politsei võib kohustada koosoleku korraldajat täitma lisaseid kohustusi, kui on alust arvata, et määratud korrapidaja ei suuda koosoleku korraldajat piisavalt abistada. See on siis tänu suurendatud korrapidajate arvu vajavusele. Kas sa mõtled rohkem sellele, mis juhtuks, kui korrapidaja ei suuda head abi pakkuda?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma saan probleeme ID-kaardi kasutamisega. Kas teil on ID.ee veebis kohta, kus ma saan vastuseid? Kas see on midagi, mis on loodud just selleks? +assistant: Tere! Jah, ID.ee veebis on täiesti loodud suur hulk teavet ID-kaardi ja mobiil-ID kohta. Sa võid seda kasutada, kui sul on probleeme. Palun kasuta meie otsingut – see on kõige parem viis leida just seda, mida sa otsid. Kui sul on raskusi leplikust või oled märganud midagi ebaselge, palun anna teada!",output_ID.ee +"user: Tere! Mida täpsemalt tähendab see, et Tarbijakaitse ja Tehnilise Järelevalve Amet (TJAA) reguleerib kemikaale? +assistant: Tere! TJAA reguleerib kemikaale selleks, et kaitsta inimese elu ja tervist, asja ja keskkonda ning tagada kaupade vaba liikumine. Kemikaaliseadus seab piiranguid kemikaali käitlemisele ja sellega seotud majandustegevusele. See puudutab nii tootmist, importi kui ka müüki.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kaalun passi taotlemist. Kas te teate, kuidas ma Põlva politseijaoskonna teeninduses sellega hakkama saan? +assistant: Tere! Parim ja kiireim viis on kasutada PPA iseteeninduskeskkonda: https://www.politsei.ee/et/iseteenindus. Kui eelistate, saate kasutada digikioski Põlva teeninduses. Teiste võimaluste kohta küsige aga edasi!",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma olen kuulnud, et energiamärgistused muutuvad. Mis see tähendab täpselt? Mis toodete puhul seda muutust ootada? +assistant: Tere! Jah, energiamärgistused muutuvad Euroopa Liidus. Peamiselt muutub skaala lihtsustatuks – tagasi algsele A-st G-ni. Muutus puudutab mitmeid toodete gruppe, sealhulgas nõudepesumasinaid, pesumasinaid, külmutus- ja jahutusseadmeid, televiisoreid ja valgusallikaid. Muudatused on järk-järgult sisse viidud alates 2021. aasta 1. märtsist, valgusallikate osas aga 1. septembrist 2021.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ostsin hiljaaegu internetist riideid Soomest. Mis juhtub, kui riideid ostes avastan vea? Kas mul on mingid õigused? +assistant: Tere! Küll on. Kuna Soome on Euroopa Liidu riik, siis kehtivad Sinule samad tarbijaõigused, nagu ostes ka Eestis. Sul on õigus esitada kaebus puuduste kohta vähemalt kahe aasta jooksul alates ostukuupäevast. Oluline on, et Sul oleks ostu tõendav dokument, näiteks arve või maksekõrralduse väljavõte.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma arvan, et Riigiportaali eesti.ee on teadaolev asi, aga ma ei ole kindel, kuidas seesama eesti.ee kasutada. Kas sa saaksid mulle aidata? +assistant: Muidugi! Riigiportaali eesti.ee on üsna lahe asi. See on üks keskoht, kus saad kõik oma riigiga seotud teave ühes kohas. Kas sa oled mõelnud, mida selle kaudu teha saaksid? Näiteks, kas sul on küpsus testi tulemused, mis sa tahaksid kontrollida?",output_ID.ee +"user: Tere! Ma ei saa aru, miks mul peab PIN-koodi ja PUK-koodi vahetama. Kas see on oluline? +assistant: Tere! Pigil, see on väga hea tava turvalisuse tagamiseks. Kui PIN- või PUK-koodid lähevad kadunud või on teiega otseselt mitte liigutav (näiteks, nad on lukustatud), on uus PIN ja PUK seadmine oluline. Kui kehtivad PIN-koodid on teiega selge selge, pole see eriti oluline, aga kui need on kadunud või lukustatud, siis tuleb neid kindlasti vahetada.",output_ID.ee +"user: Tere! Ma olen huvitatud raudtee kaitsevööndi kohta rohkem teada saama. Mis see üldse on ja miks see on olemas? +assistant: Tere! Raudtee kaitsevöönd on piirkond raudtee läheduses, mis on ette nähtud, et tagada raudtee sujuv toimimine ja turvalisus. See piirab tegevusi, mis saaksid häirida raudteeliiklust või suurendada ohtu. See on ka mõeldud selleks, et vähendada raudteelt lähtuvaid kahjulikke mõjusid keskkonda.",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma tahan teada, kuidas toimib Eesti kodakondsuse menetlus lapsele. Mis juhtub pärast taotluse esitamist? +assistant: Tere! Pärast lapse kodakondsuse taotluse esitamist, edastatakse kõik dokumendid kolme kuu jooksul Vabariigi Valitsusele. Just nemad teevad otsuse lapse Eesti kodakondsuse andmise kohta.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma elan välismaal ja tahan teada, kuidas ma saan Eesti passi taotleda. Mis on kõige lihtsam viis? +assistant: Tere! Välismaal saad passi taotleda kas iseteenindusportaali (https://etaotlus.politsei.ee/#/login) või Eesti Vabariigi välisesinduse kaudu. Milline variant sulle sobivam on?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma plaanisin avaliku ürituse korraldada. Mis on kõige tähtsam, mida pean silmas pidama, et Politsei- ja Piirivalveamet sellega toetaks? +assistant: Tere! See on väga hea küsimus! Avaliku ürituse korraldamisel on mitmeid olulisi asju, millele tasub tähelepanu juhtida. Esiteks, pead pöörduma avaliku ürituse toimumise kohalikku omavalitsusse ja saama nende poolt luba. Neil on kohustus kontrollida, kas üritus kohalikele elanikele ei tekita liigset olumajanduskoormust. Kui sa saad luba, Politsei- ja Piirivalveamet soovivad, et saaksid ka ürituse korraldamisega algust juba ette mõelda. Kas sa saaksid selgeks, millest muidu peaksime arutama?",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma saan probleeme e-teenuste sisenemisega. Mis ma teha saan? +assistant: Tere! Ma saan aru, et sul on raskusi e-teenustega. Palju maasõnki! Alustame kõige lihtsamatest asjadest. Kas su arvatakse, et probleem on seotud ID-kaardiga või rohkem üldisemalt veebisõidumisel?",output_ID.ee +"user: Tere! Ma arvan, et digitaalse allkirjastamine on vaimujaur, aga kas teeksite sellest mõru, et nad on nüüd igasugused allkirjad digiallkirjad? Kas see tähendab, et iga dokumendi allkiri on samamoodi juriidiliselt võrdne omakäelise allkirjaga? +assistant: Tere! Päris hea küsimus! See on päris oluline erinevus. Tegelikult tähendab see, et ‘digiallkiri’ (ehk digiallkirjastamine jm) Eestis viitab ainult sellele signeerimisviisile, mis on seaduslikult kehtiv ja juriidiliselt võrdne omakäelise allkirjaga. See tähendab, et allkirja andmise ajal on kindlalt kontrollitud kasutaja identiteet ja sertifikaadi väljaandja taust. See tagab, et keegi teine ei saaks allkirjastatud dokumenti muutma. Elektrooniline allkiri ongi laiem mõiste, mis hõlmab ka digiallkirju, aga need ei pruugi olla samuti nii kindlalt juriidiliselt väärindatud.",output_ID.ee +"user: Tere! Ma saan probleeme e-teenustesse sisse logimisel. Mida ma peaks tegema? +assistant: Tere! Puhkan teada, et e-teenustesse sisse logimisel tekib probleem. Ma aitaksin sulle probleemi lahendada. Alustame kõige lihtsamatest sammudest. Kas sa kasutad Windowsi, macOSi või Linuxi arvutit? See aitab mulle paremini aidata.",output_ID.ee +"user: Tere! Ma lugesin Politsei- ja Piirivalveameti veebilehe ligipääsetavuse teatisest. Mis see üldse tähendab, et veebileht on osaliselt vastavuses ligipääsetavusnõuetele? +assistant: Tere! See tähendab, et Politsei- ja Piirivalveameti veebileht www.politsei.ee ei vasta täielikult kõikidele ligipääsetavusnõuetele. See vastab neile osaliselt, aga teatud funktsioonid ja sisud ei ole veel täies mahus ligipääsetavad kõigile kasutajatele. Seda on tehtud välispartneri hindamise põhjal.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin Rail Balticu kohta palju. Kus ma saaksin infot selle keskkonnamõjude hindamise kohta? +assistant: Tere! Tarbijakaitse ja Tehnilise Järelevalve Amet (TTJA) avalikustab seonduvaid teateid oma veebilehel. Leiad need siit: https://ttja.ee/eraklient/tarbija-oigused/avalikud-teated/keskkonnamoju-hindamisega-seotud-teated",output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet +"user: Tere! Ma kuulin, et Politsei ja Piirivalveameti koduleht on tehtud nii, et see oleks kõigile kättesaadav. Kuidas see täpselt tagatakse? +assistant: Tere! See on õige. Meie koduleht on ehitatud vastavalt WCAG 2.1 AA juurdepääsetavuse suunistele. See tähendab, et oleme kasutanud nii tehnilisi vahendeid kui ka sisu koostamise põhimõtteid, et puuetega inimesed saaksid seda kasutajakogemust nautida. See kehtib näiteks pimese, vaegnägemise, kurtuse, vaegkuulmise, õpiraskuste ja teiste inimeste jaoks, kellel on erinevaid erivajadusi.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma ei saa ID-kaardiga e-teenustes midagi teha. Mis ma peaks tegema? +assistant: Tere! Üldiselt on see probleem paljudel juhtudel seotud sellega, et veebilehitseja ei ole õigesti seadistatud ID-kaardi sertifikaatide kasutamiseks. Palju oluline on see, et veebilehitsejas on Web eID laiendus lubatud. Kas olete proovinud Chrome'i seadeid vaadata? Kui te seda ei ole, siis veenduge, et olete samm 2 läbi viinud - vajutage Chrome paremas servas ikoonile ja vali ‘Lisad’. Kui seda ei ole, siis võtke kindlasti puudu, et lisada `ncibgoaomkmdpilpocfeponihegamlic` lisada Chrome'ile.",output_ID.ee +"user: Tere! Mul on tähtajaline elamisluba töötamiseks Eestis ja see lõppeb peagi. Kuidas ma saan selle pikendada? +assistant: Tere! Elamisluba töötamiseks saab pikendada. Peate esitama pikendamise taotluse vähemalt 2 kuud enne praeguse loa lõppemist. Vastus antakse hiljemalt 10 päeva enne kehtiva loa lõppemist, kui taotlus on esitatud õigeaegselt.",output_Politsei-_ja_Piirivalveamet +"user: Tere! Ma kuulsin, et täisealise lapse saab Eestisse elama kutsuda. Mis see täpselt tähendab, mis on elamisluba pereliikmega liitumiseks? +assistant: Tere! Elamisluba pereliikmega liitumiseks tähendab, et kui oled Eesti kodanik või Eestis elamisloa alusel elav välismaalane, saad kutsuda oma täisealise lapse Eestisse elama, et ta saaks siin sinuga koos elada. Elamisluba annab selleks õiguse ja väljastatakse elamisloakaartina.",output_Politsei-_ja_Piirivalveamet diff --git a/experiments/ood_detection/evaluation/__init__.py b/experiments/ood_detection/evaluation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/experiments/ood_detection/evaluation/inference_metrics.py b/experiments/ood_detection/evaluation/inference_metrics.py new file mode 100644 index 00000000..030c444b --- /dev/null +++ b/experiments/ood_detection/evaluation/inference_metrics.py @@ -0,0 +1,454 @@ +""" +Inference performance metrics for OOD detection models. +""" + +import tensorflow as tf +import numpy as np +from typing import Dict, List, Optional, Any, Callable +import time +import logging +import os +import json +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class InferenceMetrics: + """ + Inference performance metrics for OOD detection models. + + This class provides methods for measuring and analyzing + inference performance of OOD detection models. + """ + + def __init__(self, use_gpu: bool = False): + """ + Initialize inference metrics calculator. + + Args: + use_gpu: Whether to use GPU for inference + """ + self.use_gpu = use_gpu + + # Set TensorFlow device + if use_gpu: + # Check if GPU is available + gpus = tf.config.list_physical_devices("GPU") + if not gpus: + logger.warning("No GPU found. Using CPU instead.") + self.device = "/CPU:0" + else: + self.device = "/GPU:0" + else: + self.device = "/CPU:0" + + def measure_inference_time( + self, + model: tf.keras.Model, + inputs: Dict[str, tf.Tensor], + batch_sizes: List[int] = [1, 4, 8, 16, 32, 64], + num_runs: int = 100, + warmup_runs: int = 10, + use_uncertainty: bool = True, + ) -> Dict[str, Dict[str, float]]: + """ + Measure inference time for different batch sizes. + + Args: + model: The model to measure + inputs: Example inputs to the model + batch_sizes: List of batch sizes to test + num_runs: Number of inference runs for each batch size + warmup_runs: Number of warmup runs + use_uncertainty: Whether to use predict_with_uncertainty + + Returns: + results: Dictionary of inference times for each batch size + """ + results = {} + + # Sample input data + sample_input_ids = inputs["input_ids"] + sample_attention_mask = inputs["attention_mask"] + + for batch_size in batch_sizes: + logger.info(f"Measuring inference time for batch size {batch_size}") + + # Create batch of data + if batch_size <= len(sample_input_ids): + input_ids = sample_input_ids[:batch_size] + attention_mask = sample_attention_mask[:batch_size] + else: + # Replicate data to reach the desired batch size + copies = batch_size // len(sample_input_ids) + 1 + input_ids = tf.tile(sample_input_ids, [copies, 1])[:batch_size] + attention_mask = tf.tile(sample_attention_mask, [copies, 1])[ + :batch_size + ] + + batch_inputs = {"input_ids": input_ids, "attention_mask": attention_mask} + + # Warmup runs + for _ in range(warmup_runs): + if use_uncertainty: + _ = model.predict_with_uncertainty(batch_inputs) + else: + _ = model(batch_inputs, training=False) + + # Measure inference time + times = [] + with tf.device(self.device): + for _ in range(num_runs): + start_time = time.time() + + if use_uncertainty: + _ = model.predict_with_uncertainty(batch_inputs) + else: + _ = model(batch_inputs, training=False) + + end_time = time.time() + times.append(end_time - start_time) + + # Calculate statistics + mean_time = np.mean(times) + std_time = np.std(times) + median_time = np.median(times) + p95_time = np.percentile(times, 95) + + # Store results + results[str(batch_size)] = { + "mean": mean_time, + "std": std_time, + "median": median_time, + "p95": p95_time, + "throughput": batch_size / mean_time, + } + + return results + + def measure_training_time( + self, + model_fn: Callable[[], tf.keras.Model], + train_dataset: tf.data.Dataset, + val_dataset: tf.data.Dataset, + epochs: int = 5, + num_runs: int = 3, + ) -> Dict[str, float]: + """ + Measure training time for the model. + + Args: + model_fn: Function that returns a new model instance + train_dataset: Training dataset + val_dataset: Validation dataset + epochs: Number of epochs to train + num_runs: Number of training runs + + Returns: + results: Dictionary of training time statistics + """ + times = [] + + for run in range(num_runs): + logger.info(f"Training run {run + 1}/{num_runs}") + + # Create new model instance + model = model_fn() + + # Measure training time + start_time = time.time() + + # Train the model + model.fit( + train_dataset, validation_data=val_dataset, epochs=epochs, verbose=0 + ) + + end_time = time.time() + total_time = end_time - start_time + times.append(total_time) + + logger.info(f"Training time: {total_time:.2f} seconds") + + # Calculate statistics + mean_time = np.mean(times) + std_time = np.std(times) + min_time = np.min(times) + max_time = np.max(times) + + return { + "mean": mean_time, + "std": std_time, + "min": min_time, + "max": max_time, + "per_epoch": mean_time / epochs, + } + + def measure_memory_usage( + self, + model: tf.keras.Model, + inputs: Dict[str, tf.Tensor], + batch_sizes: List[int] = [1, 4, 8, 16, 32, 64], + use_uncertainty: bool = True, + ) -> Dict[str, float]: + """ + Estimate memory usage for different batch sizes. + + Note: This is a rough estimation and may not be accurate for all models. + + Args: + model: The model to measure + inputs: Example inputs to the model + batch_sizes: List of batch sizes to test + use_uncertainty: Whether to use predict_with_uncertainty + + Returns: + results: Dictionary of memory usage for each batch size + """ + results = {} + + # Sample input data + sample_input_ids = inputs["input_ids"] + sample_attention_mask = inputs["attention_mask"] + + for batch_size in batch_sizes: + logger.info(f"Measuring memory usage for batch size {batch_size}") + + # Create batch of data + if batch_size <= len(sample_input_ids): + input_ids = sample_input_ids[:batch_size] + attention_mask = sample_attention_mask[:batch_size] + else: + # Replicate data to reach the desired batch size + copies = batch_size // len(sample_input_ids) + 1 + input_ids = tf.tile(sample_input_ids, [copies, 1])[:batch_size] + attention_mask = tf.tile(sample_attention_mask, [copies, 1])[ + :batch_size + ] + + batch_inputs = {"input_ids": input_ids, "attention_mask": attention_mask} + + # Clear memory + if self.use_gpu: + tf.keras.backend.clear_session() + + # Run once to initialize + if use_uncertainty: + _ = model.predict_with_uncertainty(batch_inputs) + else: + _ = model(batch_inputs, training=False) + + # Estimate memory usage (rough estimate) + # This only works for certain environments and may not be accurate + try: + if self.use_gpu and tf.config.list_physical_devices("GPU"): + memory_info = tf.config.experimental.get_memory_info("GPU:0") + memory_usage = memory_info["current"] / (1024**2) # Convert to MB + else: + # For CPU, we use the model size as a proxy + model_size = model.count_params() * 4 # 4 bytes per parameter + memory_usage = model_size / (1024**2) # Convert to MB + except Exception as e: + logger.warning( + f"Could not retrieve memory info: {e}. Using model size as proxy." + ) + # If memory info is not available, use a placeholder + memory_usage = -1 + + results[str(batch_size)] = memory_usage + + return results + + def plot_inference_times( + self, + inference_results: Dict[str, Dict[str, float]], + title: str = "Inference Time vs Batch Size", + save_path: Optional[str] = None, + ) -> plt.Figure: + """ + Plot inference times for different batch sizes. + + Args: + inference_results: Results from measure_inference_time + title: Plot title + save_path: Path to save the plot + + Returns: + fig: The figure object + """ + batch_sizes = sorted([int(bs) for bs in inference_results.keys()]) + mean_times = [inference_results[str(bs)]["mean"] for bs in batch_sizes] + p95_times = [inference_results[str(bs)]["p95"] for bs in batch_sizes] + + fig, ax1 = plt.subplots(figsize=(10, 6)) + + # Plot inference time + line1 = ax1.plot(batch_sizes, mean_times, "b-", marker="o", label="Mean Time") + line2 = ax1.plot(batch_sizes, p95_times, "b--", marker="s", label="P95 Time") + ax1.set_xlabel("Batch Size") + ax1.set_ylabel("Inference Time (seconds)", color="b") + ax1.tick_params(axis="y", labelcolor="b") + + # Plot throughput on secondary axis + ax2 = ax1.twinx() + throughput = [inference_results[str(bs)]["throughput"] for bs in batch_sizes] + line3 = ax2.plot(batch_sizes, throughput, "r-", marker="x", label="Throughput") + ax2.set_ylabel("Throughput (examples/second)", color="r") + ax2.tick_params(axis="y", labelcolor="r") + + # Combine legends + lines = line1 + line2 + line3 + labels = [line.get_label() for line in lines] + ax1.legend(lines, labels, loc="upper left") + + plt.title(title) + plt.grid(True, alpha=0.3) + + if save_path is not None: + plt.savefig(save_path, bbox_inches="tight", dpi=300) + + return fig + + def compare_models( + self, + model_results: Dict[str, Dict[str, Dict[str, float]]], + metric: str = "mean", + title: str = "Model Inference Time Comparison", + save_path: Optional[str] = None, + ) -> plt.Figure: + """ + Compare inference times across different models. + + Args: + model_results: Dictionary of results for each model + metric: Which metric to compare ('mean', 'p95', 'throughput') + title: Plot title + save_path: Path to save the plot + + Returns: + fig: The figure object + """ + fig, ax = plt.subplots(figsize=(12, 8)) + + # Extract batch sizes from the first model + model_name = list(model_results.keys())[0] + batch_sizes = sorted([int(bs) for bs in model_results[model_name].keys()]) + + # Prepare data for plotting + data = [] + for model, results in model_results.items(): + for bs in batch_sizes: + value = results[str(bs)][metric] + data.append({"Model": model, "Batch Size": bs, "Value": value}) + + # Create DataFrame + df = pd.DataFrame(data) + + # Plot + if metric == "throughput": + # For throughput, higher is better + sns.barplot(x="Batch Size", y="Value", hue="Model", data=df, ax=ax) + ax.set_ylabel("Throughput (examples/second)") + else: + # For time metrics, lower is better + sns.barplot(x="Batch Size", y="Value", hue="Model", data=df, ax=ax) + ax.set_ylabel(f"Inference Time ({metric})") + + ax.set_title(title) + ax.grid(True, alpha=0.3) + + # Rotate x-axis labels if there are many batch sizes + if len(batch_sizes) > 6: + plt.xticks(rotation=45) + + plt.tight_layout() + + if save_path is not None: + plt.savefig(save_path, bbox_inches="tight", dpi=300) + + return fig + + def save_results( + self, results: Dict[str, Any], model_name: str, output_dir: str + ) -> str: + """ + Save inference results to a file. + + Args: + results: Results dictionary + model_name: Name of the model + output_dir: Directory to save the results + + Returns: + file_path: Path to the saved file + """ + os.makedirs(output_dir, exist_ok=True) + file_path = os.path.join(output_dir, f"{model_name}_inference_metrics.json") + + with open(file_path, "w") as f: + json.dump(results, f, indent=2) + + logger.info(f"Results saved to {file_path}") + return file_path + + def generate_performance_report( + self, + inference_results: Dict[str, Dict[str, float]], + training_results: Optional[Dict[str, float]] = None, + model_name: str = "Model", + output_dir: str = "results", + ) -> str: + """ + Generate a performance report. + + Args: + inference_results: Results from measure_inference_time + training_results: Results from measure_training_time + model_name: Name of the model + output_dir: Directory to save the report + + Returns: + report_path: Path to the generated report + """ + os.makedirs(output_dir, exist_ok=True) + report_path = os.path.join(output_dir, f"{model_name}_performance_report.txt") + + with open(report_path, "w") as f: + f.write(f"Performance Report for {model_name}\n") + f.write("=" * 50 + "\n\n") + + f.write("Inference Performance:\n") + f.write("-" * 30 + "\n") + + f.write( + "Batch Size | Mean Time (s) | P95 Time (s) | Throughput (examples/s)\n" + ) + f.write("-" * 70 + "\n") + + for bs in sorted([int(bs) for bs in inference_results.keys()]): + results = inference_results[str(bs)] + f.write( + f"{bs:^10} | {results['mean']:^13.4f} | {results['p95']:^12.4f} | {results['throughput']:^24.2f}\n" + ) + + f.write("\n") + + if training_results is not None: + f.write("\nTraining Performance:\n") + f.write("-" * 30 + "\n") + + f.write( + f"Total Training Time: {training_results['mean']:.2f} seconds\n" + ) + f.write( + f"Time per Epoch: {training_results['per_epoch']:.2f} seconds\n" + ) + f.write(f"Standard Deviation: {training_results['std']:.2f} seconds\n") + f.write(f"Min Training Time: {training_results['min']:.2f} seconds\n") + f.write(f"Max Training Time: {training_results['max']:.2f} seconds\n") + + logger.info(f"Performance report saved to {report_path}") + return report_path diff --git a/experiments/ood_detection/evaluation/metrics.py b/experiments/ood_detection/evaluation/metrics.py new file mode 100644 index 00000000..07fd82cb --- /dev/null +++ b/experiments/ood_detection/evaluation/metrics.py @@ -0,0 +1,471 @@ +""" +Evaluation metrics for OOD detection models. +""" + +import numpy as np +from typing import Dict, List, Optional +import sklearn.metrics as skmetrics +import matplotlib.pyplot as plt +import seaborn as sns +import logging +import os +from scipy import interpolate + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class OODMetrics: + """ + Evaluation metrics for OOD detection. + + This class provides methods for computing various metrics + for evaluating OOD detection performance. + """ + + def __init__(self): + """Initialize OOD metrics calculator.""" + pass + + def compute_metrics( + self, + id_uncertainties: np.ndarray, + ood_uncertainties: np.ndarray, + id_labels: Optional[np.ndarray] = None, + id_predictions: Optional[np.ndarray] = None, + metric_names: Optional[List[str]] = None, + ) -> Dict[str, float]: + """ + Compute OOD detection metrics. + + Args: + id_uncertainties: Uncertainty scores for in-distribution examples + ood_uncertainties: Uncertainty scores for out-of-distribution examples + id_labels: Ground truth labels for in-distribution examples + id_predictions: Predicted labels for in-distribution examples + metric_names: List of metric names to compute + + Returns: + metrics: Dictionary of metric values + """ + if metric_names is None: + metric_names = [ + "auroc", + "aupr", + "fpr_at_95_tpr", + "detection_error", + "accuracy", + "f1", + "precision", + "recall", + ] + + metrics = {} + + # Create binary labels for all examples + # 0 for ID, 1 for OOD + n_id = len(id_uncertainties) + n_ood = len(ood_uncertainties) + + y_true = np.concatenate( + [ + np.zeros(n_id, dtype=np.int32), # ID examples + np.ones(n_ood, dtype=np.int32), # OOD examples + ] + ) + + # Concatenate uncertainties + y_score = np.concatenate([id_uncertainties, ood_uncertainties]) + + # Compute OOD detection metrics + if "auroc" in metric_names: + metrics["auroc"] = self.compute_auroc(y_true, y_score) + + if "aupr" in metric_names: + metrics["aupr"] = self.compute_aupr(y_true, y_score) + + if "fpr_at_95_tpr" in metric_names: + metrics["fpr_at_95_tpr"] = self.compute_fpr_at_95_tpr(y_true, y_score) + + if "detection_error" in metric_names: + metrics["detection_error"] = self.compute_detection_error(y_true, y_score) + + # Compute classification metrics (if labels and predictions provided) + if id_labels is not None and id_predictions is not None: + if "accuracy" in metric_names: + metrics["accuracy"] = skmetrics.accuracy_score( + id_labels, id_predictions + ) + + if "f1" in metric_names: + metrics["f1"] = skmetrics.f1_score( + id_labels, id_predictions, average="weighted" + ) + + if "precision" in metric_names: + metrics["precision"] = skmetrics.precision_score( + id_labels, id_predictions, average="weighted" + ) + + if "recall" in metric_names: + metrics["recall"] = skmetrics.recall_score( + id_labels, id_predictions, average="weighted" + ) + + if "confusion_matrix" in metric_names: + metrics["confusion_matrix"] = skmetrics.confusion_matrix( + id_labels, id_predictions + ).tolist() + + return metrics + + def compute_auroc(self, y_true: np.ndarray, y_score: np.ndarray) -> float: + """ + Compute Area Under the Receiver Operating Characteristic curve. + + Args: + y_true: Binary labels (0 for ID, 1 for OOD) + y_score: Uncertainty scores + + Returns: + auroc: AUROC score + """ + return skmetrics.roc_auc_score(y_true, y_score) + + def compute_aupr(self, y_true: np.ndarray, y_score: np.ndarray) -> float: + """ + Compute Area Under the Precision-Recall curve. + + Args: + y_true: Binary labels (0 for ID, 1 for OOD) + y_score: Uncertainty scores + + Returns: + aupr: AUPR score + """ + precision, recall, _ = skmetrics.precision_recall_curve(y_true, y_score) + return skmetrics.auc(recall, precision) + + def compute_fpr_at_95_tpr(self, y_true: np.ndarray, y_score: np.ndarray) -> float: + """ + Compute False Positive Rate at 95% True Positive Rate. + + Args: + y_true: Binary labels (0 for ID, 1 for OOD) + y_score: Uncertainty scores + + Returns: + fpr_at_95_tpr: FPR at 95% TPR + """ + fpr, tpr, thresholds = skmetrics.roc_curve(y_true, y_score) + + # Find the threshold that gives TPR closest to 95% + target_tpr = 0.95 + abs_diff = np.abs(tpr - target_tpr) + idx = np.argmin(abs_diff) + + # If TPR is exactly 95%, return the corresponding FPR + if abs_diff[idx] < 1e-10: + return fpr[idx] + + # Otherwise, interpolate to get the exact FPR at 95% TPR + interp_function = interpolate.interp1d(tpr, fpr) + try: + return float(interp_function(target_tpr)) + except (ValueError, TypeError): + # If interpolation fails, return the closest value + return fpr[idx] + + def compute_detection_error(self, y_true: np.ndarray, y_score: np.ndarray) -> float: + """ + Compute minimum detection error. + + This is defined as the minimum average of the false positive rate + and the false negative rate. + + Args: + y_true: Binary labels (0 for ID, 1 for OOD) + y_score: Uncertainty scores + + Returns: + detection_error: Minimum detection error + """ + fpr, tpr, thresholds = skmetrics.roc_curve(y_true, y_score) + + # Detection error = 0.5 * (FPR + FNR) = 0.5 * (FPR + (1 - TPR)) + detection_error = 0.5 * (fpr + (1 - tpr)) + + # Return the minimum detection error + return min(detection_error) + + def get_threshold_for_fpr( + self, + id_uncertainties: np.ndarray, + ood_uncertainties: np.ndarray, + target_fpr: float = 0.05, + ) -> float: + """ + Get the threshold for a target false positive rate. + + Args: + id_uncertainties: Uncertainty scores for ID examples + ood_uncertainties: Uncertainty scores for OOD examples + target_fpr: Target false positive rate + + Returns: + threshold: Threshold for the target FPR + """ + # Create binary labels for all examples + n_id = len(id_uncertainties) + n_ood = len(ood_uncertainties) + + y_true = np.concatenate( + [ + np.zeros(n_id, dtype=np.int32), # ID examples + np.ones(n_ood, dtype=np.int32), # OOD examples + ] + ) + + # Concatenate uncertainties + y_score = np.concatenate([id_uncertainties, ood_uncertainties]) + + # Compute ROC curve + fpr, tpr, thresholds = skmetrics.roc_curve(y_true, y_score) + + # Find the threshold that gives FPR closest to target + abs_diff = np.abs(fpr - target_fpr) + idx = np.argmin(abs_diff) + + return thresholds[idx] + + def plot_roc_curve( + self, + y_true: np.ndarray, + y_score: np.ndarray, + title: str = "ROC Curve", + save_path: Optional[str] = None, + ) -> plt.Figure: + """ + Plot the ROC curve. + + Args: + y_true: Binary labels (0 for ID, 1 for OOD) + y_score: Uncertainty scores + title: Plot title + save_path: Path to save the plot + + Returns: + fig: The figure object + """ + fpr, tpr, _ = skmetrics.roc_curve(y_true, y_score) + auroc = skmetrics.roc_auc_score(y_true, y_score) + + fig, ax = plt.subplots(figsize=(8, 6)) + ax.plot(fpr, tpr, lw=2, label=f"AUROC = {auroc:.3f}") + ax.plot([0, 1], [0, 1], "k--", lw=2) + ax.set_xlim([0.0, 1.0]) + ax.set_ylim([0.0, 1.05]) + ax.set_xlabel("False Positive Rate") + ax.set_ylabel("True Positive Rate") + ax.set_title(title) + ax.legend(loc="lower right") + ax.grid(True, alpha=0.3) + + if save_path is not None: + plt.savefig(save_path, bbox_inches="tight", dpi=300) + + return fig + + def plot_pr_curve( + self, + y_true: np.ndarray, + y_score: np.ndarray, + title: str = "Precision-Recall Curve", + save_path: Optional[str] = None, + ) -> plt.Figure: + """ + Plot the Precision-Recall curve. + + Args: + y_true: Binary labels (0 for ID, 1 for OOD) + y_score: Uncertainty scores + title: Plot title + save_path: Path to save the plot + + Returns: + fig: The figure object + """ + precision, recall, _ = skmetrics.precision_recall_curve(y_true, y_score) + aupr = skmetrics.auc(recall, precision) + + fig, ax = plt.subplots(figsize=(8, 6)) + ax.plot(recall, precision, lw=2, label=f"AUPR = {aupr:.3f}") + ax.set_xlim([0.0, 1.0]) + ax.set_ylim([0.0, 1.05]) + ax.set_xlabel("Recall") + ax.set_ylabel("Precision") + ax.set_title(title) + ax.legend(loc="lower left") + ax.grid(True, alpha=0.3) + + if save_path is not None: + plt.savefig(save_path, bbox_inches="tight", dpi=300) + + return fig + + def plot_score_distributions( + self, + id_uncertainties: np.ndarray, + ood_uncertainties: np.ndarray, + title: str = "Uncertainty Score Distributions", + save_path: Optional[str] = None, + ) -> plt.Figure: + """ + Plot the distributions of uncertainty scores for ID and OOD examples. + + Args: + id_uncertainties: Uncertainty scores for ID examples + ood_uncertainties: Uncertainty scores for OOD examples + title: Plot title + save_path: Path to save the plot + + Returns: + fig: The figure object + """ + fig, ax = plt.subplots(figsize=(10, 6)) + + # Plot ID and OOD distributions + sns.histplot( + id_uncertainties, + color="blue", + alpha=0.6, + label="In-Distribution", + kde=True, + stat="density", + ax=ax, + ) + sns.histplot( + ood_uncertainties, + color="red", + alpha=0.6, + label="Out-of-Distribution", + kde=True, + stat="density", + ax=ax, + ) + + # Compute the threshold for 95% TPR + n_id = len(id_uncertainties) + n_ood = len(ood_uncertainties) + + y_true = np.concatenate( + [np.zeros(n_id, dtype=np.int32), np.ones(n_ood, dtype=np.int32)] + ) + + y_score = np.concatenate([id_uncertainties, ood_uncertainties]) + + fpr, tpr, thresholds = skmetrics.roc_curve(y_true, y_score) + + # Find the threshold for 95% TPR + target_tpr = 0.95 + idx = np.argmin(np.abs(tpr - target_tpr)) + threshold = thresholds[idx] + + # Add threshold line + ax.axvline( + threshold, color="green", linestyle="--", label="Threshold at 95% TPR" + ) + + ax.set_xlabel("Uncertainty Score") + ax.set_ylabel("Density") + ax.set_title(title) + ax.legend() + + if save_path is not None: + plt.savefig(save_path, bbox_inches="tight", dpi=300) + + return fig + + def plot_confusion_matrix( + self, + y_true: np.ndarray, + y_pred: np.ndarray, + labels: Optional[List[str]] = None, + title: str = "Confusion Matrix", + save_path: Optional[str] = None, + ) -> plt.Figure: + """ + Plot the confusion matrix. + + Args: + y_true: Ground truth labels + y_pred: Predicted labels + labels: Class labels + title: Plot title + save_path: Path to save the plot + + Returns: + fig: The figure object + """ + cm = skmetrics.confusion_matrix(y_true, y_pred) + + fig, ax = plt.subplots(figsize=(10, 8)) + + sns.heatmap( + cm, + annot=True, + fmt="d", + cmap="Blues", + xticklabels=labels if labels else "auto", + yticklabels=labels if labels else "auto", + ax=ax, + ) + + ax.set_xlabel("Predicted Labels") + ax.set_ylabel("True Labels") + ax.set_title(title) + + if save_path is not None: + plt.savefig(save_path, bbox_inches="tight", dpi=300) + + return fig + + def generate_summary_report( + self, metrics: Dict[str, float], model_name: str, output_dir: str + ) -> str: + """ + Generate a summary report of the metrics. + + Args: + metrics: Dictionary of metric values + model_name: Name of the model + output_dir: Directory to save the report + + Returns: + report_path: Path to the generated report + """ + os.makedirs(output_dir, exist_ok=True) + report_path = os.path.join(output_dir, f"{model_name}_summary.txt") + + with open(report_path, "w") as f: + f.write(f"Summary Report for {model_name}\n") + f.write("=" * 50 + "\n\n") + + f.write("OOD Detection Metrics:\n") + f.write("-" * 30 + "\n") + + ood_metrics = ["auroc", "aupr", "fpr_at_95_tpr", "detection_error"] + for metric in ood_metrics: + if metric in metrics: + f.write(f"{metric.upper()}: {metrics[metric]:.4f}\n") + + f.write("\nClassification Metrics:\n") + f.write("-" * 30 + "\n") + + cls_metrics = ["accuracy", "f1", "precision", "recall"] + for metric in cls_metrics: + if metric in metrics: + f.write(f"{metric.capitalize()}: {metrics[metric]:.4f}\n") + + logger.info(f"Summary report saved to {report_path}") + return report_path diff --git a/experiments/ood_detection/experiments/__init__.py b/experiments/ood_detection/experiments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/experiments/ood_detection/experiments/train_energy.py b/experiments/ood_detection/experiments/train_energy.py new file mode 100644 index 00000000..f44b3b27 --- /dev/null +++ b/experiments/ood_detection/experiments/train_energy.py @@ -0,0 +1,623 @@ +""" +Training script for Energy-based OOD detection model. +""" + +import os +import sys +import logging +import argparse +import tensorflow as tf +import numpy as np +import json +import time +from config.config import TRAINING_CONFIG, EnergyConfig +from data.data_loader import DataLoader +from models.energy_model import EnergyModel, EnergyLoss +from evaluation.metrics import OODMetrics +from evaluation.inference_metrics import InferenceMetrics +from utils.mlflow_logger import MLflowLogger +from utils.visualization import Visualizer + +# Setup paths +current_dir = os.path.dirname(os.path.abspath(__file__)) +project_root = os.path.dirname(current_dir) +if project_root not in sys.path: + sys.path.insert(0, project_root) + + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description="Train and evaluate Energy-based OOD detection model" + ) + + parser.add_argument("--train-file", type=str, help="Path to training data") + parser.add_argument("--dev-file", type=str, help="Path to validation data") + parser.add_argument("--test-file", type=str, help="Path to test data") + parser.add_argument("--ood-file", type=str, help="Path to OOD test data") + + parser.add_argument( + "--output-dir", + type=str, + default="outputs/energy", + help="Directory to save outputs", + ) + parser.add_argument( + "--model-dir", type=str, default="models/energy", help="Directory to save model" + ) + + parser.add_argument( + "--pretrained-model", + type=str, + default="xlm-roberta-base", + help="Pretrained model name or path", + ) + parser.add_argument( + "--max-seq-length", type=int, default=512, help="Maximum sequence length" + ) + parser.add_argument( + "--batch-size", + type=int, + default=32, + help="Batch size for training and evaluation", + ) + parser.add_argument( + "--epochs", type=int, default=10, help="Number of training epochs" + ) + parser.add_argument( + "--learning-rate", type=float, default=2e-5, help="Learning rate" + ) + + parser.add_argument( + "--energy-temp", type=float, default=1.0, help="Temperature for energy scoring" + ) + parser.add_argument( + "--energy-margin", type=float, default=10.0, help="Margin for energy-based loss" + ) + parser.add_argument( + "--energy-weight", type=float, default=0.1, help="Weight for energy loss term" + ) + parser.add_argument( + "--use-energy-loss", + action="store_true", + help="Whether to use energy-based loss during training", + ) + + parser.add_argument("--seed", type=int, default=42, help="Random seed") + parser.add_argument("--use-gpu", action="store_true", help="Whether to use GPU") + + parser.add_argument( + "--mlflow-tracking-uri", type=str, default=None, help="MLflow tracking URI" + ) + parser.add_argument( + "--mlflow-experiment-name", + type=str, + default="ood_detection_energy", + help="MLflow experiment name", + ) + + parser.add_argument( + "--log-performance", + action="store_true", + help="Whether to log performance metrics", + ) + + return parser.parse_args() + + +def prepare_configs(args): + """Prepare configurations based on command-line arguments.""" + # Create Energy config + energy_config = EnergyConfig( + name="energy_model", + model_type="energy", + pretrained_model=args.pretrained_model, + energy_temp=args.energy_temp, + learning_rate=args.learning_rate, + batch_size=args.batch_size, + epochs=args.epochs, + max_seq_length=args.max_seq_length, + seed=args.seed, + ) + + # Create training config + training_config = TRAINING_CONFIG + training_config.train_file = args.train_file or training_config.train_file + training_config.dev_file = args.dev_file or training_config.dev_file + training_config.test_file = args.test_file or training_config.test_file + training_config.ood_test_file = args.ood_file or training_config.ood_test_file + training_config.output_dir = args.output_dir or training_config.output_dir + training_config.mlflow_tracking_uri = ( + args.mlflow_tracking_uri or training_config.mlflow_tracking_uri + ) + training_config.mlflow_experiment_name = ( + args.mlflow_experiment_name or training_config.mlflow_experiment_name + ) + training_config.model_config = energy_config + + return training_config, energy_config + + +def train_and_evaluate( + config, + model_config, + use_energy_loss=False, + energy_margin=10.0, + energy_weight=0.1, + use_gpu=False, + log_performance=False, +): + """ + Train and evaluate the Energy-based OOD detection model. + + Args: + config: Training configuration + model_config: Model configuration + use_energy_loss: Whether to use energy-based loss during training + energy_margin: Margin for energy-based loss + energy_weight: Weight for energy loss term + use_gpu: Whether to use GPU + log_performance: Whether to log performance metrics + + Returns: + results: Dictionary of evaluation results + """ + # Set up directories + os.makedirs(config.output_dir, exist_ok=True) + model_dir = os.path.join(config.output_dir, "model") + os.makedirs(model_dir, exist_ok=True) + + # Set random seed + tf.random.set_seed(model_config.seed) + np.random.seed(model_config.seed) + + # Set up MLflow logger + mlflow_logger = MLflowLogger( + experiment_name=config.mlflow_experiment_name, + tracking_uri=config.mlflow_tracking_uri, + model_dir=model_dir, + artifact_dir=os.path.join(config.output_dir, "artifacts"), + ) + + # Start MLflow run + random_id = np.random.default_rng(40).standard_normal() + run_name = f"energy_{model_config.energy_temp}_{random_id:.4f}" + if use_energy_loss: + run_name += f"_margin{energy_margin}_weight{energy_weight}" + mlflow_logger.start_run(run_name=run_name) + + # Log parameters + params = { + "model_type": model_config.model_type, + "pretrained_model": model_config.pretrained_model, + "energy_temp": model_config.energy_temp, + "use_energy_loss": use_energy_loss, + "learning_rate": model_config.learning_rate, + "batch_size": model_config.batch_size, + "epochs": model_config.epochs, + "max_seq_length": model_config.max_seq_length, + "seed": model_config.seed, + } + + if use_energy_loss: + params.update({"energy_margin": energy_margin, "energy_weight": energy_weight}) + + mlflow_logger.log_params(params) + + # Load data + logger.info("Loading data...") + data_loader = DataLoader( + tokenizer_name=model_config.pretrained_model, + max_seq_length=model_config.max_seq_length, + batch_size=model_config.batch_size, + seed=model_config.seed, + ) + + train_dataset, label_map, num_train_examples = data_loader.load_data( + config.train_file, is_training=True + ) + + dev_dataset, _, num_dev_examples = data_loader.load_data( + config.dev_file, is_training=False + ) + + test_dataset, _, num_test_examples = data_loader.load_data( + config.test_file, is_training=False + ) + + if config.ood_test_file: + ood_test_dataset = data_loader.load_ood_data(config.ood_test_file, label_map) + else: + ood_test_dataset = None + + num_labels = len(label_map) + + # Log data information + mlflow_logger.log_params( + { + "num_train_examples": num_train_examples, + "num_dev_examples": num_dev_examples, + "num_test_examples": num_test_examples, + "num_labels": num_labels, + "label_map": label_map, + } + ) + + # Create model + logger.info("Creating Energy-based model...") + + model = EnergyModel( + pretrained_model=model_config.pretrained_model, + num_labels=num_labels, + hidden_dims=[768], # Add a hidden layer + dropout_rate=0.1, + energy_temp=model_config.energy_temp, + ) + + # Set up loss function + if use_energy_loss: + # Use energy-based loss + loss = EnergyLoss( + ood_label=-1, # OOD label for synthetic examples + energy_margin=energy_margin, + energy_weight=energy_weight, + ) + else: + # Use standard cross-entropy loss + loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) + + # Compile model + optimizer = tf.keras.optimizers.Adam(learning_rate=model_config.learning_rate) + metrics = [tf.keras.metrics.SparseCategoricalAccuracy(name="accuracy")] + + model.compile(optimizer=optimizer, loss=loss, metrics=metrics) + + # Create callbacks + callbacks = [ + tf.keras.callbacks.ModelCheckpoint( + filepath=os.path.join(model_dir, "checkpoint.keras"), + save_best_only=True, + monitor="val_accuracy", + mode="max", + ), + tf.keras.callbacks.EarlyStopping( + monitor="val_accuracy", patience=3, restore_best_weights=True + ), + ] + + # Train model + logger.info("Training model...") + training_start_time = time.time() + + # If using energy loss, we need to prepare special training data with OOD examples + if use_energy_loss and ood_test_dataset: + # Create a dataset that includes both ID and OOD examples + # First, extract a small portion of the OOD data for training + ood_train_examples = [] + ood_train_labels = [] + + # Take a small sample of OOD data (10% of the size of the training data) + num_ood_train = min(int(num_train_examples * 0.1), 500) + ood_count = 0 + + for batch in ood_test_dataset: + inputs, _ = batch + for i in range(len(inputs["input_ids"])): + if ood_count >= num_ood_train: + break + ood_train_examples.append( + { + "input_ids": inputs["input_ids"][i].numpy(), + "attention_mask": inputs["attention_mask"][i].numpy(), + } + ) + ood_train_labels.append(-1) # OOD label + ood_count += 1 + if ood_count >= num_ood_train: + break + + # Now extract the ID examples + id_train_examples = [] + id_train_labels = [] + + for batch in train_dataset: + inputs, labels = batch + for i in range(len(labels)): + id_train_examples.append( + { + "input_ids": inputs["input_ids"][i].numpy(), + "attention_mask": inputs["attention_mask"][i].numpy(), + } + ) + id_train_labels.append(labels[i].numpy()) + + # Combine ID and OOD examples + all_examples = id_train_examples + ood_train_examples + all_labels = id_train_labels + ood_train_labels + + # Shuffle + indices = np.arange(len(all_examples)) + np.random.shuffle(indices) + + shuffled_examples = [all_examples[i] for i in indices] + shuffled_labels = [all_labels[i] for i in indices] + + # Create batches + batched_inputs = [] + batched_labels = [] + + for i in range(0, len(shuffled_examples), model_config.batch_size): + batch_examples = shuffled_examples[i : i + model_config.batch_size] + batch_labels = shuffled_labels[i : i + model_config.batch_size] + + if ( + len(batch_examples) == model_config.batch_size + ): # Skip incomplete batches + # Stack examples + input_ids = np.stack([ex["input_ids"] for ex in batch_examples]) + attention_mask = np.stack( + [ex["attention_mask"] for ex in batch_examples] + ) + + batched_inputs.append( + {"input_ids": input_ids, "attention_mask": attention_mask} + ) + batched_labels.append(np.array(batch_labels)) + + # Convert to TensorFlow dataset + mixed_dataset = tf.data.Dataset.from_tensor_slices( + (batched_inputs, batched_labels) + ) + mixed_dataset = mixed_dataset.unbatch().batch(model_config.batch_size) + + # Train on the mixed dataset + history = model.fit( + mixed_dataset, + validation_data=dev_dataset, + epochs=model_config.epochs, + callbacks=callbacks, + verbose=1, + ) + else: + # Standard training on ID examples only + history = model.fit( + train_dataset, + validation_data=dev_dataset, + epochs=model_config.epochs, + callbacks=callbacks, + verbose=1, + ) + + training_time = time.time() - training_start_time + + # Log training metrics + for epoch, (loss, accuracy, val_loss, val_accuracy) in enumerate( + zip( + history.history["loss"], + history.history["accuracy"], + history.history["val_loss"], + history.history["val_accuracy"], + ) + ): + mlflow_logger.log_metrics( + { + "train_loss": loss, + "train_accuracy": accuracy, + "val_loss": val_loss, + "val_accuracy": val_accuracy, + }, + step=epoch, + ) + + # Log training history plot + visualizer = Visualizer( + output_dir=os.path.join(config.output_dir, "visualizations") + ) + + # Evaluate on test set + logger.info("Evaluating on test set...") + evaluation_results = {} + + # Get predictions and uncertainties + id_predictions = [] + id_uncertainties = [] + id_labels_list = [] + + for batch in test_dataset: + inputs, labels = batch + preds, uncertainties = model.predict_with_uncertainty(inputs) + + id_predictions.extend(preds.numpy()) + id_uncertainties.extend(uncertainties.numpy()) + id_labels_list.extend(labels.numpy()) + + id_predictions = np.array(id_predictions) + id_uncertainties = np.array(id_uncertainties) + id_labels = np.array(id_labels_list) + + # Evaluate OOD detection + if ood_test_dataset: + logger.info("Evaluating OOD detection...") + + ood_predictions = [] + ood_uncertainties = [] + + for batch in ood_test_dataset: + inputs, _ = batch + preds, uncertainties = model.predict_with_uncertainty(inputs) + + ood_predictions.extend(preds.numpy()) + ood_uncertainties.extend(uncertainties.numpy()) + + ood_predictions = np.array(ood_predictions) + ood_uncertainties = np.array(ood_uncertainties) + + # Compute OOD detection metrics + ood_metrics = OODMetrics() + metrics_dict = ood_metrics.compute_metrics( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + id_labels=id_labels, + id_predictions=id_predictions, + ) + + evaluation_results.update(metrics_dict) + + # Log OOD detection metrics + mlflow_logger.log_metrics(metrics_dict) + + # Create and log visualizations + roc_fig = visualizer.plot_roc_curve( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="Energy-based", + ) + mlflow_logger.log_figure(roc_fig, "visualizations/roc_curve.png") + + pr_fig = visualizer.plot_pr_curve( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="Energy-based", + ) + mlflow_logger.log_figure(pr_fig, "visualizations/pr_curve.png") + + dist_fig = visualizer.plot_score_distributions( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="Energy-based", + ) + mlflow_logger.log_figure(dist_fig, "visualizations/score_distributions.png") + + # Plot threshold impact + threshold_fig = visualizer.plot_uncertainty_threshold_impact( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="Energy-based", + ) + mlflow_logger.log_figure(threshold_fig, "visualizations/threshold_impact.png") + + # Plot confusion matrix + class_names = [k for k, v in sorted(label_map.items(), key=lambda x: x[1])] + cm_fig = visualizer.plot_confusion_matrix( + y_true=id_labels, + y_pred=id_predictions, + class_names=class_names, + model_name="Energy-based", + ) + mlflow_logger.log_figure(cm_fig, "visualizations/confusion_matrix.png") + + # Log performance metrics if requested + if log_performance: + logger.info("Measuring inference performance...") + inference_metrics = InferenceMetrics(use_gpu=use_gpu) + + # Get a sample batch + for batch in test_dataset.take(1): + sample_inputs, _ = batch + break + + # Measure inference time + inference_results = inference_metrics.measure_inference_time( + model=model, + inputs=sample_inputs, + batch_sizes=[1, 4, 8, 16, 32], + num_runs=50, + warmup_runs=10, + use_uncertainty=True, + ) + + # Log inference metrics + for batch_size, metrics in inference_results.items(): + mlflow_logger.log_metrics( + { + f"inference_time_bs{batch_size}_mean": metrics["mean"], + f"inference_time_bs{batch_size}_p95": metrics["p95"], + f"throughput_bs{batch_size}": metrics["throughput"], + } + ) + + # Generate and log inference time plot + time_fig = inference_metrics.plot_inference_times(inference_results) + mlflow_logger.log_figure(time_fig, "visualizations/inference_times.png") + + # Add inference metrics to evaluation results + evaluation_results["inference_performance"] = inference_results + evaluation_results["training_time"] = training_time + + # Save model + logger.info("Saving model...") + model_path = os.path.join(model_dir, "final_model.keras") + model.save(model_path) + + # Log model summary + mlflow_logger.log_model_summary(model) + + # Save evaluation results + results_path = os.path.join(config.output_dir, "evaluation_results.json") + with open(results_path, "w") as f: + json.dump(evaluation_results, f, indent=2) + + # Log evaluation results as artifact + mlflow_logger.log_artifact(results_path) + + # End MLflow run + mlflow_logger.end_run() + + return evaluation_results + + +def main(): + """Main function.""" + # Parse arguments + args = parse_args() + + # Prepare configurations + training_config, model_config = prepare_configs(args) + + # Set GPU usage + if args.use_gpu: + # Check if GPU is available + gpus = tf.config.list_physical_devices("GPU") + if gpus: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + logger.info(f"Using GPU: {len(gpus)} GPU(s) available") + except RuntimeError as e: + logger.error(f"Error setting GPU memory growth: {e}") + else: + logger.warning("No GPU found. Using CPU instead.") + else: + # Disable GPU + os.environ["CUDA_VISIBLE_DEVICES"] = "-1" + logger.info("Using CPU as requested") + + # Train and evaluate model + results = train_and_evaluate( + config=training_config, + model_config=model_config, + use_energy_loss=args.use_energy_loss, + energy_margin=args.energy_margin, + energy_weight=args.energy_weight, + use_gpu=args.use_gpu, + log_performance=args.log_performance, + ) + + logger.info("Completed Energy-based OOD detection training and evaluation") + + if "auroc" in results: + logger.info(f"AUROC: {results['auroc']:.4f}") + if "accuracy" in results: + logger.info(f"Accuracy: {results['accuracy']:.4f}") + if "fpr_at_95_tpr" in results: + logger.info(f"FPR@95%TPR: {results['fpr_at_95_tpr']:.4f}") + + +if __name__ == "__main__": + main() diff --git a/experiments/ood_detection/experiments/train_ood_class.py b/experiments/ood_detection/experiments/train_ood_class.py new file mode 100644 index 00000000..012eb1a6 --- /dev/null +++ b/experiments/ood_detection/experiments/train_ood_class.py @@ -0,0 +1,483 @@ +""" +Training script for OOD as a class model. +""" + +import os +import sys +import logging +import argparse +import tensorflow as tf +import numpy as np +import json +import time + +from config.config import TRAINING_CONFIG, OODClassConfig +from data.data_loader import DataLoader +from models.ood_class_model import OODClassModel +from evaluation.metrics import OODMetrics +from evaluation.inference_metrics import InferenceMetrics +from utils.mlflow_logger import MLflowLogger +from utils.visualization import Visualizer + + +# Setup paths +current_dir = os.path.dirname(os.path.abspath(__file__)) +project_root = os.path.dirname(current_dir) +if project_root not in sys.path: + sys.path.insert(0, project_root) + + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description="Train and evaluate OOD-as-class model" + ) + + parser.add_argument("--train-file", type=str, help="Path to training data") + parser.add_argument("--dev-file", type=str, help="Path to validation data") + parser.add_argument("--test-file", type=str, help="Path to test data") + parser.add_argument("--ood-file", type=str, help="Path to OOD test data") + + parser.add_argument( + "--output-dir", + type=str, + default="outputs/ood_class", + help="Directory to save outputs", + ) + parser.add_argument( + "--model-dir", + type=str, + default="models/ood_class", + help="Directory to save model", + ) + + parser.add_argument( + "--pretrained-model", + type=str, + default="xlm-roberta-base", + help="Pretrained model name or path", + ) + parser.add_argument( + "--max-seq-length", type=int, default=512, help="Maximum sequence length" + ) + parser.add_argument( + "--batch-size", + type=int, + default=32, + help="Batch size for training and evaluation", + ) + parser.add_argument( + "--epochs", type=int, default=10, help="Number of training epochs" + ) + parser.add_argument( + "--learning-rate", type=float, default=2e-5, help="Learning rate" + ) + + parser.add_argument( + "--synthetic-ood-ratio", + type=float, + default=0.2, + help="Ratio of synthetic OOD examples to generate", + ) + + parser.add_argument("--seed", type=int, default=42, help="Random seed") + parser.add_argument("--use-gpu", action="store_true", help="Whether to use GPU") + + parser.add_argument( + "--mlflow-tracking-uri", type=str, default=None, help="MLflow tracking URI" + ) + parser.add_argument( + "--mlflow-experiment-name", + type=str, + default="ood_detection_ood_class", + help="MLflow experiment name", + ) + + parser.add_argument( + "--log-performance", + action="store_true", + help="Whether to log performance metrics", + ) + + return parser.parse_args() + + +def prepare_configs(args): + """Prepare configurations based on command-line arguments.""" + # Create OOD class config + ood_class_config = OODClassConfig( + name="ood_class_model", + model_type="ood_class", + pretrained_model=args.pretrained_model, + synthetic_ood_ratio=args.synthetic_ood_ratio, + learning_rate=args.learning_rate, + batch_size=args.batch_size, + epochs=args.epochs, + max_seq_length=args.max_seq_length, + seed=args.seed, + ) + + # Create training config + training_config = TRAINING_CONFIG + training_config.train_file = args.train_file or training_config.train_file + training_config.dev_file = args.dev_file or training_config.dev_file + training_config.test_file = args.test_file or training_config.test_file + training_config.ood_test_file = args.ood_file or training_config.ood_test_file + training_config.output_dir = args.output_dir or training_config.output_dir + training_config.mlflow_tracking_uri = ( + args.mlflow_tracking_uri or training_config.mlflow_tracking_uri + ) + training_config.mlflow_experiment_name = ( + args.mlflow_experiment_name or training_config.mlflow_experiment_name + ) + training_config.model_config = ood_class_config + + return training_config, ood_class_config + + +def train_and_evaluate(config, model_config, use_gpu=False, log_performance=False): + """ + Train and evaluate the OOD-as-class model. + + Args: + config: Training configuration + model_config: Model configuration + use_gpu: Whether to use GPU + log_performance: Whether to log performance metrics + + Returns: + results: Dictionary of evaluation results + """ + # Set up directories + os.makedirs(config.output_dir, exist_ok=True) + model_dir = os.path.join(config.output_dir, "model") + os.makedirs(model_dir, exist_ok=True) + + # Set random seed + tf.random.set_seed(model_config.seed) + np.random.seed(model_config.seed) + + # Set up MLflow logger + mlflow_logger = MLflowLogger( + experiment_name=config.mlflow_experiment_name, + tracking_uri=config.mlflow_tracking_uri, + model_dir=model_dir, + artifact_dir=os.path.join(config.output_dir, "artifacts"), + ) + + # Start MLflow run + random_id = np.random.default_rng(40).standard_normal() + run_name = f"ood_class_{model_config.synthetic_ood_ratio}_{random_id:.4f}" + mlflow_logger.start_run(run_name=run_name) + + # Log parameters + mlflow_logger.log_params( + { + "model_type": model_config.model_type, + "pretrained_model": model_config.pretrained_model, + "synthetic_ood_ratio": model_config.synthetic_ood_ratio, + "learning_rate": model_config.learning_rate, + "batch_size": model_config.batch_size, + "epochs": model_config.epochs, + "max_seq_length": model_config.max_seq_length, + "seed": model_config.seed, + } + ) + + # Load data + logger.info("Loading data...") + data_loader = DataLoader( + tokenizer_name=model_config.pretrained_model, + max_seq_length=model_config.max_seq_length, + batch_size=model_config.batch_size, + seed=model_config.seed, + ) + + # Load regular training data first + train_dataset, label_map, num_train_examples = data_loader.load_data( + config.train_file, is_training=True + ) + + # Add OOD class to label map + num_id_classes = len(label_map) + label_map["OOD"] = num_id_classes + + # Create modified datasets with OOD class + train_dataset_with_ood = data_loader.create_synthetic_ood( + train_dataset, label_map, ratio=model_config.synthetic_ood_ratio + ) + + # Load validation and test data + dev_dataset, _, num_dev_examples = data_loader.load_data( + config.dev_file, is_training=False, add_ood_class=True + ) + + test_dataset, _, num_test_examples = data_loader.load_data( + config.test_file, is_training=False, add_ood_class=True + ) + + # Load OOD test data if available + if config.ood_test_file: + ood_test_dataset = data_loader.load_ood_data(config.ood_test_file, label_map) + else: + ood_test_dataset = None + + # Number of classes including OOD + num_labels = len(label_map) + + # Log data information + mlflow_logger.log_params( + { + "num_train_examples": num_train_examples, + "num_dev_examples": num_dev_examples, + "num_test_examples": num_test_examples, + "num_id_classes": num_id_classes, + "num_labels": num_labels, + "label_map": label_map, + } + ) + + # Create model + logger.info("Creating OOD-as-class model...") + model = OODClassModel( + pretrained_model=model_config.pretrained_model, + num_labels=num_id_classes, # Original number of classes, OOD will be added + hidden_dims=[768], # Add a hidden layer + dropout_rate=0.1, + synthetic_ood_ratio=model_config.synthetic_ood_ratio, + ) + # Compile model + optimizer = tf.keras.optimizers.Adam(learning_rate=model_config.learning_rate) + loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) + metrics = [tf.keras.metrics.SparseCategoricalAccuracy(name="accuracy")] + model.compile(optimizer=optimizer, loss=loss, metrics=metrics) + # Create callbacks + callbacks = [ + tf.keras.callbacks.ModelCheckpoint( + filepath=os.path.join(model_dir, "checkpoint.keras"), + save_best_only=True, + monitor="val_accuracy", + mode="max", + ), + tf.keras.callbacks.EarlyStopping( + monitor="val_accuracy", patience=3, restore_best_weights=True + ), + ] + # Train model + logger.info("Training model...") + training_start_time = time.time() + history = model.fit( + train_dataset_with_ood, + validation_data=dev_dataset, + epochs=model_config.epochs, + callbacks=callbacks, + verbose=1, + ) + training_time = time.time() - training_start_time + # Log training metrics + for epoch, (loss, accuracy, val_loss, val_accuracy) in enumerate( + zip( + history.history["loss"], + history.history["accuracy"], + history.history["val_loss"], + history.history["val_accuracy"], + ) + ): + mlflow_logger.log_metrics( + { + "train_loss": loss, + "train_accuracy": accuracy, + "val_loss": val_loss, + "val_accuracy": val_accuracy, + }, + step=epoch, + ) + # Log training history plot + visualizer = Visualizer( + output_dir=os.path.join(config.output_dir, "visualizations") + ) + # Evaluate on test set + logger.info("Evaluating on test set...") + evaluation_results = {} + # Get predictions and uncertainties + id_predictions = [] + id_uncertainties = [] + id_labels_list = [] + for batch in test_dataset: + inputs, labels = batch + preds, uncertainties = model.predict_with_uncertainty(inputs) + id_predictions.extend(preds.numpy()) + id_uncertainties.extend(uncertainties.numpy()) + id_labels_list.extend(labels.numpy()) + id_predictions = np.array(id_predictions) + id_uncertainties = np.array(id_uncertainties) + id_labels = np.array(id_labels_list) + # Evaluate OOD detection + if ood_test_dataset: + logger.info("Evaluating OOD detection...") + ood_predictions = [] + ood_uncertainties = [] + for batch in ood_test_dataset: + inputs, _ = batch + preds, uncertainties = model.predict_with_uncertainty(inputs) + ood_predictions.extend(preds.numpy()) + ood_uncertainties.extend(uncertainties.numpy()) + ood_predictions = np.array(ood_predictions) + ood_uncertainties = np.array(ood_uncertainties) + # Compute OOD detection metrics + ood_metrics = OODMetrics() + metrics_dict = ood_metrics.compute_metrics( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + id_labels=id_labels, + id_predictions=id_predictions, + ) + evaluation_results.update(metrics_dict) + # Log OOD detection metrics + mlflow_logger.log_metrics(metrics_dict) + # Create and log visualizations + roc_fig = visualizer.plot_roc_curve( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="OOD as Class", + ) + mlflow_logger.log_figure(roc_fig, "visualizations/roc_curve.png") + pr_fig = visualizer.plot_pr_curve( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="OOD as Class", + ) + mlflow_logger.log_figure(pr_fig, "visualizations/pr_curve.png") + dist_fig = visualizer.plot_score_distributions( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="OOD as Class", + ) + mlflow_logger.log_figure(dist_fig, "visualizations/score_distributions.png") + # Plot threshold impact + threshold_fig = visualizer.plot_uncertainty_threshold_impact( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="OOD as Class", + ) + mlflow_logger.log_figure(threshold_fig, "visualizations/threshold_impact.png") + # Plot confusion matrix + # Only include original classes, not OOD class + class_names = [ + k + for k, v in sorted(label_map.items(), key=lambda x: x[1]) + if k != "OOD" and v < num_id_classes + ] + # Filter labels and predictions to include only ID examples + id_mask = id_labels < num_id_classes + id_only_labels = id_labels[id_mask] + id_only_predictions = id_predictions[id_mask] + cm_fig = visualizer.plot_confusion_matrix( + y_true=id_only_labels, + y_pred=id_only_predictions, + class_names=class_names, + model_name="OOD as Class", + ) + mlflow_logger.log_figure(cm_fig, "visualizations/confusion_matrix.png") + # Log performance metrics if requested + if log_performance: + logger.info("Measuring inference performance...") + inference_metrics = InferenceMetrics(use_gpu=use_gpu) + # Get a sample batch + for batch in test_dataset.take(1): + sample_inputs, _ = batch + break + # Measure inference time + inference_results = inference_metrics.measure_inference_time( + model=model, + inputs=sample_inputs, + batch_sizes=[1, 4, 8, 16, 32], + num_runs=50, + warmup_runs=10, + use_uncertainty=True, + ) + # Log inference metrics + for batch_size, metrics in inference_results.items(): + mlflow_logger.log_metrics( + { + f"inference_time_bs{batch_size}_mean": metrics["mean"], + f"inference_time_bs{batch_size}_p95": metrics["p95"], + f"throughput_bs{batch_size}": metrics["throughput"], + } + ) + # Generate and log inference time plot + time_fig = inference_metrics.plot_inference_times(inference_results) + mlflow_logger.log_figure(time_fig, "visualizations/inference_times.png") + # Add inference metrics to evaluation results + evaluation_results["inference_performance"] = inference_results + evaluation_results["training_time"] = training_time + # Save model + logger.info("Saving model...") + model_path = os.path.join(model_dir, "final_model.keras") + model.save(model_path) + # Log model to MLflow + # Log model summary + mlflow_logger.log_model_summary(model) + # Save evaluation results + results_path = os.path.join(config.output_dir, "evaluation_results.json") + with open(results_path, "w") as f: + json.dump(evaluation_results, f, indent=2) + # Log evaluation results as artifact + mlflow_logger.log_artifact(results_path) + # End MLflow run + mlflow_logger.end_run() + return evaluation_results + + +def main(): + """Main function.""" + # Parse arguments + args = parse_args() + + # Prepare configurations + training_config, model_config = prepare_configs(args) + + # Set GPU usage + if args.use_gpu: + # Check if GPU is available + gpus = tf.config.list_physical_devices("GPU") + if gpus: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + logger.info(f"Using GPU: {len(gpus)} GPU(s) available") + except RuntimeError as e: + logger.error(f"Error setting GPU memory growth: {e}") + else: + logger.warning("No GPU found. Using CPU instead.") + else: + # Disable GPU + os.environ["CUDA_VISIBLE_DEVICES"] = "-1" + logger.info("Using CPU as requested") + + # Train and evaluate model + results = train_and_evaluate( + config=training_config, + model_config=model_config, + use_gpu=args.use_gpu, + log_performance=args.log_performance, + ) + + logger.info("Completed OOD-as-class training and evaluation") + + if "auroc" in results: + logger.info(f"AUROC: {results['auroc']:.4f}") + if "accuracy" in results: + logger.info(f"Accuracy: {results['accuracy']:.4f}") + if "fpr_at_95_tpr" in results: + logger.info(f"FPR@95%TPR: {results['fpr_at_95_tpr']:.4f}") + + +if __name__ == "__main__": + main() diff --git a/experiments/ood_detection/experiments/train_sngp.py b/experiments/ood_detection/experiments/train_sngp.py new file mode 100644 index 00000000..e73441ec --- /dev/null +++ b/experiments/ood_detection/experiments/train_sngp.py @@ -0,0 +1,545 @@ +""" +Training script for SNGP model. +""" + +import os +import sys +import logging +import argparse + +# Suppress TensorFlow logging BEFORE importing tensorflow +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" +os.environ["CUDA_VISIBLE_DEVICES"] = "" + +import tensorflow as tf +import numpy as np +import json +import time +from config.config import TRAINING_CONFIG, SNGPConfig +from data.data_loader import DataLoader +from models.sngp_model import SNGPModel, ResetCovarianceCallback +from evaluation.metrics import OODMetrics +from evaluation.inference_metrics import InferenceMetrics +from utils.mlflow_logger import MLflowLogger +from utils.visualization import Visualizer + + +# Further suppress TensorFlow logging +tf.get_logger().setLevel(logging.ERROR) + +# Suppress absl logging +try: + import absl.logging + + absl.logging.set_verbosity(absl.logging.ERROR) +except ImportError: + pass + +# Setup paths +current_dir = os.path.dirname(os.path.abspath(__file__)) +project_root = os.path.dirname(current_dir) +if project_root not in sys.path: + sys.path.insert(0, project_root) + + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Train and evaluate SNGP model") + + parser.add_argument("--train-file", type=str, help="Path to training data") + parser.add_argument("--dev-file", type=str, help="Path to validation data") + parser.add_argument("--test-file", type=str, help="Path to test data") + parser.add_argument("--ood-file", type=str, help="Path to OOD test data") + + parser.add_argument( + "--output-dir", + type=str, + default="outputs/sngp", + help="Directory to save outputs", + ) + parser.add_argument( + "--model-dir", type=str, default="models/sngp", help="Directory to save model" + ) + + parser.add_argument( + "--pretrained-model", + type=str, + default="xlm-roberta-base", + help="Pretrained model name or path", + ) + parser.add_argument( + "--max-seq-length", type=int, default=512, help="Maximum sequence length" + ) + parser.add_argument( + "--batch-size", + type=int, + default=32, + help="Batch size for training and evaluation", + ) + parser.add_argument( + "--epochs", type=int, default=10, help="Number of training epochs" + ) + parser.add_argument( + "--learning-rate", type=float, default=2e-5, help="Learning rate" + ) + + parser.add_argument( + "--spec-norm-bound", + type=float, + default=0.9, + help="Spectral normalization bound", + ) + parser.add_argument( + "--gp-hidden-dim", + type=int, + default=1024, + help="Gaussian process hidden dimension", + ) + parser.add_argument( + "--gp-scale-random-features", + action="store_true", + help="Whether to scale random features in GP layer", + ) + parser.add_argument( + "--gp-cov-momentum", + type=float, + default=-1.0, + help="Covariance momentum (-1.0 for exact calculation)", + ) + + parser.add_argument("--seed", type=int, default=42, help="Random seed") + parser.add_argument("--use-gpu", action="store_true", help="Whether to use GPU") + + parser.add_argument( + "--mlflow-tracking-uri", type=str, default=None, help="MLflow tracking URI" + ) + parser.add_argument( + "--mlflow-experiment-name", + type=str, + default="ood_detection_sngp", + help="MLflow experiment name", + ) + + parser.add_argument( + "--log-performance", + action="store_true", + help="Whether to log performance metrics", + ) + + return parser.parse_args() + + +def prepare_configs(args): + """Prepare configurations based on command-line arguments.""" + # Create SNGP config + sngp_config = SNGPConfig( + name="sngp_model", + model_type="sngp", + pretrained_model=args.pretrained_model, + spec_norm_bound=args.spec_norm_bound, + gp_hidden_dim=args.gp_hidden_dim, + gp_scale_random_features=args.gp_scale_random_features, + gp_cov_momentum=args.gp_cov_momentum, + learning_rate=args.learning_rate, + batch_size=args.batch_size, + epochs=args.epochs, + max_seq_length=args.max_seq_length, + seed=args.seed, + ) + + # Create training config + training_config = TRAINING_CONFIG + training_config.train_file = args.train_file or training_config.train_file + training_config.dev_file = args.dev_file or training_config.dev_file + training_config.test_file = args.test_file or training_config.test_file + training_config.ood_test_file = args.ood_file or training_config.ood_test_file + training_config.output_dir = args.output_dir or training_config.output_dir + training_config.mlflow_tracking_uri = ( + args.mlflow_tracking_uri or training_config.mlflow_tracking_uri + ) + training_config.mlflow_experiment_name = ( + args.mlflow_experiment_name or training_config.mlflow_experiment_name + ) + training_config.model_config = sngp_config + + return training_config, sngp_config + + +def train_and_evaluate(config, model_config, use_gpu=False, log_performance=False): + """ + Train and evaluate the SNGP model. + + Args: + config: Training configuration + model_config: Model configuration + use_gpu: Whether to use GPU + log_performance: Whether to log performance metrics + + Returns: + results: Dictionary of evaluation results + """ + # Set up directories + os.makedirs(config.output_dir, exist_ok=True) + model_dir = os.path.join(config.output_dir, "model") + os.makedirs(model_dir, exist_ok=True) + + # Set random seed + tf.random.set_seed(model_config.seed) + np.random.seed(model_config.seed) + + # Set up MLflow logger + mlflow_logger = MLflowLogger( + experiment_name=config.mlflow_experiment_name, + tracking_uri=config.mlflow_tracking_uri, + model_dir=model_dir, + artifact_dir=os.path.join(config.output_dir, "artifacts"), + ) + + # Start MLflow run + random_id = np.random.default_rng(40).standard_normal() + run_name = ( + f"sngp_{model_config.spec_norm_bound}_{model_config.gp_hidden_dim}_{random_id}" + ) + mlflow_logger.start_run(run_name=run_name) + + # Log parameters + mlflow_logger.log_params( + { + "model_type": model_config.model_type, + "pretrained_model": model_config.pretrained_model, + "spec_norm_bound": model_config.spec_norm_bound, + "gp_hidden_dim": model_config.gp_hidden_dim, + "gp_scale_random_features": model_config.gp_scale_random_features, + "gp_cov_momentum": model_config.gp_cov_momentum, + "learning_rate": model_config.learning_rate, + "batch_size": model_config.batch_size, + "epochs": model_config.epochs, + "max_seq_length": model_config.max_seq_length, + "seed": model_config.seed, + } + ) + + # Load data + logger.info("Loading data...") + data_loader = DataLoader( + tokenizer_name=model_config.pretrained_model, + max_seq_length=model_config.max_seq_length, + batch_size=model_config.batch_size, + seed=model_config.seed, + ) + + train_dataset, label_map, num_train_examples = data_loader.load_data( + config.train_file, is_training=True + ) + + dev_dataset, _, num_dev_examples = data_loader.load_data( + config.dev_file, is_training=False + ) + + test_dataset, _, num_test_examples = data_loader.load_data( + config.test_file, is_training=False + ) + + if config.ood_test_file: + ood_test_dataset = data_loader.load_ood_data(config.ood_test_file, label_map) + else: + ood_test_dataset = None + + num_labels = len(label_map) + + # Log data information + mlflow_logger.log_params( + { + "num_train_examples": num_train_examples, + "num_dev_examples": num_dev_examples, + "num_test_examples": num_test_examples, + "num_labels": num_labels, + "label_map": label_map, + } + ) + + # Create model + logger.info("Creating SNGP model...") + + model = SNGPModel( + pretrained_model=model_config.pretrained_model, + num_labels=num_labels, + hidden_dims=[768, 512], # Add a hidden layer + dropout_rate=0.1, + spec_norm_bound=model_config.spec_norm_bound, + gp_hidden_dim=model_config.gp_hidden_dim, + gp_scale_random_features=model_config.gp_scale_random_features, + gp_normalize_input=True, + gp_cov_momentum=model_config.gp_cov_momentum, + gp_cov_ridge_penalty=1.0, + ) + + # Compile model + optimizer = tf.keras.optimizers.Adam(learning_rate=model_config.learning_rate) + loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) + metrics = [tf.keras.metrics.SparseCategoricalAccuracy(name="accuracy")] + + model.compile(optimizer=optimizer, loss=loss, metrics=metrics) + + # Create callbacks + callbacks = [ + ResetCovarianceCallback(), + tf.keras.callbacks.ModelCheckpoint( + filepath=os.path.join(model_dir, "checkpoint.keras"), + save_best_only=True, + monitor="val_accuracy", + mode="max", + ), + tf.keras.callbacks.EarlyStopping( + monitor="val_accuracy", patience=3, restore_best_weights=True + ), + ] + + # Train model + logger.info("Training model...") + training_start_time = time.time() + + history = model.fit( + train_dataset, + validation_data=dev_dataset, + epochs=model_config.epochs, + callbacks=callbacks, + verbose=1, + ) + + training_time = time.time() - training_start_time + + # Log training metrics + for epoch, (loss, accuracy, val_loss, val_accuracy) in enumerate( + zip( + history.history["loss"], + history.history["accuracy"], + history.history["val_loss"], + history.history["val_accuracy"], + ) + ): + mlflow_logger.log_metrics( + { + "train_loss": loss, + "train_accuracy": accuracy, + "val_loss": val_loss, + "val_accuracy": val_accuracy, + }, + step=epoch, + ) + + # Log training history plot + visualizer = Visualizer( + output_dir=os.path.join(config.output_dir, "visualizations") + ) + + # Evaluate on test set + logger.info("Evaluating on test set...") + evaluation_results = {} + + # Get predictions and uncertainties + id_predictions = [] + id_uncertainties = [] + id_labels_list = [] + + for batch in test_dataset: + inputs, labels = batch + preds, uncertainties = model.predict_with_uncertainty(inputs) + preds_np = preds.numpy() + uncertainties_np = uncertainties.numpy() + labels_np = labels.numpy() + if len(preds_np.shape) == 0: # scalar + id_predictions.append(preds_np.item()) + id_uncertainties.append(uncertainties_np.item()) + else: # array + id_predictions.extend(preds_np.flatten()) + id_uncertainties.extend(uncertainties_np.flatten()) + if len(labels_np.shape) == 0: # scalar + id_labels_list.append(labels_np.item()) + else: # array + id_labels_list.extend(labels_np.flatten()) + id_predictions = np.array(id_predictions) + id_uncertainties = np.array(id_uncertainties) + id_labels = np.array(id_labels_list) + + # Evaluate OOD detection + if ood_test_dataset: + logger.info("Evaluating OOD detection...") + + ood_predictions = [] + ood_uncertainties = [] + + for batch in ood_test_dataset: + inputs, _ = batch + preds, uncertainties = model.predict_with_uncertainty(inputs) + preds_np = preds.numpy() + uncertainties_np = uncertainties.numpy() + if len(preds_np.shape) == 0: # scalar + ood_predictions.append(preds_np.item()) + ood_uncertainties.append(uncertainties_np.item()) + else: # array + ood_predictions.extend(preds_np.flatten()) + ood_uncertainties.extend(uncertainties_np.flatten()) + ood_predictions = np.array(ood_predictions) + ood_uncertainties = np.array(ood_uncertainties) + + # Compute OOD detection metrics + ood_metrics = OODMetrics() + metrics_dict = ood_metrics.compute_metrics( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + id_labels=id_labels, + id_predictions=id_predictions, + ) + + evaluation_results.update(metrics_dict) + + # Log OOD detection metrics + mlflow_logger.log_metrics(metrics_dict) + + # Create and log visualizations + roc_fig = visualizer.plot_roc_curve( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="SNGP", + ) + mlflow_logger.log_figure(roc_fig, "visualizations/roc_curve.png") + + pr_fig = visualizer.plot_pr_curve( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="SNGP", + ) + mlflow_logger.log_figure(pr_fig, "visualizations/pr_curve.png") + + dist_fig = visualizer.plot_score_distributions( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="SNGP", + ) + mlflow_logger.log_figure(dist_fig, "visualizations/score_distributions.png") + + # Plot confusion matrix + class_names = [k for k, v in sorted(label_map.items(), key=lambda x: x[1])] + cm_fig = visualizer.plot_confusion_matrix( + y_true=id_labels, + y_pred=id_predictions, + class_names=class_names, + model_name="SNGP", + ) + mlflow_logger.log_figure(cm_fig, "visualizations/confusion_matrix.png") + + # Log performance metrics if requested + if log_performance: + logger.info("Measuring inference performance...") + inference_metrics = InferenceMetrics(use_gpu=use_gpu) + + # Get a sample batch + for batch in test_dataset.take(1): + sample_inputs, _ = batch + break + + # Measure inference time + inference_results = inference_metrics.measure_inference_time( + model=model, + inputs=sample_inputs, + batch_sizes=[1, 4, 8, 16, 32], + num_runs=50, + warmup_runs=10, + use_uncertainty=True, + ) + + # Log inference metrics + for batch_size, metrics in inference_results.items(): + mlflow_logger.log_metrics( + { + f"inference_time_bs{batch_size}_mean": metrics["mean"], + f"inference_time_bs{batch_size}_p95": metrics["p95"], + f"throughput_bs{batch_size}": metrics["throughput"], + } + ) + + # Generate and log inference time plot + time_fig = inference_metrics.plot_inference_times(inference_results) + mlflow_logger.log_figure(time_fig, "visualizations/inference_times.png") + + # Add inference metrics to evaluation results + evaluation_results["inference_performance"] = inference_results + evaluation_results["training_time"] = training_time + + # Save model + logger.info("Saving model...") + model_path = os.path.join(model_dir, "final_model.keras") + model.save(model_path) + + # Log model to MLflow + + # Log model summary + mlflow_logger.log_model_summary(model) + + # Save evaluation results + results_path = os.path.join(config.output_dir, "evaluation_results.json") + with open(results_path, "w") as f: + json.dump(evaluation_results, f, indent=2) + + # Log evaluation results as artifact + mlflow_logger.log_artifact(results_path) + + # End MLflow run + mlflow_logger.end_run() + + return evaluation_results + + +def main(): + """Main function.""" + # Parse arguments + args = parse_args() + + # Prepare configurations + training_config, model_config = prepare_configs(args) + + # Set GPU usage + if args.use_gpu: + # Check if GPU is available + gpus = tf.config.list_physical_devices("GPU") + if gpus: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + logger.info(f"Using GPU: {len(gpus)} GPU(s) available") + except RuntimeError as e: + logger.error(f"Error setting GPU memory growth: {e}") + else: + logger.warning("No GPU found. Using CPU instead.") + else: + # Disable GPU + os.environ["CUDA_VISIBLE_DEVICES"] = "-1" + logger.info("Using CPU as requested") + + # Train and evaluate model + results = train_and_evaluate( + config=training_config, + model_config=model_config, + use_gpu=args.use_gpu, + log_performance=args.log_performance, + ) + + logger.info("Completed SNGP training and evaluation") + + if "auroc" in results: + logger.info(f"AUROC: {results['auroc']:.4f}") + if "accuracy" in results: + logger.info(f"Accuracy: {results['accuracy']:.4f}") + if "fpr_at_95_tpr" in results: + logger.info(f"FPR@95%TPR: {results['fpr_at_95_tpr']:.4f}") + + +if __name__ == "__main__": + main() diff --git a/experiments/ood_detection/experiments/train_sngp_energy.py b/experiments/ood_detection/experiments/train_sngp_energy.py new file mode 100644 index 00000000..78a69c2d --- /dev/null +++ b/experiments/ood_detection/experiments/train_sngp_energy.py @@ -0,0 +1,532 @@ +""" +Training script for combined SNGP + Energy model. +""" + +import os +import sys +import logging +import argparse +import tensorflow as tf +import numpy as np +import json +import time +from config.config import TRAINING_CONFIG, SNGPEnergyConfig +from data.data_loader import DataLoader +from models.sngp_energy_model import SNGPEnergyModel, SNGPEnergyLoss +from models.sngp_model import ResetCovarianceCallback +from evaluation.metrics import OODMetrics +from evaluation.inference_metrics import InferenceMetrics +from utils.mlflow_logger import MLflowLogger +from utils.visualization import Visualizer + +# Setup paths +current_dir = os.path.dirname(os.path.abspath(__file__)) +project_root = os.path.dirname(current_dir) +if project_root not in sys.path: + sys.path.insert(0, project_root) + + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description="Train and evaluate SNGP + Energy model" + ) + + parser.add_argument("--train-file", type=str, help="Path to training data") + parser.add_argument("--dev-file", type=str, help="Path to validation data") + parser.add_argument("--test-file", type=str, help="Path to test data") + parser.add_argument("--ood-file", type=str, help="Path to OOD test data") + + parser.add_argument( + "--output-dir", + type=str, + default="outputs/sngp_energy", + help="Directory to save outputs", + ) + parser.add_argument( + "--model-dir", + type=str, + default="models/sngp_energy", + help="Directory to save model", + ) + + parser.add_argument( + "--pretrained-model", + type=str, + default="xlm-roberta-base", + help="Pretrained model name or path", + ) + parser.add_argument( + "--max-seq-length", type=int, default=512, help="Maximum sequence length" + ) + parser.add_argument( + "--batch-size", + type=int, + default=32, + help="Batch size for training and evaluation", + ) + parser.add_argument( + "--epochs", type=int, default=10, help="Number of training epochs" + ) + parser.add_argument( + "--learning-rate", type=float, default=2e-5, help="Learning rate" + ) + + # SNGP parameters + parser.add_argument( + "--spec-norm-bound", + type=float, + default=0.9, + help="Spectral normalization bound", + ) + parser.add_argument( + "--gp-hidden-dim", + type=int, + default=1024, + help="Gaussian process hidden dimension", + ) + parser.add_argument( + "--gp-scale-random-features", + action="store_true", + help="Whether to scale random features in GP layer", + ) + parser.add_argument( + "--gp-cov-momentum", + type=float, + default=-1.0, + help="Covariance momentum (-1.0 for exact calculation)", + ) + + # Energy parameters + parser.add_argument( + "--energy-temp", type=float, default=1.0, help="Temperature for energy scoring" + ) + parser.add_argument( + "--energy-margin", type=float, default=10.0, help="Margin for energy-based loss" + ) + parser.add_argument( + "--energy-weight", type=float, default=0.1, help="Weight for energy loss term" + ) + + # Combination parameter + parser.add_argument( + "--alpha", + type=float, + default=0.5, + help="Weight for combining SNGP and Energy scores (0-1)", + ) + + parser.add_argument("--seed", type=int, default=42, help="Random seed") + parser.add_argument("--use-gpu", action="store_true", help="Whether to use GPU") + + parser.add_argument( + "--mlflow-tracking-uri", type=str, default=None, help="MLflow tracking URI" + ) + parser.add_argument( + "--mlflow-experiment-name", + type=str, + default="ood_detection_sngp_energy", + help="MLflow experiment name", + ) + + parser.add_argument( + "--log-performance", + action="store_true", + help="Whether to log performance metrics", + ) + + return parser.parse_args() + + +def prepare_configs(args): + """Prepare configurations based on command-line arguments.""" + # Create SNGP+Energy config + sngp_energy_config = SNGPEnergyConfig( + name="sngp_energy_model", + model_type="sngp_energy", + pretrained_model=args.pretrained_model, + spec_norm_bound=args.spec_norm_bound, + gp_hidden_dim=args.gp_hidden_dim, + gp_scale_random_features=args.gp_scale_random_features, + gp_cov_momentum=args.gp_cov_momentum, + energy_temp=args.energy_temp, + alpha=args.alpha, + learning_rate=args.learning_rate, + batch_size=args.batch_size, + epochs=args.epochs, + max_seq_length=args.max_seq_length, + seed=args.seed, + ) + + # Create training config + training_config = TRAINING_CONFIG + training_config.train_file = args.train_file or training_config.train_file + training_config.dev_file = args.dev_file or training_config.dev_file + training_config.test_file = args.test_file or training_config.test_file + training_config.ood_test_file = args.ood_file or training_config.ood_test_file + training_config.output_dir = args.output_dir or training_config.output_dir + training_config.mlflow_tracking_uri = ( + args.mlflow_tracking_uri or training_config.mlflow_tracking_uri + ) + training_config.mlflow_experiment_name = ( + args.mlflow_experiment_name or training_config.mlflow_experiment_name + ) + training_config.model_config = sngp_energy_config + + return training_config, sngp_energy_config + + +def train_and_evaluate( + config, + model_config, + energy_margin=10.0, + energy_weight=0.1, + use_gpu=False, + log_performance=False, +): + """ + Train and evaluate the SNGP+Energy model. + + Args: + config: Training configuration + model_config: Model configuration + energy_margin: Margin for energy-based loss + energy_weight: Weight for energy loss term + use_gpu: Whether to use GPU + log_performance: Whether to log performance metrics + + Returns: + results: Dictionary of evaluation results + """ + # Set up directories + os.makedirs(config.output_dir, exist_ok=True) + model_dir = os.path.join(config.output_dir, "model") + os.makedirs(model_dir, exist_ok=True) + + # Set random seed + tf.random.set_seed(model_config.seed) + np.random.seed(model_config.seed) + + # Set up MLflow logger + mlflow_logger = MLflowLogger( + experiment_name=config.mlflow_experiment_name, + tracking_uri=config.mlflow_tracking_uri, + model_dir=model_dir, + artifact_dir=os.path.join(config.output_dir, "artifacts"), + ) + + # Start MLflow run + random_id = np.random.default_rng(40).standard_normal() + run_name = f"sngp_energy_{model_config.spec_norm_bound}_{model_config.energy_temp}_{model_config.alpha}_{random_id:.4f}" + mlflow_logger.start_run(run_name=run_name) + + # Log parameters + mlflow_logger.log_params( + { + "model_type": model_config.model_type, + "pretrained_model": model_config.pretrained_model, + "spec_norm_bound": model_config.spec_norm_bound, + "gp_hidden_dim": model_config.gp_hidden_dim, + "gp_scale_random_features": model_config.gp_scale_random_features, + "gp_cov_momentum": model_config.gp_cov_momentum, + "energy_temp": model_config.energy_temp, + "energy_margin": energy_margin, + "energy_weight": energy_weight, + "alpha": model_config.alpha, + "learning_rate": model_config.learning_rate, + "batch_size": model_config.batch_size, + "epochs": model_config.epochs, + "max_seq_length": model_config.max_seq_length, + "seed": model_config.seed, + } + ) + + # Load data + logger.info("Loading data...") + data_loader = DataLoader( + tokenizer_name=model_config.pretrained_model, + max_seq_length=model_config.max_seq_length, + batch_size=model_config.batch_size, + seed=model_config.seed, + ) + + train_dataset, label_map, num_train_examples = data_loader.load_data( + config.train_file, is_training=True + ) + + dev_dataset, _, num_dev_examples = data_loader.load_data( + config.dev_file, is_training=False + ) + + test_dataset, _, num_test_examples = data_loader.load_data( + config.test_file, is_training=False + ) + + if config.ood_test_file: + ood_test_dataset = data_loader.load_ood_data(config.ood_test_file, label_map) + else: + ood_test_dataset = None + + num_labels = len(label_map) + + # Log data information + mlflow_logger.log_params( + { + "num_train_examples": num_train_examples, + "num_dev_examples": num_dev_examples, + "num_test_examples": num_test_examples, + "num_labels": num_labels, + "label_map": label_map, + } + ) + + # Create model + logger.info("Creating SNGP+Energy model...") + model = SNGPEnergyModel( + pretrained_model=model_config.pretrained_model, + num_labels=num_labels, + hidden_dims=[768], # Add a hidden layer + dropout_rate=0.1, + spec_norm_bound=model_config.spec_norm_bound, + gp_hidden_dim=model_config.gp_hidden_dim, + gp_scale_random_features=model_config.gp_scale_random_features, + gp_normalize_input=True, + gp_cov_momentum=model_config.gp_cov_momentum, + gp_cov_ridge_penalty=1.0, + energy_temp=model_config.energy_temp, + alpha=model_config.alpha, + ) + # Custom loss function that combines cross-entropy with energy-based loss + loss = SNGPEnergyLoss( + ood_label=-1, # OOD label in dataset + energy_margin=energy_margin, + energy_weight=energy_weight, + ) + # Compile model + optimizer = tf.keras.optimizers.Adam(learning_rate=model_config.learning_rate) + metrics = [tf.keras.metrics.SparseCategoricalAccuracy(name="accuracy")] + model.compile(optimizer=optimizer, loss=loss, metrics=metrics) + # Create callbacks + callbacks = [ + ResetCovarianceCallback(), + tf.keras.callbacks.ModelCheckpoint( + filepath=os.path.join(model_dir, "checkpoint.keras"), + save_best_only=True, + monitor="val_accuracy", + mode="max", + ), + tf.keras.callbacks.EarlyStopping( + monitor="val_accuracy", patience=3, restore_best_weights=True + ), + ] + # Train model + logger.info("Training model...") + training_start_time = time.time() + history = model.fit( + train_dataset, + validation_data=dev_dataset, + epochs=model_config.epochs, + callbacks=callbacks, + verbose=1, + ) + training_time = time.time() - training_start_time + # Log training metrics + for epoch, (loss, accuracy, val_loss, val_accuracy) in enumerate( + zip( + history.history["loss"], + history.history["accuracy"], + history.history["val_loss"], + history.history["val_accuracy"], + ) + ): + mlflow_logger.log_metrics( + { + "train_loss": loss, + "train_accuracy": accuracy, + "val_loss": val_loss, + "val_accuracy": val_accuracy, + }, + step=epoch, + ) + # Log training history plot + visualizer = Visualizer( + output_dir=os.path.join(config.output_dir, "visualizations") + ) + # Evaluate on test set + logger.info("Evaluating on test set...") + evaluation_results = {} + # Get predictions and uncertainties + id_predictions = [] + id_uncertainties = [] + id_labels_list = [] + for batch in test_dataset: + inputs, labels = batch + preds, uncertainties = model.predict_with_uncertainty(inputs) + id_predictions.extend(preds.numpy()) + id_uncertainties.extend(uncertainties.numpy()) + id_labels_list.extend(labels.numpy()) + id_predictions = np.array(id_predictions) + id_uncertainties = np.array(id_uncertainties) + id_labels = np.array(id_labels_list) + # Evaluate OOD detection + if ood_test_dataset: + logger.info("Evaluating OOD detection...") + ood_predictions = [] + ood_uncertainties = [] + for batch in ood_test_dataset: + inputs, _ = batch + preds, uncertainties = model.predict_with_uncertainty(inputs) + ood_predictions.extend(preds.numpy()) + ood_uncertainties.extend(uncertainties.numpy()) + ood_predictions = np.array(ood_predictions) + ood_uncertainties = np.array(ood_uncertainties) + # Compute OOD detection metrics + ood_metrics = OODMetrics() + metrics_dict = ood_metrics.compute_metrics( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + id_labels=id_labels, + id_predictions=id_predictions, + ) + evaluation_results.update(metrics_dict) + # Log OOD detection metrics + mlflow_logger.log_metrics(metrics_dict) + # Create and log visualizations + roc_fig = visualizer.plot_roc_curve( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="SNGP+Energy", + ) + mlflow_logger.log_figure(roc_fig, "visualizations/roc_curve.png") + pr_fig = visualizer.plot_pr_curve( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="SNGP+Energy", + ) + mlflow_logger.log_figure(pr_fig, "visualizations/pr_curve.png") + dist_fig = visualizer.plot_score_distributions( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="SNGP+Energy", + ) + mlflow_logger.log_figure(dist_fig, "visualizations/score_distributions.png") + # Plot threshold impact + threshold_fig = visualizer.plot_uncertainty_threshold_impact( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name="SNGP+Energy", + ) + mlflow_logger.log_figure(threshold_fig, "visualizations/threshold_impact.png") + # Plot confusion matrix + class_names = [k for k, v in sorted(label_map.items(), key=lambda x: x[1])] + cm_fig = visualizer.plot_confusion_matrix( + y_true=id_labels, + y_pred=id_predictions, + class_names=class_names, + model_name="SNGP+Energy", + ) + mlflow_logger.log_figure(cm_fig, "visualizations/confusion_matrix.png") + # Log performance metrics if requested + if log_performance: + logger.info("Measuring inference performance...") + inference_metrics = InferenceMetrics(use_gpu=use_gpu) + # Get a sample batch + for batch in test_dataset.take(1): + sample_inputs, _ = batch + break + # Measure inference time + inference_results = inference_metrics.measure_inference_time( + model=model, + inputs=sample_inputs, + batch_sizes=[1, 4, 8, 16, 32], + num_runs=50, + warmup_runs=10, + use_uncertainty=True, + ) + # Log inference metrics + for batch_size, metrics in inference_results.items(): + mlflow_logger.log_metrics( + { + f"inference_time_bs{batch_size}_mean": metrics["mean"], + f"inference_time_bs{batch_size}_p95": metrics["p95"], + f"throughput_bs{batch_size}": metrics["throughput"], + } + ) + # Generate and log inference time plot + time_fig = inference_metrics.plot_inference_times(inference_results) + mlflow_logger.log_figure(time_fig, "visualizations/inference_times.png") + # Add inference metrics to evaluation results + evaluation_results["inference_performance"] = inference_results + evaluation_results["training_time"] = training_time + # Save model + logger.info("Saving model...") + model_path = os.path.join(model_dir, "final_model.keras") + model.save(model_path) + # Log model to MLflow + # Log model summary + mlflow_logger.log_model_summary(model) + # Save evaluation results + results_path = os.path.join(config.output_dir, "evaluation_results.json") + with open(results_path, "w") as f: + json.dump(evaluation_results, f, indent=2) + # Log evaluation results as artifact + mlflow_logger.log_artifact(results_path) + # End MLflow run + mlflow_logger.end_run() + return evaluation_results + + +def main(): + """Main function.""" + # Parse arguments + args = parse_args() + + # Prepare configurations + training_config, model_config = prepare_configs(args) + + # Set GPU usage + if args.use_gpu: + # Check if GPU is available + gpus = tf.config.list_physical_devices("GPU") + if gpus: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + logger.info(f"Using GPU: {len(gpus)} GPU(s) available") + except RuntimeError as e: + logger.error(f"Error setting GPU memory growth: {e}") + else: + logger.warning("No GPU found. Using CPU instead.") + else: + # Disable GPU + os.environ["CUDA_VISIBLE_DEVICES"] = "-1" + logger.info("Using CPU as requested") + + # Train and evaluate model + results = train_and_evaluate( + config=training_config, + model_config=model_config, + energy_margin=args.energy_margin, + energy_weight=args.energy_weight, + use_gpu=args.use_gpu, + log_performance=args.log_performance, + ) + + logger.info("Completed SNGP+Energy training and evaluation") + + if "auroc" in results: + logger.info(f"AUROC: {results['auroc']:.4f}") + if "accuracy" in results: + logger.info(f"Accuracy: {results['accuracy']:.4f}") + if "fpr_at_95_tpr" in results: + logger.info(f"FPR@95%TPR: {results['fpr_at_95_tpr']:.4f}") + + +if __name__ == "__main__": + main() diff --git a/experiments/ood_detection/experiments/train_softmax.py b/experiments/ood_detection/experiments/train_softmax.py new file mode 100644 index 00000000..47b26dea --- /dev/null +++ b/experiments/ood_detection/experiments/train_softmax.py @@ -0,0 +1,507 @@ +""" +Training script for Softmax threshold OOD detection model. +""" + +import os +import sys +import logging +import argparse +import tensorflow as tf +import numpy as np +import json +import time +from config.config import TRAINING_CONFIG, SoftmaxConfig +from data.data_loader import DataLoader +from models.softmax_model import SoftmaxModel, TemperatureScaling, CalibrationCallback +from evaluation.metrics import OODMetrics +from evaluation.inference_metrics import InferenceMetrics +from utils.mlflow_logger import MLflowLogger +from utils.visualization import Visualizer + +# Setup paths +current_dir = os.path.dirname(os.path.abspath(__file__)) +project_root = os.path.dirname(current_dir) +if project_root not in sys.path: + sys.path.insert(0, project_root) + + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description="Train and evaluate Softmax threshold OOD detection model" + ) + + parser.add_argument("--train-file", type=str, help="Path to training data") + parser.add_argument("--dev-file", type=str, help="Path to validation data") + parser.add_argument("--test-file", type=str, help="Path to test data") + parser.add_argument("--ood-file", type=str, help="Path to OOD test data") + + parser.add_argument( + "--output-dir", + type=str, + default="outputs/softmax", + help="Directory to save outputs", + ) + parser.add_argument( + "--model-dir", + type=str, + default="models/softmax", + help="Directory to save model", + ) + + parser.add_argument( + "--pretrained-model", + type=str, + default="xlm-roberta-base", + help="Pretrained model name or path", + ) + parser.add_argument( + "--max-seq-length", type=int, default=512, help="Maximum sequence length" + ) + parser.add_argument( + "--batch-size", + type=int, + default=32, + help="Batch size for training and evaluation", + ) + parser.add_argument( + "--epochs", type=int, default=10, help="Number of training epochs" + ) + parser.add_argument( + "--learning-rate", type=float, default=2e-5, help="Learning rate" + ) + + parser.add_argument( + "--temperature", type=float, default=1.0, help="Initial temperature for scaling" + ) + parser.add_argument( + "--use-entropy", + action="store_true", + help="Use entropy instead of max probability for uncertainty", + ) + parser.add_argument( + "--calibrate", + action="store_true", + help="Perform temperature scaling calibration", + ) + + parser.add_argument("--seed", type=int, default=42, help="Random seed") + parser.add_argument("--use-gpu", action="store_true", help="Whether to use GPU") + + parser.add_argument( + "--mlflow-tracking-uri", type=str, default=None, help="MLflow tracking URI" + ) + parser.add_argument( + "--mlflow-experiment-name", + type=str, + default="ood_detection_softmax", + help="MLflow experiment name", + ) + + parser.add_argument( + "--log-performance", + action="store_true", + help="Whether to log performance metrics", + ) + + return parser.parse_args() + + +def prepare_configs(args): + """Prepare configurations based on command-line arguments.""" + # Create Softmax config + softmax_config = SoftmaxConfig( + name="softmax_model", + model_type="softmax", + pretrained_model=args.pretrained_model, + temperature=args.temperature, + use_entropy=args.use_entropy, + learning_rate=args.learning_rate, + batch_size=args.batch_size, + epochs=args.epochs, + max_seq_length=args.max_seq_length, + seed=args.seed, + ) + + # Create training config + training_config = TRAINING_CONFIG + training_config.train_file = args.train_file or training_config.train_file + training_config.dev_file = args.dev_file or training_config.dev_file + training_config.test_file = args.test_file or training_config.test_file + training_config.ood_test_file = args.ood_file or training_config.ood_test_file + training_config.output_dir = args.output_dir or training_config.output_dir + training_config.mlflow_tracking_uri = ( + args.mlflow_tracking_uri or training_config.mlflow_tracking_uri + ) + training_config.mlflow_experiment_name = ( + args.mlflow_experiment_name or training_config.mlflow_experiment_name + ) + training_config.model_config = softmax_config + + return training_config, softmax_config + + +def train_and_evaluate( + config, model_config, calibrate=False, use_gpu=False, log_performance=False +): + """ + Train and evaluate the Softmax threshold OOD detection model. + + Args: + config: Training configuration + model_config: Model configuration + calibrate: Whether to perform temperature scaling calibration + use_gpu: Whether to use GPU + log_performance: Whether to log performance metrics + + Returns: + results: Dictionary of evaluation results + """ + # Set up directories + os.makedirs(config.output_dir, exist_ok=True) + model_dir = os.path.join(config.output_dir, "model") + os.makedirs(model_dir, exist_ok=True) + + # Set random seed + tf.random.set_seed(model_config.seed) + np.random.seed(model_config.seed) + + # Set up MLflow logger + mlflow_logger = MLflowLogger( + experiment_name=config.mlflow_experiment_name, + tracking_uri=config.mlflow_tracking_uri, + model_dir=model_dir, + artifact_dir=os.path.join(config.output_dir, "artifacts"), + ) + + # Start MLflow run + method_name = "entropy" if model_config.use_entropy else "max_prob" + calibration_name = "_calibrated" if calibrate else "" + random_id = np.random.default_rng(40).standard_normal() + run_name = f"softmax_{method_name}{calibration_name}_{model_config.temperature}_{random_id:.4f}" + mlflow_logger.start_run(run_name=run_name) + + # Log parameters + mlflow_logger.log_params( + { + "model_type": model_config.model_type, + "pretrained_model": model_config.pretrained_model, + "temperature": model_config.temperature, + "use_entropy": model_config.use_entropy, + "calibrate": calibrate, + "learning_rate": model_config.learning_rate, + "batch_size": model_config.batch_size, + "epochs": model_config.epochs, + "max_seq_length": model_config.max_seq_length, + "seed": model_config.seed, + } + ) + + # Load data + logger.info("Loading data...") + data_loader = DataLoader( + tokenizer_name=model_config.pretrained_model, + max_seq_length=model_config.max_seq_length, + batch_size=model_config.batch_size, + seed=model_config.seed, + ) + + train_dataset, label_map, num_train_examples = data_loader.load_data( + config.train_file, is_training=True + ) + + dev_dataset, _, num_dev_examples = data_loader.load_data( + config.dev_file, is_training=False + ) + + test_dataset, _, num_test_examples = data_loader.load_data( + config.test_file, is_training=False + ) + + if config.ood_test_file: + ood_test_dataset = data_loader.load_ood_data(config.ood_test_file, label_map) + else: + ood_test_dataset = None + + num_labels = len(label_map) + + # Log data information + mlflow_logger.log_params( + { + "num_train_examples": num_train_examples, + "num_dev_examples": num_dev_examples, + "num_test_examples": num_test_examples, + "num_labels": num_labels, + "label_map": label_map, + } + ) + + # Create model + logger.info("Creating Softmax threshold model...") + base_model = SoftmaxModel( + pretrained_model=model_config.pretrained_model, + num_labels=num_labels, + hidden_dims=[768], # Add a hidden layer + dropout_rate=0.1, + temperature=model_config.temperature, + use_entropy=model_config.use_entropy, + ) + # Compile model + optimizer = tf.keras.optimizers.Adam(learning_rate=model_config.learning_rate) + loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) + metrics = [tf.keras.metrics.SparseCategoricalAccuracy(name="accuracy")] + base_model.compile(optimizer=optimizer, loss=loss, metrics=metrics) + # Create callbacks + callbacks = [ + tf.keras.callbacks.ModelCheckpoint( + filepath=os.path.join(model_dir, "checkpoint.keras"), + save_best_only=True, + monitor="val_accuracy", + mode="max", + ), + tf.keras.callbacks.EarlyStopping( + monitor="val_accuracy", patience=3, restore_best_weights=True + ), + ] + # Train model + logger.info("Training model...") + training_start_time = time.time() + history = base_model.fit( + train_dataset, + validation_data=dev_dataset, + epochs=model_config.epochs, + callbacks=callbacks, + verbose=1, + ) + training_time = time.time() - training_start_time + # Log training metrics + for epoch, (loss, accuracy, val_loss, val_accuracy) in enumerate( + zip( + history.history["loss"], + history.history["accuracy"], + history.history["val_loss"], + history.history["val_accuracy"], + ) + ): + mlflow_logger.log_metrics( + { + "train_loss": loss, + "train_accuracy": accuracy, + "val_loss": val_loss, + "val_accuracy": val_accuracy, + }, + step=epoch, + ) + # If calibration is requested, add temperature scaling layer + if calibrate: + logger.info("Calibrating model with temperature scaling...") + # Create a new model with temperature scaling layer + model = tf.keras.Sequential() + model.add(base_model) + model.add(TemperatureScaling(temperature=model_config.temperature)) + # Add temperature_layer attribute for access by the callback + model.temperature_layer = model.layers[-1] + # Compile the model + model.compile(optimizer=optimizer, loss=loss, metrics=metrics) + # Perform calibration on dev set + calibration_callback = CalibrationCallback( + calibration_data=dev_dataset, patience=5, verbose=1 + ) + # Run calibration + calibration_callback.on_train_end() + # Get final temperature + final_temp = model.temperature_layer.temp.numpy() + logger.info(f"Calibration complete. Final temperature: {final_temp:.4f}") + # Log calibration results + mlflow_logger.log_metrics({"calibration_temperature": final_temp}) + else: + # Use the base model directly + model = base_model + # Save the final model for evaluation + # If calibration was done, this is the calibrated model + # Evaluate on test set + logger.info("Evaluating on test set...") + evaluation_results = {} + # Get predictions and uncertainties + id_predictions = [] + id_uncertainties = [] + id_labels_list = [] + for batch in test_dataset: + inputs, labels = batch + preds, uncertainties = model.predict_with_uncertainty(inputs) + id_predictions.extend(preds.numpy()) + id_uncertainties.extend(uncertainties.numpy()) + id_labels_list.extend(labels.numpy()) + id_predictions = np.array(id_predictions) + id_uncertainties = np.array(id_uncertainties) + id_labels = np.array(id_labels_list) + # Evaluate OOD detection + if ood_test_dataset: + logger.info("Evaluating OOD detection...") + ood_predictions = [] + ood_uncertainties = [] + for batch in ood_test_dataset: + inputs, _ = batch + preds, uncertainties = model.predict_with_uncertainty(inputs) + ood_predictions.extend(preds.numpy()) + ood_uncertainties.extend(uncertainties.numpy()) + ood_predictions = np.array(ood_predictions) + ood_uncertainties = np.array(ood_uncertainties) + # Compute OOD detection metrics + ood_metrics = OODMetrics() + metrics_dict = ood_metrics.compute_metrics( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + id_labels=id_labels, + id_predictions=id_predictions, + ) + evaluation_results.update(metrics_dict) + # Log OOD detection metrics + mlflow_logger.log_metrics(metrics_dict) + visualizer = Visualizer( + output_dir=os.path.join(config.output_dir, "visualizations") + ) + # Create and log visualizations + model_name = f"Softmax ({method_name.capitalize()})" + if calibrate: + model_name += f" Calibrated (T={final_temp:.2f})" + roc_fig = visualizer.plot_roc_curve( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name=model_name, + ) + mlflow_logger.log_figure(roc_fig, "visualizations/roc_curve.png") + pr_fig = visualizer.plot_pr_curve( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name=model_name, + ) + mlflow_logger.log_figure(pr_fig, "visualizations/pr_curve.png") + dist_fig = visualizer.plot_score_distributions( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name=model_name, + ) + mlflow_logger.log_figure(dist_fig, "visualizations/score_distributions.png") + # Plot threshold impact + threshold_fig = visualizer.plot_uncertainty_threshold_impact( + id_uncertainties=id_uncertainties, + ood_uncertainties=ood_uncertainties, + model_name=model_name, + ) + mlflow_logger.log_figure(threshold_fig, "visualizations/threshold_impact.png") + # Plot confusion matrix + class_names = [k for k, v in sorted(label_map.items(), key=lambda x: x[1])] + cm_fig = visualizer.plot_confusion_matrix( + y_true=id_labels, + y_pred=id_predictions, + class_names=class_names, + model_name=model_name, + ) + mlflow_logger.log_figure(cm_fig, "visualizations/confusion_matrix.png") + # Log performance metrics if requested + if log_performance: + logger.info("Measuring inference performance...") + inference_metrics = InferenceMetrics(use_gpu=use_gpu) + # Get a sample batch + for batch in test_dataset.take(1): + sample_inputs, _ = batch + break + # Measure inference time + inference_results = inference_metrics.measure_inference_time( + model=model, + inputs=sample_inputs, + batch_sizes=[1, 4, 8, 16, 32], + num_runs=50, + warmup_runs=10, + use_uncertainty=True, + ) + # Log inference metrics + for batch_size, metrics in inference_results.items(): + mlflow_logger.log_metrics( + { + f"inference_time_bs{batch_size}_mean": metrics["mean"], + f"inference_time_bs{batch_size}_p95": metrics["p95"], + f"throughput_bs{batch_size}": metrics["throughput"], + } + ) + # Generate and log inference time plot + time_fig = inference_metrics.plot_inference_times(inference_results) + mlflow_logger.log_figure(time_fig, "visualizations/inference_times.png") + # Add inference metrics to evaluation results + evaluation_results["inference_performance"] = inference_results + evaluation_results["training_time"] = training_time + # Save model + logger.info("Saving model...") + model_path = os.path.join(model_dir, "final_model.keras") + model.save(model_path) + # Log model to MLflow + # Log model summary + mlflow_logger.log_model_summary(model) + # Save evaluation results + results_path = os.path.join(config.output_dir, "evaluation_results.json") + with open(results_path, "w") as f: + json.dump(evaluation_results, f, indent=2) + # Log evaluation results as artifact + mlflow_logger.log_artifact(results_path) + # End MLflow run + mlflow_logger.end_run() + return evaluation_results + + +def main(): + """Main function.""" + # Parse arguments + args = parse_args() + + # Prepare configurations + training_config, model_config = prepare_configs(args) + + # Set up visualization directory + Visualizer(output_dir=os.path.join(training_config.output_dir, "visualizations")) + + # Set GPU usage + if args.use_gpu: + # Check if GPU is available + gpus = tf.config.list_physical_devices("GPU") + if gpus: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + logger.info(f"Using GPU: {len(gpus)} GPU(s) available") + except RuntimeError as e: + logger.error(f"Error setting GPU memory growth: {e}") + else: + logger.warning("No GPU found. Using CPU instead.") + else: + # Disable GPU + os.environ["CUDA_VISIBLE_DEVICES"] = "-1" + logger.info("Using CPU as requested") + + # Train and evaluate model + results = train_and_evaluate( + config=training_config, + model_config=model_config, + calibrate=args.calibrate, + use_gpu=args.use_gpu, + log_performance=args.log_performance, + ) + + logger.info("Completed Softmax threshold OOD detection training and evaluation") + + if "auroc" in results: + logger.info(f"AUROC: {results['auroc']:.4f}") + if "accuracy" in results: + logger.info(f"Accuracy: {results['accuracy']:.4f}") + if "fpr_at_95_tpr" in results: + logger.info(f"FPR@95%TPR: {results['fpr_at_95_tpr']:.4f}") + + +if __name__ == "__main__": + main() diff --git a/experiments/ood_detection/main.py b/experiments/ood_detection/main.py new file mode 100644 index 00000000..3c9ac727 --- /dev/null +++ b/experiments/ood_detection/main.py @@ -0,0 +1,735 @@ +""" +Main entry point for running experiments. +""" + +import os +import sys +import logging +import argparse +import tensorflow as tf +import numpy as np +import json +from datetime import datetime +from utils.visualization import Visualizer + +# Setup Python path +project_root = os.path.dirname(os.path.abspath(__file__)) +if project_root not in sys.path: + sys.path.insert(0, project_root) + + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Run OOD detection experiments") + + parser.add_argument( + "--experiment", + type=str, + choices=["sngp", "energy", "sngp_energy", "ood_class", "softmax", "all"], + default="all", + help="Experiment to run", + ) + + parser.add_argument("--train-file", type=str, help="Path to training data") + parser.add_argument("--dev-file", type=str, help="Path to validation data") + parser.add_argument("--test-file", type=str, help="Path to test data") + parser.add_argument("--ood-file", type=str, help="Path to OOD test data") + + parser.add_argument( + "--output-dir", type=str, default="outputs", help="Directory to save outputs" + ) + + parser.add_argument( + "--batch-size", + type=int, + default=32, + help="Batch size for training and evaluation", + ) + parser.add_argument( + "--epochs", type=int, default=10, help="Number of training epochs" + ) + parser.add_argument("--seed", type=int, default=42, help="Random seed") + + parser.add_argument("--use-gpu", action="store_true", help="Whether to use GPU") + + parser.add_argument( + "--mlflow-tracking-uri", type=str, default=None, help="MLflow tracking URI" + ) + parser.add_argument( + "--mlflow-experiment-name", + type=str, + default="ood_detection", + help="MLflow experiment name", + ) + + parser.add_argument( + "--log-performance", + action="store_true", + help="Whether to log performance metrics", + ) + + parser.add_argument( + "--analyze", action="store_true", help="Run analysis after experiments" + ) + + return parser.parse_args() + + +def run_experiment(experiment_name, args): + """ + Run an experiment. + + Args: + experiment_name: Name of the experiment + args: Command-line arguments + + Returns: + results: Dictionary of evaluation results + """ + logger.info(f"Running experiment: {experiment_name}") + + # Set up experiment-specific script + script_name = f"train_{experiment_name}.py" + script_path = os.path.join("experiments", script_name) + + if not os.path.exists(script_path): + logger.error(f"Script not found: {script_path}") + return None + + # Set up experiment-specific output directory + experiment_output_dir = os.path.join(args.output_dir, experiment_name) + os.makedirs(experiment_output_dir, exist_ok=True) + + # Construct command + cmd_args = [ + f"--train-file={args.train_file}" if args.train_file else "", + f"--dev-file={args.dev_file}" if args.dev_file else "", + f"--test-file={args.test_file}" if args.test_file else "", + f"--ood-file={args.ood_file}" if args.ood_file else "", + f"--output-dir={experiment_output_dir}", + f"--batch-size={args.batch_size}", + f"--epochs={args.epochs}", + f"--seed={args.seed}", + "--use-gpu" if args.use_gpu else "", + ( + f"--mlflow-tracking-uri={args.mlflow_tracking_uri}" + if args.mlflow_tracking_uri + else "" + ), + f"--mlflow-experiment-name={args.mlflow_experiment_name}_{experiment_name}", + "--log-performance" if args.log_performance else "", + ] + + cmd_args = [arg for arg in cmd_args if arg] # Remove empty args + + # Run the script + cmd = f"python {script_path} {' '.join(cmd_args)}" + logger.info(f"Running command: {cmd}") + + import subprocess + + try: + subprocess.run(cmd, shell=True, check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Error running experiment {experiment_name}: {e}") + return None + + # Load results + results_path = os.path.join(experiment_output_dir, "evaluation_results.json") + if os.path.exists(results_path): + with open(results_path) as f: + results = json.load(f) + return results + else: + logger.warning(f"Results file not found: {results_path}") + return None + + +def run_all_experiments(args): + """ + Run all experiments. + + Args: + args: Command-line arguments + + Returns: + all_results: Dictionary of results for all experiments + """ + experiments = [ + "sngp", + "energy", + "sngp_energy", + "ood_class", + "softmax", + ] + all_results = {} + + for experiment in experiments: + logger.info(f"=== Running experiment: {experiment} ===") + results = run_experiment(experiment, args) + if results: + all_results[experiment] = results + + # Save combined results + results_path = os.path.join(args.output_dir, "all_results.json") + with open(results_path, "w") as f: + json.dump(all_results, f, indent=2) + + logger.info(f"All results saved to {results_path}") + + return all_results + + +def analyze_results(results, args): + """ + Analyze results from all experiments. + + Args: + results: Dictionary of results for all experiments + args: Command-line arguments + """ + logger.info("Analyzing results...") + + # Set up visualization directory + viz_dir = os.path.join(args.output_dir, "visualizations") + os.makedirs(viz_dir, exist_ok=True) + + # Create visualizer + visualizer = Visualizer(output_dir=viz_dir) + + # Compare models based on OOD detection metrics + model_metrics = {} + for model_name, model_results in results.items(): + metrics = {} + for metric in [ + "auroc", + "aupr", + "fpr_at_95_tpr", + "detection_error", + "accuracy", + "f1", + ]: + if metric in model_results: + metrics[metric] = model_results[metric] + model_metrics[model_name] = metrics + + # Plot comparison for individual metrics + for metric in [ + "auroc", + "aupr", + "fpr_at_95_tpr", + "detection_error", + "accuracy", + "f1", + ]: + if all(metric in model_results for model_results in model_metrics.values()): + visualizer.plot_model_comparison( + model_metrics=model_metrics, + metric_name=metric, + title=f"Model Comparison - {metric.upper()}", + ) + + # Plot multi-metric comparison + visualizer.plot_multi_metric_comparison( + model_metrics=model_metrics, + metrics=["auroc", "aupr", "fpr_at_95_tpr", "accuracy"], + title="Multi-Metric Model Comparison", + ) + + # Compare inference performance if available + if all( + "inference_performance" in model_results for model_results in results.values() + ): + inference_results = {} + for model_name, model_results in results.items(): + inference_results[model_name] = model_results["inference_performance"] + + # Create inference performance comparison + from evaluation.inference_metrics import InferenceMetrics + + inference_metrics = InferenceMetrics(use_gpu=args.use_gpu) + + # Plot comparison for mean inference time + inference_fig = inference_metrics.compare_models( + model_results=inference_results, + metric="mean", + title="Model Inference Time Comparison", + ) + inference_fig.savefig( + os.path.join(viz_dir, "inference_time_comparison.png"), + bbox_inches="tight", + dpi=300, + ) + + # Plot comparison for throughput + throughput_fig = inference_metrics.compare_models( + model_results=inference_results, + metric="throughput", + title="Model Throughput Comparison", + ) + throughput_fig.savefig( + os.path.join(viz_dir, "throughput_comparison.png"), + bbox_inches="tight", + dpi=300, + ) + + # Generate analysis report + generate_analysis_report(results, args.output_dir) + + +def generate_analysis_report(results, output_dir): + """ + Generate analysis report. + + Args: + results: Dictionary of results for all experiments + output_dir: Output directory + """ + logger.info("Generating analysis report...") + + report_path = os.path.join(output_dir, "ANALYSIS.md") + + with open(report_path, "w") as f: + f.write("# OOD Detection Analysis Report\n\n") + f.write(f"*Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n") + + f.write("## Overview\n\n") + f.write( + "This report analyzes the performance of various OOD detection methods for conversation classification. " + ) + f.write( + "The goal is to identify the most effective approach for detecting out-of-distribution examples " + ) + f.write("while maintaining high accuracy on in-distribution data.\n\n") + + f.write("## Methods Evaluated\n\n") + f.write( + "1. **SNGP (Spectral-normalized Neural Gaussian Process)**: Enhances distance awareness through spectral normalization and Gaussian process output layer.\n" + ) + f.write( + "2. **Energy-based OOD Detection**: Uses energy scores as uncertainty measures.\n" + ) + f.write("3. **SNGP + Energy**: Combines SNGP with energy-based detection.\n") + f.write( + "4. **OOD as a Class**: Treats OOD examples as an additional class during training.\n" + ) + f.write( + "5. **Softmax Threshold**: Uses softmax probabilities or entropy for uncertainty estimation.\n\n" + ) + + f.write("## Performance Metrics\n\n") + f.write("### OOD Detection Performance\n\n") + + # Create OOD detection metrics table + f.write("| Method | AUROC | AUPR | FPR@95%TPR | Detection Error |\n") + f.write("|--------|-------|------|------------|----------------|\n") + + for model_name, model_results in results.items(): + auroc = model_results.get("auroc", "N/A") + aupr = model_results.get("aupr", "N/A") + fpr_at_95_tpr = model_results.get("fpr_at_95_tpr", "N/A") + detection_error = model_results.get("detection_error", "N/A") + + # Format numbers + if isinstance(auroc, (float, int)): + auroc = f"{auroc:.4f}" + if isinstance(aupr, (float, int)): + aupr = f"{aupr:.4f}" + if isinstance(fpr_at_95_tpr, (float, int)): + fpr_at_95_tpr = f"{fpr_at_95_tpr:.4f}" + if isinstance(detection_error, (float, int)): + detection_error = f"{detection_error:.4f}" + + f.write( + f"| {model_name} | {auroc} | {aupr} | {fpr_at_95_tpr} | {detection_error} |\n" + ) + + f.write("\n### Classification Performance\n\n") + + # Create classification metrics table + f.write("| Method | Accuracy | F1 Score | Precision | Recall |\n") + f.write("|--------|----------|----------|-----------|--------|\n") + + for model_name, model_results in results.items(): + accuracy = model_results.get("accuracy", "N/A") + f1 = model_results.get("f1", "N/A") + precision = model_results.get("precision", "N/A") + recall = model_results.get("recall", "N/A") + + # Format numbers + if isinstance(accuracy, (float, int)): + accuracy = f"{accuracy:.4f}" + if isinstance(f1, (float, int)): + f1 = f"{f1:.4f}" + if isinstance(precision, (float, int)): + precision = f"{precision:.4f}" + if isinstance(recall, (float, int)): + recall = f"{recall:.4f}" + + f.write(f"| {model_name} | {accuracy} | {f1} | {precision} | {recall} |\n") + + f.write("\n### Inference Performance\n\n") + + # Check if inference performance is available + if all( + "inference_performance" in model_results + for model_results in results.values() + ): + # Create inference performance table + f.write( + "| Method | Mean Time (ms) - Batch Size 1 | Throughput (examples/s) - Batch Size 32 |\n" + ) + f.write( + "|--------|------------------------------|----------------------------------------|\n" + ) + + for model_name, model_results in results.items(): + inference_perf = model_results.get("inference_performance", {}) + + # Get metrics for batch sizes 1 and 32 + mean_time_bs1 = inference_perf.get("1", {}).get("mean", "N/A") + throughput_bs32 = inference_perf.get("32", {}).get("throughput", "N/A") + + # Convert to milliseconds and format + if isinstance(mean_time_bs1, (float, int)): + mean_time_bs1 = f"{mean_time_bs1 * 1000:.2f}" + if isinstance(throughput_bs32, (float, int)): + throughput_bs32 = f"{throughput_bs32:.2f}" + + f.write(f"| {model_name} | {mean_time_bs1} | {throughput_bs32} |\n") + else: + f.write("Inference performance metrics are not available for all models.\n") + + f.write("\n## Analysis\n\n") + f.write("### OOD Detection Capability\n\n") + f.write( + "Analysis of how well each method detects out-of-distribution examples...\n\n" + ) + + f.write("### Impact on Classification Accuracy\n\n") + f.write( + "Analysis of how OOD detection methods affect classification accuracy...\n\n" + ) + + f.write("### Computational Efficiency\n\n") + f.write("Analysis of computational requirements and inference speed...\n\n") + + f.write("## Conclusion\n\n") + f.write( + "Based on the evaluation results, the recommended approach for OOD detection is...\n\n" + ) + + f.write("## Recommendations\n\n") + f.write( + "Specific recommendations for implementing OOD detection in production...\n\n" + ) + + logger.info(f"Analysis report generated: {report_path}") + + # Also generate decision document + generate_decision_document(results, output_dir) + + +def generate_decision_document(results, output_dir): + """ + Generate decision document. + + Args: + results: Dictionary of results for all experiments + output_dir: Output directory + """ + logger.info("Generating decision document...") + + # Find best model based on AUROC and FPR@95%TPR + best_model = None + best_auroc = -1 + best_fpr = float("inf") + + for model_name, model_results in results.items(): + auroc = model_results.get("auroc", 0) + fpr = model_results.get("fpr_at_95_tpr", 1.0) + + # Prioritize models with higher AUROC + if auroc > best_auroc: + best_model = model_name + best_auroc = auroc + best_fpr = fpr + # If AUROC is similar, choose the one with lower FPR + elif abs(auroc - best_auroc) < 0.01 and fpr < best_fpr: + best_model = model_name + best_auroc = auroc + best_fpr = fpr + + decision_path = os.path.join(output_dir, "DECISION.md") + + with open(decision_path, "w") as f: + f.write("# OOD Detection Decision Document\n\n") + f.write(f"*Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n") + + f.write("## Decision Summary\n\n") + if best_model: + f.write( + f"Based on comprehensive evaluation of multiple OOD detection approaches, we have selected **{best_model}** " + ) + f.write( + "as the optimal method for detecting out-of-distribution examples in conversation classification.\n\n" + ) + else: + f.write( + "Based on comprehensive evaluation of multiple OOD detection approaches, we need to conduct further analysis " + ) + f.write( + "before making a final decision on the optimal method for detecting out-of-distribution examples.\n\n" + ) + + f.write("## Selection Criteria\n\n") + f.write( + "The following criteria were used to evaluate and compare OOD detection methods:\n\n" + ) + f.write( + "1. **OOD Detection Performance**: Measured using AUROC, AUPR, and FPR@95%TPR metrics.\n" + ) + f.write( + "2. **Classification Accuracy**: Impact on in-distribution classification performance.\n" + ) + f.write( + "3. **Computational Efficiency**: Inference time and resource requirements.\n" + ) + f.write( + "4. **Implementation Complexity**: Ease of integration with existing system.\n\n" + ) + + f.write("## Method Evaluation\n\n") + + for model_name, model_results in results.items(): + f.write(f"### {model_name}\n\n") + + # OOD detection metrics + auroc = model_results.get("auroc", "N/A") + aupr = model_results.get("aupr", "N/A") + fpr_at_95_tpr = model_results.get("fpr_at_95_tpr", "N/A") + + f.write("**OOD Detection Performance**:\n") + f.write(f"- AUROC: {auroc if isinstance(auroc, str) else f'{auroc:.4f}'}\n") + f.write(f"- AUPR: {aupr if isinstance(aupr, str) else f'{aupr:.4f}'}\n") + f.write( + f"- FPR@95%TPR: {fpr_at_95_tpr if isinstance(fpr_at_95_tpr, str) else f'{fpr_at_95_tpr:.4f}'}\n\n" + ) + + # Classification metrics + accuracy = model_results.get("accuracy", "N/A") + f1 = model_results.get("f1", "N/A") + + f.write("**Classification Performance**:\n") + f.write( + f"- Accuracy: {accuracy if isinstance(accuracy, str) else f'{accuracy:.4f}'}\n" + ) + f.write(f"- F1 Score: {f1 if isinstance(f1, str) else f'{f1:.4f}'}\n\n") + + # Inference performance + if "inference_performance" in model_results: + inference_perf = model_results["inference_performance"] + mean_time_bs1 = inference_perf.get("1", {}).get("mean", "N/A") + mean_time_bs32 = inference_perf.get("32", {}).get("mean", "N/A") + + f.write("**Computational Performance**:\n") + f.write( + f"- Inference Time (Batch Size 1): {mean_time_bs1 if isinstance(mean_time_bs1, str) else f'{mean_time_bs1 * 1000:.2f} ms'}\n" + ) + f.write( + f"- Inference Time (Batch Size 32): {mean_time_bs32 if isinstance(mean_time_bs32, str) else f'{mean_time_bs32 * 1000:.2f} ms'}\n\n" + ) + + # Pros and cons + f.write("**Pros**:\n") + if model_name == "sngp": + f.write("- Strong uncertainty estimation through distance awareness\n") + f.write( + "- Single model without ensemble averaging, efficient inference\n" + ) + f.write("- Compatible with modern deep learning architectures\n") + elif model_name == "energy": + f.write("- Simple to implement on top of existing models\n") + f.write("- No architectural changes required\n") + f.write("- Theoretically well-founded for OOD detection\n") + elif model_name == "sngp_energy": + f.write("- Combines benefits of both SNGP and energy-based methods\n") + f.write("- Better OOD detection through complementary approaches\n") + f.write("- Robust to different types of OOD examples\n") + elif model_name == "ood_class": + f.write("- Intuitive approach that directly learns OOD patterns\n") + f.write( + "- Simple to implement within existing classification framework\n" + ) + f.write("- No post-processing required during inference\n") + elif model_name == "softmax": + f.write("- Simplest approach with minimal changes to existing models\n") + f.write("- Fast inference with no additional computation\n") + f.write("- Well-understood calibration techniques available\n") + f.write("\n") + + f.write("**Cons**:\n") + if model_name == "sngp": + f.write("- More complex implementation than baseline methods\n") + f.write( + "- Requires careful handling of covariance reset during training\n" + ) + f.write("- May require tuning of spectral normalization parameters\n") + elif model_name == "energy": + f.write( + "- Performance can be sensitive to energy temperature parameter\n" + ) + f.write("- May require additional training with energy-based loss\n") + f.write( + "- Less effective for detecting certain types of OOD examples\n" + ) + elif model_name == "sngp_energy": + f.write("- Most complex implementation among all methods\n") + f.write("- Higher computational overhead during training\n") + f.write("- Requires tuning of multiple hyperparameters\n") + elif model_name == "ood_class": + f.write("- Performance depends on quality of synthetic OOD examples\n") + f.write("- May reduce ID classification performance\n") + f.write("- Struggles with OOD examples that differ from training OOD\n") + elif model_name == "softmax": + f.write("- Often less effective than more sophisticated methods\n") + f.write("- Requires proper calibration for reliable uncertainty\n") + f.write("- Tends to be overconfident far from the decision boundary\n") + f.write("\n\n") + + f.write("## Final Decision\n\n") + if best_model: + f.write( + f"We have selected **{best_model}** as our OOD detection approach based on its strong performance across multiple metrics. " + ) + if best_model == "sngp": + f.write( + "SNGP provides excellent uncertainty estimation through distance awareness while maintaining efficient inference. " + ) + f.write( + "The model achieved the best balance of OOD detection performance and classification accuracy.\n\n" + ) + elif best_model == "energy": + f.write( + "Energy-based OOD detection offers a good balance of implementation simplicity and detection performance. " + ) + f.write( + "It requires minimal changes to the existing model architecture while providing reliable uncertainty estimates.\n\n" + ) + elif best_model == "sngp_energy": + f.write( + "The combined SNGP+Energy approach provides the most robust OOD detection by leveraging both distance awareness and energy scores. " + ) + f.write( + "Despite its higher complexity, the superior detection performance justifies the implementation effort.\n\n" + ) + elif best_model == "ood_class": + f.write( + "Treating OOD as a separate class proved most effective in our evaluation, offering an intuitive approach with strong performance. " + ) + f.write( + "The simplicity of implementation and direct uncertainty scores make it a practical choice for production deployment.\n\n" + ) + elif best_model == "softmax": + f.write( + "The softmax threshold approach provides a good balance of simplicity and effectiveness. " + ) + f.write( + "While more sophisticated methods exist, this approach requires minimal changes to existing infrastructure while delivering acceptable performance.\n\n" + ) + else: + f.write("Further evaluation is needed before making a final decision. ") + f.write( + "While all methods demonstrated strengths in certain areas, no single approach emerged as clearly superior across all metrics.\n\n" + ) + + f.write("## Implementation Plan\n\n") + f.write("### Integration Steps\n\n") + f.write( + "1. Implement the selected OOD detection method within the existing classification model\n" + ) + f.write( + "2. Establish appropriate uncertainty thresholds based on validation data\n" + ) + f.write("3. Set up monitoring for OOD detection performance in production\n") + f.write("4. Create fallback mechanisms for handling detected OOD examples\n\n") + + f.write("### Threshold Selection\n\n") + f.write( + "We recommend setting the uncertainty threshold to achieve a 95% true positive rate (TPR) on validation data. " + ) + f.write( + "This corresponds to correctly identifying 95% of in-distribution examples, with the remaining 5% incorrectly flagged as OOD. " + ) + f.write( + "Based on our experiments, this threshold provides a good balance between catching OOD examples and minimizing false alarms.\n\n" + ) + + f.write("## Conclusion\n\n") + f.write( + "The selected OOD detection approach will enable the system to identify conversations that fall outside the trained distribution, " + ) + f.write( + "allowing for more reliable classification and appropriate handling of uncertain cases. This implementation will improve the " + ) + f.write( + "overall robustness of the conversation classification system and enhance user experience by avoiding incorrect classifications " + ) + f.write("when the model is uncertain.\n") + + logger.info(f"Decision document generated: {decision_path}") + + +def main(): + """Main function.""" + # Parse arguments + args = parse_args() + + # Set up output directory + os.makedirs(args.output_dir, exist_ok=True) + + # Set random seed + np.random.seed(args.seed) + tf.random.set_seed(args.seed) + + # Configure GPU usage + if args.use_gpu: + # Check if GPU is available + gpus = tf.config.list_physical_devices("GPU") + if gpus: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + logger.info(f"Using GPU: {len(gpus)} GPU(s) available") + except RuntimeError as e: + logger.error(f"Error setting GPU memory growth: {e}") + else: + logger.warning("No GPU found. Using CPU instead.") + else: + # Disable GPU + os.environ["CUDA_VISIBLE_DEVICES"] = "-1" + logger.info("Using CPU as requested") + + # Run experiments + if args.experiment == "all": + logger.info("Running all experiments") + results = run_all_experiments(args) + else: + logger.info(f"Running experiment: {args.experiment}") + results = {args.experiment: run_experiment(args.experiment, args)} + + # Analyze results if requested + if args.analyze and results: + analyze_results(results, args) + + logger.info("Done") + + +if __name__ == "__main__": + main() diff --git a/experiments/ood_detection/models/__init__.py b/experiments/ood_detection/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/experiments/ood_detection/models/base_model.py b/experiments/ood_detection/models/base_model.py new file mode 100644 index 00000000..ffd8fd65 --- /dev/null +++ b/experiments/ood_detection/models/base_model.py @@ -0,0 +1,163 @@ +""" +Base model architecture for conversation classification with OOD detection. +""" + +import tensorflow as tf +from transformers import TFAutoModel, AutoConfig +from typing import Dict, Tuple, List, Union, Any +import logging +import time + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class BaseModel(tf.keras.Model): + """Base model for conversation classification.""" + + def __init__( + self, + pretrained_model: str = "xlm-roberta-base", + num_labels: int = 2, + hidden_dims: List[int] = None, + dropout_rate: float = 0.1, + ): + """ + Initialize the base model. + + Args: + pretrained_model: Name or path of the pre-trained model + num_labels: Number of classification labels + hidden_dims: Dimensions of hidden layers after the transformer + dropout_rate: Dropout rate for regularization + **kwargs: Additional arguments + """ + super().__init__() + + self.num_labels = num_labels + self.dropout_rate = dropout_rate + + # Initialize the transformer model + self.config = AutoConfig.from_pretrained(pretrained_model) + self.transformer = TFAutoModel.from_pretrained(pretrained_model) + + # Initialize hidden layers + self.hidden_layers = [] + if hidden_dims: + for dim in hidden_dims: + self.hidden_layers.append(tf.keras.layers.Dense(dim, activation="relu")) + self.hidden_layers.append(tf.keras.layers.Dropout(dropout_rate)) + + # Initialize the classification layer + self.classifier = tf.keras.layers.Dense(num_labels, name="classifier") + + # For inference time tracking + self.inference_times = [] + + def call( + self, + inputs: Dict[str, tf.Tensor], + training: bool = None, + return_features: bool = False, + measure_inference_time: bool = False, + ) -> Union[tf.Tensor, Tuple[tf.Tensor, tf.Tensor]]: + """ + Forward pass of the model. + + Args: + inputs: Input tensors containing 'input_ids' and 'attention_mask' + training: Whether we are training or not + return_features: Whether to return the features before classification + measure_inference_time: Whether to measure inference time + **kwargs: Additional arguments + + Returns: + logits: Classification logits + features: (Optional) Features before the classification layer + """ + start_time = time.time() if measure_inference_time else None + + # Extract inputs + input_ids = inputs["input_ids"] + attention_mask = inputs["attention_mask"] + + # Pass through transformer + transformer_outputs = self.transformer( + input_ids=input_ids, attention_mask=attention_mask, training=training + ) + + # Use the [CLS] token representation + pooled_output = ( + transformer_outputs[1] + if hasattr(transformer_outputs, "__getitem__") + else transformer_outputs.pooler_output + ) + + # Apply hidden layers + features = pooled_output + for layer in self.hidden_layers: + features = layer(features, training=training) + + # Apply classification layer + logits = self.classifier(features) + + if measure_inference_time: + end_time = time.time() + self.inference_times.append(end_time - start_time) + + if return_features: + return logits, features + + return logits + + def get_config(self) -> Dict[str, Any]: + """ + Get model configuration. + + Returns: + config: Model configuration dictionary + """ + config = super().get_config() + config.update( + { + "num_labels": self.num_labels, + "dropout_rate": self.dropout_rate, + } + ) + return config + + def get_average_inference_time(self) -> float: + """ + Get the average inference time. + + Returns: + average_time: Average inference time in seconds + """ + if not self.inference_times: + return 0.0 + return sum(self.inference_times) / len(self.inference_times) + + def predict_with_uncertainty( + self, inputs: Dict[str, tf.Tensor], **kwargs + ) -> Tuple[tf.Tensor, tf.Tensor]: + """ + Base method for prediction with uncertainty. + Subclasses should override this method to provide OOD detection. + + Args: + inputs: Input tensors + **kwargs: Additional arguments + + Returns: + predictions: Class predictions + uncertainty: Uncertainty scores + """ + logits = self(inputs, training=False, measure_inference_time=True) + probabilities = tf.nn.softmax(logits, axis=-1) + predictions = tf.argmax(probabilities, axis=-1) + + # Default uncertainty is just 1 - max probability + max_probs = tf.reduce_max(probabilities, axis=-1) + uncertainty = 1.0 - max_probs + + return predictions, uncertainty diff --git a/experiments/ood_detection/models/energy_model.py b/experiments/ood_detection/models/energy_model.py new file mode 100644 index 00000000..d78e5233 --- /dev/null +++ b/experiments/ood_detection/models/energy_model.py @@ -0,0 +1,174 @@ +""" +Energy-based OOD detection model for conversation classification. +""" + +import tensorflow as tf +from typing import Dict, Tuple, List +import logging +from models.base_model import BaseModel + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class EnergyModel(BaseModel): + """ + Energy-based model for OOD detection. + + This model uses the energy score as an uncertainty measure, which + is computed as -log(sum(exp(logits))). Lower energy indicates + higher likelihood of in-distribution data. + """ + + def __init__( + self, + pretrained_model: str = "xlm-roberta-base", + num_labels: int = 2, + hidden_dims: List[int] = None, + dropout_rate: float = 0.1, + energy_temp: float = 1.0, + ): + """ + Initialize the energy-based model. + + Args: + pretrained_model: Pre-trained model name + num_labels: Number of classes + hidden_dims: Hidden layer dimensions + dropout_rate: Dropout rate + energy_temp: Temperature parameter for energy score + **kwargs: Additional arguments + """ + super().__init__( + pretrained_model=pretrained_model, + num_labels=num_labels, + hidden_dims=hidden_dims, + dropout_rate=dropout_rate, + ) + + self.energy_temp = energy_temp + + def compute_energy(self, logits: tf.Tensor, temp: float = None) -> tf.Tensor: + """ + Compute the energy score. + + Args: + logits: Model logits + temp: Temperature parameter (defaults to self.energy_temp) + + Returns: + energy: Energy scores + """ + if temp is None: + temp = self.energy_temp + + # Scale logits by temperature + logits_scaled = logits / temp + + # Compute energy score: -log(sum(exp(logits))) + energy = -tf.math.log(tf.reduce_sum(tf.exp(logits_scaled), axis=-1)) + + return energy + + def predict_with_uncertainty( + self, inputs: Dict[str, tf.Tensor], **kwargs + ) -> Tuple[tf.Tensor, tf.Tensor]: + """ + Predict with uncertainty estimation using energy scores. + + Args: + inputs: Input tensors + **kwargs: Additional arguments + + Returns: + predictions: Class predictions + uncertainty: Uncertainty scores based on energy + """ + logits = self(inputs, training=False, measure_inference_time=True) + probabilities = tf.nn.softmax(logits, axis=-1) + predictions = tf.argmax(probabilities, axis=-1) + + # Compute energy scores for uncertainty + energy = self.compute_energy(logits) + + # Higher energy means higher uncertainty + uncertainty = energy + + return predictions, uncertainty + + def get_config(self): + """Get model configuration.""" + config = super().get_config() + config.update( + { + "energy_temp": self.energy_temp, + } + ) + return config + + +class EnergyLoss(tf.keras.losses.Loss): + """ + Energy-based loss function for OOD detection. + + This loss adds an additional term to the standard cross-entropy + loss that encourages high energy (low likelihood) for OOD examples + and low energy (high likelihood) for in-distribution examples. + """ + + def __init__( + self, + ood_label: int = -1, + energy_margin: float = 10.0, + energy_weight: float = 0.1, + ): + """ + Initialize the energy loss. + + Args: + ood_label: Label used for OOD examples + energy_margin: Margin for energy difference between ID and OOD + energy_weight: Weight for energy loss term + **kwargs: Additional arguments + """ + super().__init__() + self.ood_label = ood_label + self.energy_margin = energy_margin + self.energy_weight = energy_weight + self.base_loss = tf.keras.losses.SparseCategoricalCrossentropy( + from_logits=True, reduction=tf.keras.losses.Reduction.NONE + ) + + def call(self, y_true: tf.Tensor, y_pred: tf.Tensor) -> tf.Tensor: + """ + Compute the energy-based loss. + + Args: + y_true: Ground truth labels + y_pred: Predicted logits + + Returns: + loss: Combined loss value + """ + # Compute standard cross-entropy loss + ce_loss = self.base_loss(y_true, y_pred) + + # Separate in-distribution and OOD examples + is_ood = tf.cast(tf.equal(y_true, self.ood_label), tf.float32) + is_id = 1.0 - is_ood + + # Compute energy scores + energy = -tf.math.log(tf.reduce_sum(tf.exp(y_pred), axis=-1)) + + # Energy loss: encourage high energy for OOD, low energy for ID + energy_loss = ( + tf.maximum(0.0, self.energy_margin - energy) * is_ood + energy * is_id + ) + + # Mask ce_loss for OOD examples (they don't have a valid class) + ce_loss = ce_loss * is_id + + # Combine losses + combined_loss = ce_loss + self.energy_weight * energy_loss + + return combined_loss diff --git a/experiments/ood_detection/models/ood_class_model.py b/experiments/ood_detection/models/ood_class_model.py new file mode 100644 index 00000000..a5de5ac6 --- /dev/null +++ b/experiments/ood_detection/models/ood_class_model.py @@ -0,0 +1,213 @@ +""" +OOD class model: treats OOD data as an additional class during training. +""" + +import tensorflow as tf +from typing import Dict, Tuple, List +import numpy as np +import logging +from models.base_model import BaseModel + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class OODClassModel(BaseModel): + """ + OOD class model for OOD detection. + + This model treats OOD examples as an additional class during training, + creating a K+1 classifier where K is the number of in-distribution classes + and the +1 is for the OOD class. + """ + + def __init__( + self, + pretrained_model: str = "xlm-roberta-base", + num_labels: int = 2, + hidden_dims: List[int] = None, + dropout_rate: float = 0.1, + synthetic_ood_ratio: float = 0.2, + ): + """ + Initialize the OOD class model. + + Args: + pretrained_model: Pre-trained model name + num_labels: Number of in-distribution classes (OOD class will be added) + hidden_dims: Hidden layer dimensions + dropout_rate: Dropout rate + synthetic_ood_ratio: Ratio of synthetic OOD examples to add during training + **kwargs: Additional arguments + """ + # Add +1 for the OOD class + super().__init__( + pretrained_model=pretrained_model, + num_labels=num_labels + 1, # +1 for OOD class + hidden_dims=hidden_dims, + dropout_rate=dropout_rate, + ) + + self.num_id_classes = num_labels # Store original number of classes + self.synthetic_ood_ratio = synthetic_ood_ratio + + def predict_with_uncertainty( + self, inputs: Dict[str, tf.Tensor], **kwargs + ) -> Tuple[tf.Tensor, tf.Tensor]: + """ + Predict with uncertainty estimation using OOD class probability. + + Args: + inputs: Input tensors + **kwargs: Additional arguments + + Returns: + predictions: Class predictions (excluding OOD class) + uncertainty: Uncertainty scores based on OOD class probability + """ + logits = self(inputs, training=False, measure_inference_time=True) + probabilities = tf.nn.softmax(logits, axis=-1) + + # OOD class is the last class + id_probs = probabilities[:, : self.num_id_classes] + ood_probs = probabilities[:, -1] + + # ID class predictions (excluding OOD class) + predictions = tf.argmax(id_probs, axis=-1) + + # Uncertainty is the probability of the OOD class + uncertainty = ood_probs + + return predictions, uncertainty + + def get_config(self): + """Get model configuration.""" + config = super().get_config() + config.update( + { + "num_id_classes": self.num_id_classes, + "synthetic_ood_ratio": self.synthetic_ood_ratio, + } + ) + return config + + +class OODClassDataGenerator(tf.keras.utils.Sequence): + """ + Data generator for OOD class model. + + This generator adds synthetic OOD examples to the training data. + """ + + def __init__( + self, + dataset: tf.data.Dataset, + num_classes: int, + batch_size: int = 32, + synthetic_ood_ratio: float = 0.2, + shuffle: bool = True, + seed: int = 42, + ): + """ + Initialize the data generator. + + Args: + dataset: The original dataset + num_classes: Number of in-distribution classes + batch_size: Batch size + synthetic_ood_ratio: Ratio of synthetic OOD examples + shuffle: Whether to shuffle the data + seed: Random seed + """ + self.dataset = dataset + self.num_classes = num_classes + self.batch_size = batch_size + self.synthetic_ood_ratio = synthetic_ood_ratio + self.shuffle = shuffle + self.seed = seed + + # Convert dataset to a list for easier handling + self.data_list = [] + for features, labels in dataset: + for i in range(len(labels)): + input_ids = features["input_ids"][i].numpy() + attention_mask = features["attention_mask"][i].numpy() + label = labels[i].numpy() + self.data_list.append((input_ids, attention_mask, label)) + + # Calculate number of synthetic examples + self.num_orig = len(self.data_list) + self.num_synth = int(self.num_orig * synthetic_ood_ratio) + self.num_total = self.num_orig + self.num_synth + + # Generate synthetic OOD examples + self._generate_synthetic_ood() + + # Initial shuffle + if self.shuffle: + np.random.seed(self.seed) + np.random.shuffle(self.combined_data) + + def _generate_synthetic_ood(self): + """Generate synthetic OOD examples by token shuffling.""" + synthetic_data = [] + for _ in range(self.num_synth): + # Randomly select an example + idx = np.random.randint(0, self.num_orig) + input_ids, attention_mask, _ = self.data_list[idx] + + # Shuffle the tokens (excluding special tokens) + special_tokens = [0, 101, 102] # PAD, CLS, SEP tokens + mask = np.ones_like(input_ids, dtype=bool) + for token_id in special_tokens: + mask = mask & (input_ids != token_id) + + # Get non-special token positions + token_positions = np.where(mask)[0] + + if len(token_positions) > 0: + # Shuffle these positions + np.random.shuffle(token_positions) + shuffled_input_ids = input_ids.copy() + + # Apply shuffling + original_tokens = input_ids[mask] + shuffled_input_ids[mask] = original_tokens[np.argsort(token_positions)] + + # Add to synthetic data with OOD label (num_classes) + synthetic_data.append( + (shuffled_input_ids, attention_mask, self.num_classes) + ) + + # Combine original and synthetic data + self.combined_data = self.data_list + synthetic_data + + def __len__(self): + """Get the number of batches per epoch.""" + return int(np.ceil(len(self.combined_data) / self.batch_size)) + + def __getitem__(self, idx): + """Get a batch of data.""" + batch_data = self.combined_data[ + idx * self.batch_size : (idx + 1) * self.batch_size + ] + + # Prepare batch + batch_size = len(batch_data) + input_ids = np.zeros((batch_size, batch_data[0][0].shape[0]), dtype=np.int32) + attention_mask = np.zeros( + (batch_size, batch_data[0][1].shape[0]), dtype=np.int32 + ) + labels = np.zeros(batch_size, dtype=np.int32) + + for i, (ids, mask, label) in enumerate(batch_data): + input_ids[i] = ids + attention_mask[i] = mask + labels[i] = label + + return {"input_ids": input_ids, "attention_mask": attention_mask}, labels + + def on_epoch_end(self): + """Shuffle data at the end of each epoch.""" + if self.shuffle: + np.random.shuffle(self.combined_data) diff --git a/experiments/ood_detection/models/sngp_energy_model.py b/experiments/ood_detection/models/sngp_energy_model.py new file mode 100644 index 00000000..5ba29d2d --- /dev/null +++ b/experiments/ood_detection/models/sngp_energy_model.py @@ -0,0 +1,247 @@ +""" +Implementation of SNGP + Energy model for enhanced OOD detection. +""" + +import tensorflow as tf +from typing import Dict, Tuple, List +import numpy as np +import logging +from models.sngp_model import SNGPModel + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class SNGPEnergyModel(SNGPModel): + """ + Combined SNGP and Energy-based model for OOD detection. + + This model leverages both the distance-awareness of SNGP through + Gaussian process posterior variance and the energy-based scoring + to provide more robust OOD detection. + """ + + def __init__( + self, + pretrained_model: str = "xlm-roberta-base", + num_labels: int = 2, + hidden_dims: List[int] = None, + dropout_rate: float = 0.1, + spec_norm_bound: float = 0.9, + gp_hidden_dim: int = 1024, + gp_scale_random_features: bool = True, + gp_normalize_input: bool = True, + gp_cov_momentum: float = -1.0, + gp_cov_ridge_penalty: float = 1.0, + energy_temp: float = 1.0, + alpha: float = 0.5, # Weight for combining SNGP and Energy scores + ): + """ + Initialize the SNGP+Energy model. + + Args: + pretrained_model: Pre-trained model name + num_labels: Number of classes + hidden_dims: Hidden layer dimensions + dropout_rate: Dropout rate + spec_norm_bound: Spectral normalization bound + gp_hidden_dim: Dimension of GP random features + gp_scale_random_features: Whether to scale GP random features + gp_normalize_input: Whether to normalize GP inputs + gp_cov_momentum: GP covariance momentum + gp_cov_ridge_penalty: GP covariance ridge penalty + energy_temp: Temperature parameter for energy score + alpha: Weight for combining SNGP and Energy scores (0-1) + **kwargs: Additional arguments + """ + super().__init__( + pretrained_model=pretrained_model, + num_labels=num_labels, + hidden_dims=hidden_dims, + dropout_rate=dropout_rate, + spec_norm_bound=spec_norm_bound, + gp_hidden_dim=gp_hidden_dim, + gp_scale_random_features=gp_scale_random_features, + gp_normalize_input=gp_normalize_input, + gp_cov_momentum=gp_cov_momentum, + gp_cov_ridge_penalty=gp_cov_ridge_penalty, + ) + + self.energy_temp = energy_temp + self.alpha = alpha + + def compute_energy(self, logits: tf.Tensor, temp: float = None) -> tf.Tensor: + """ + Compute the energy score. + + Args: + logits: Model logits + temp: Temperature parameter (defaults to self.energy_temp) + + Returns: + energy: Energy scores + """ + if temp is None: + temp = self.energy_temp + + # Scale logits by temperature + logits_scaled = logits / temp + + # Compute energy score: -log(sum(exp(logits))) + energy = -tf.math.log(tf.reduce_sum(tf.exp(logits_scaled), axis=-1)) + + return energy + + def predict_with_uncertainty( + self, inputs: Dict[str, tf.Tensor], normalize_uncertainty: bool = True, **kwargs + ) -> Tuple[tf.Tensor, tf.Tensor]: + """ + Predict with combined uncertainty estimation. + + Args: + inputs: Input tensors + normalize_uncertainty: Whether to normalize uncertainty scores + **kwargs: Additional arguments + + Returns: + predictions: Class predictions + uncertainty: Combined uncertainty scores from SNGP and Energy + """ + # Get logits and covariance matrix from SNGP + logits, covmat = self( + inputs, training=False, return_covmat=True, measure_inference_time=True + ) + + # Extract variance from the diagonal of covariance matrix + sngp_variance = tf.linalg.diag_part(covmat) + + # Mean-field approximation for adjusted logits + mean_field_factor = np.pi / 8.0 + logits_adjusted = logits / tf.sqrt( + 1.0 + mean_field_factor * sngp_variance[:, tf.newaxis] + ) + + # Get predictions from adjusted logits + probabilities = tf.nn.softmax(logits_adjusted, axis=-1) + predictions = tf.argmax(probabilities, axis=-1) + + # Compute energy scores + energy = self.compute_energy(logits) + + # Normalize SNGP variance and energy if requested + if normalize_uncertainty: + # Min-max normalization for variance + sngp_variance = self._normalize_scores(sngp_variance) + + # Min-max normalization for energy + energy = self._normalize_scores(energy) + + # Combine SNGP variance and energy scores + combined_uncertainty = self.alpha * sngp_variance + (1.0 - self.alpha) * energy + + return predictions, combined_uncertainty + + def _normalize_scores(self, scores: tf.Tensor) -> tf.Tensor: + """ + Normalize scores to [0, 1] range using min-max normalization. + + Args: + scores: Input scores + + Returns: + normalized_scores: Normalized scores between 0 and 1 + """ + min_val = tf.reduce_min(scores) + max_val = tf.reduce_max(scores) + + # Prevent division by zero + range_val = tf.maximum(max_val - min_val, 1e-10) + + return (scores - min_val) / range_val + + def get_config(self): + """Get model configuration.""" + config = super().get_config() + config.update( + { + "energy_temp": self.energy_temp, + "alpha": self.alpha, + } + ) + return config + + +class SNGPEnergyLoss(tf.keras.losses.Loss): + """ + Loss function for the SNGP+Energy model. + + This loss combines cross-entropy with energy-based regularization + for improved OOD detection. + """ + + def __init__( + self, + ood_label: int = -1, + energy_margin: float = 10.0, + energy_weight: float = 0.1, + ): + """ + Initialize the SNGP+Energy loss. + + Args: + ood_label: Label used for OOD examples + energy_margin: Margin for energy difference between ID and OOD + energy_weight: Weight for energy loss term + **kwargs: Additional arguments + """ + super().__init__() + self.ood_label = ood_label + self.energy_margin = energy_margin + self.energy_weight = energy_weight + self.base_loss = tf.keras.losses.SparseCategoricalCrossentropy( + from_logits=True, reduction=tf.keras.losses.Reduction.NONE + ) + + def call(self, y_true: tf.Tensor, y_pred: Tuple[tf.Tensor, tf.Tensor]) -> tf.Tensor: + """ + Compute the SNGP+Energy loss. + + Args: + y_true: Ground truth labels + y_pred: Tuple of (logits, covmat) + + Returns: + loss: Combined loss value + """ + # Unpack predictions (may be just logits during training) + if isinstance(y_pred, tuple) and len(y_pred) == 2: + logits, _ = y_pred + else: + logits = y_pred + + # Compute standard cross-entropy loss + ce_loss = self.base_loss(y_true, logits) + + # Separate in-distribution and OOD examples + is_ood = tf.cast(tf.equal(y_true, self.ood_label), tf.float32) + is_id = 1.0 - is_ood + + # Compute energy scores + energy = -tf.math.log(tf.reduce_sum(tf.exp(logits), axis=-1)) + + # Energy loss: encourage high energy for OOD, low energy for ID + energy_loss = ( + tf.maximum(0.0, self.energy_margin - energy) * is_ood + energy * is_id + ) + + # Mask ce_loss for OOD examples (they don't have a valid class) + ce_loss = ce_loss * is_id + + # If there are no OOD examples in this batch, just return CE loss + num_ood = tf.reduce_sum(is_ood) + has_ood = tf.cast(tf.greater(num_ood, 0), tf.float32) + + # Combine losses only if OOD examples are present + combined_loss = ce_loss + has_ood * self.energy_weight * energy_loss + + return combined_loss diff --git a/experiments/ood_detection/models/sngp_model.py b/experiments/ood_detection/models/sngp_model.py new file mode 100644 index 00000000..fdcfabe9 --- /dev/null +++ b/experiments/ood_detection/models/sngp_model.py @@ -0,0 +1,554 @@ +""" +SNGP (Spectral-normalized Neural Gaussian Process) model for OOD detection. +""" + +import time +import tensorflow as tf +from typing import Dict, Tuple, List +import numpy as np +import logging +from models.base_model import BaseModel + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class SpectralNormalization(tf.keras.layers.Wrapper): + """ + Spectral Normalization layer wrapper. + + This wrapper normalizes the kernel matrix by constraining + its spectral norm (the largest singular value) to be less than + or equal to the specified norm_multiplier. + """ + + def __init__( + self, + layer: tf.keras.layers.Layer, + norm_multiplier: float = 0.9, + n_power_iterations: int = 1, + ): + """ + Initialize the wrapper. + + Args: + layer: The layer to wrap + norm_multiplier: Target spectral norm + n_power_iterations: Number of power iterations for spectral norm estimation + **kwargs: Additional arguments + """ + super().__init__(layer) + self.norm_multiplier = norm_multiplier + self.n_power_iterations = n_power_iterations + + if not isinstance(layer, tf.keras.layers.Dense): + raise ValueError( + "SpectralNormalization can only wrap Dense layers. " + f"Received: {layer.__class__.__name__}" + ) + + def build(self, input_shape): + """Build the wrapper.""" + if not self.layer.built: + self.layer.build(input_shape) + + self.w = self.layer.kernel + self.w_shape = self.w.shape.as_list() + + # Initialize u and v vectors for power iteration + # u should match the input dimension, v should match the output dimension + self.u = self.add_weight( + name="u", + shape=(1, self.w_shape[0]), # Input dimension + initializer=tf.initializers.TruncatedNormal(stddev=0.1), + trainable=False, + ) + + self.v = self.add_weight( + name="v", + shape=(1, self.w_shape[1]), # Output dimension + initializer=tf.initializers.TruncatedNormal(stddev=0.1), + trainable=False, + ) + + super().build(input_shape) + + def call(self, inputs, training=None): + """Forward pass with spectral normalization.""" + # Only perform power iterations during training + if training: + self._update_weights() + + return self.layer(inputs) + + def _update_weights(self): + """Update weights using power iteration method.""" + w_reshaped = tf.reshape(self.w, [self.w_shape[0], self.w_shape[1]]) + + # Power iteration to find largest singular value + for _ in range(self.n_power_iterations): + # v = W^T u / ||W^T u|| + v_new = tf.matmul(self.u, w_reshaped) + self.v.assign(tf.nn.l2_normalize(v_new, axis=1)) + + # u = W v / ||W v|| + u_new = tf.matmul(self.v, w_reshaped, transpose_b=True) + self.u.assign(tf.nn.l2_normalize(u_new, axis=1)) + + # Estimate spectral norm: sigma = u^T W v + sigma = tf.matmul(tf.matmul(self.u, w_reshaped), self.v, transpose_b=True) + sigma = tf.reshape(sigma, []) # Make it a scalar + + # Normalize weights + self.layer.kernel.assign(self.w / tf.maximum(sigma / self.norm_multiplier, 1.0)) + + def get_config(self): + """Get configuration.""" + config = { + "norm_multiplier": self.norm_multiplier, + "n_power_iterations": self.n_power_iterations, + } + base_config = super().get_config() + return {**base_config, **config} + + +class RandomFeatureGaussianProcess(tf.keras.layers.Layer): + """ + Random Feature Gaussian Process layer. + + This layer implements a random feature-based approximation to a + Gaussian process model that is end-to-end trainable with a deep neural network. + """ + + def __init__( + self, + units: int, + num_inducing: int = 1024, + normalize_input: bool = True, + scale_random_features: bool = True, + gp_cov_momentum: float = -1.0, + gp_cov_ridge_penalty: float = 1.0, + **kwargs, + ): + """ + Initialize the layer. + + Args: + units: Number of output units (classes) + num_inducing: Number of random features for GP approximation + normalize_input: Whether to normalize inputs + scale_random_features: Whether to scale random features + gp_cov_momentum: Momentum for covariance update (-1.0 for no momentum) + gp_cov_ridge_penalty: Ridge penalty for covariance matrix + **kwargs: Additional arguments + """ + super().__init__(**kwargs) + self.units = units + self.num_inducing = num_inducing + self.normalize_input = normalize_input + self.scale_random_features = scale_random_features + self.gp_cov_momentum = gp_cov_momentum + self.gp_cov_ridge_penalty = gp_cov_ridge_penalty + + # Set to True when cov_matrix needs resetting + self._reset_covariance = False + + def build(self, input_shape): + """Build the layer.""" + input_dim = input_shape[-1] + + # Layer normalization for input + if self.normalize_input: + self.layer_norm = tf.keras.layers.LayerNormalization( + epsilon=1e-12, name="LayerNorm" + ) + + # Random Fourier feature parameters + stddev = 1.0 / tf.math.sqrt(tf.cast(input_dim, tf.float32)) + self.random_features = self.add_weight( + name="random_features", + shape=[input_dim, self.num_inducing], + initializer=tf.keras.initializers.RandomNormal(stddev=stddev), + trainable=False, + ) + + # Gaussian process parameters + self.kernel = self.add_weight( + name="kernel", + shape=[self.num_inducing * 2, self.units], # *2 for cos and sin + initializer="glorot_uniform", + trainable=True, + ) + + # Initialize precision matrix + self.precision_matrix = self.add_weight( + name="precision_matrix", + shape=[self.num_inducing * 2, self.num_inducing * 2], # *2 for cos and sin + initializer=tf.keras.initializers.Identity(gain=self.gp_cov_ridge_penalty), + trainable=False, + ) + + # Covariance matrix + self.covariance_matrix = self.add_weight( + name="covariance_matrix", + shape=[self.num_inducing * 2, self.num_inducing * 2], # *2 for cos and sin + initializer="zeros", + trainable=False, + ) + + # Counter for covariance updates + self.counter = self.add_weight( + name="counter", + shape=[], + initializer="zeros", + trainable=False, + dtype=tf.float32, + ) + + super().build(input_shape) + + def reset_covariance_matrix(self): + """Reset the covariance matrix at the beginning of each epoch.""" + self._reset_covariance = True + + def call(self, inputs, training=None): + """Forward pass.""" + # Apply layer normalization if needed + if self.normalize_input: + inputs = self.layer_norm(inputs) + + # Reset covariance if needed and in training + if training and self._reset_covariance: + logger.info("Resetting GP covariance matrix") + self.covariance_matrix.assign(tf.zeros_like(self.covariance_matrix)) + self.counter.assign(0.0) + self._reset_covariance = False + + # Project inputs to random features + gp_features = tf.matmul(inputs, self.random_features) + + # Apply cos and sin functions + gp_features = tf.concat( + [tf.math.cos(gp_features), tf.math.sin(gp_features)], axis=-1 + ) + + # Scale features if needed + if self.scale_random_features: + gp_features = gp_features * tf.math.sqrt( + 2.0 / tf.cast(self.random_features.shape[1], tf.float32) + ) + + # Update covariance matrix during training + if training: + self._update_covariance(gp_features) + + # Compute logits + logits = tf.matmul(gp_features, self.kernel) + + # Return logits and also compute posterior covariance if not training + if not training: + # Compute posterior covariance + feature_cov = self._compute_predictive_covariance(gp_features) + + return logits, feature_cov + + return logits, tf.zeros([1, 1]) # Dummy covariance during training + + def _update_covariance(self, features): + """Update GP covariance matrix during training.""" + batch_size = tf.cast(tf.shape(features)[0], tf.float32) + self.counter.assign_add(batch_size) + + # Compute batch covariance + features_t = tf.transpose(features) + batch_cov = tf.matmul(features_t, features) + + if self.gp_cov_momentum > 0: + # Momentum-based update + self.covariance_matrix.assign( + self.gp_cov_momentum * self.covariance_matrix + + (1 - self.gp_cov_momentum) * batch_cov + ) + else: + # Running average update + self.covariance_matrix.assign_add(batch_cov) + + def _compute_predictive_covariance(self, features): + """Compute predictive covariance for uncertainty estimation.""" + # Normalize the covariance matrix by total count + cov_matrix = self.covariance_matrix / tf.maximum(self.counter, 1.0) + + # Add ridge penalty - FIXED: use correct dimension + feature_dim = self.num_inducing * 2 + cov_matrix = cov_matrix + self.gp_cov_ridge_penalty * tf.eye( + feature_dim, batch_shape=[], dtype=cov_matrix.dtype + ) + + # Compute posterior covariance + # K_* = k(x, X) = features + # K = k(X, X) + λI = cov_matrix + # Posterior covariance = K_* K^-1 K_*^T + + # We use Cholesky decomposition for numerical stability + try: + chol = tf.linalg.cholesky(cov_matrix) + except tf.errors.InvalidArgumentError: + # If Cholesky fails, add more regularization + cov_matrix = cov_matrix + 1e-6 * tf.eye( + feature_dim, batch_shape=[], dtype=cov_matrix.dtype + ) + chol = tf.linalg.cholesky(cov_matrix) + + # Solve the system: K^-1 K_*^T + kern_prod = tf.linalg.triangular_solve(chol, tf.transpose(features), lower=True) + + # Compute the predictive covariance + predictive_cov = tf.matmul(kern_prod, kern_prod, transpose_a=True) + + return predictive_cov + + def get_config(self): + """Get configuration.""" + config = { + "units": self.units, + "num_inducing": self.num_inducing, + "normalize_input": self.normalize_input, + "scale_random_features": self.scale_random_features, + "gp_cov_momentum": self.gp_cov_momentum, + "gp_cov_ridge_penalty": self.gp_cov_ridge_penalty, + } + base_config = super().get_config() + return {**base_config, **config} + + +class SNGPModel(BaseModel): + """ + SNGP model for OOD detection in conversation classification. + + This model applies spectral normalization to hidden layers + and uses a Gaussian process layer for output to enable + uncertainty estimation. + """ + + def __init__( + self, + pretrained_model: str = "xlm-roberta-base", + num_labels: int = 2, + hidden_dims: List[int] = None, + dropout_rate: float = 0.1, + spec_norm_bound: float = 0.9, + gp_hidden_dim: int = 1024, + gp_scale_random_features: bool = True, + gp_normalize_input: bool = True, + gp_cov_momentum: float = -1.0, + gp_cov_ridge_penalty: float = 1.0, + **kwargs, + ): + """ + Initialize the SNGP model. + + Args: + pretrained_model: Pre-trained model name + num_labels: Number of classes + hidden_dims: Hidden layer dimensions + dropout_rate: Dropout rate + spec_norm_bound: Spectral normalization bound + gp_hidden_dim: Dimension of GP random features + gp_scale_random_features: Whether to scale GP random features + gp_normalize_input: Whether to normalize GP inputs + gp_cov_momentum: GP covariance momentum + gp_cov_ridge_penalty: GP covariance ridge penalty + **kwargs: Additional arguments + """ + # Don't pass num_labels to parent class as we'll handle the classifier differently + super().__init__( + pretrained_model=pretrained_model, + num_labels=0, # No classifier layer in parent + hidden_dims=None, # We'll build our own hidden layers + dropout_rate=dropout_rate, + **kwargs, + ) + + self.num_labels = num_labels + self.spec_norm_bound = spec_norm_bound + + # Build hidden layers with spectral normalization + if hidden_dims is None: + hidden_dims = [768] + + self.hidden_layers = [] + for dim in hidden_dims: + dense_layer = tf.keras.layers.Dense(dim, activation="relu") + spectral_layer = SpectralNormalization( + dense_layer, norm_multiplier=spec_norm_bound + ) + self.hidden_layers.append(spectral_layer) + self.hidden_layers.append(tf.keras.layers.Dropout(dropout_rate)) + + # Replace classifier with GP layer + self.classifier = RandomFeatureGaussianProcess( + units=num_labels, + num_inducing=gp_hidden_dim, + normalize_input=gp_normalize_input, + scale_random_features=gp_scale_random_features, + gp_cov_momentum=gp_cov_momentum, + gp_cov_ridge_penalty=gp_cov_ridge_penalty, + name="gp_classifier", + ) + + def build(self, input_shape): + """Build the model.""" + # Build parent first + super().build(input_shape) + + # Build hidden layers with proper input shapes + current_shape = (None, 768) # Shape after transformer + for layer in self.hidden_layers: + if hasattr(layer, "build"): + layer.build(current_shape) + if isinstance(layer, SpectralNormalization): + current_shape = (None, layer.layer.units) + + # Build classifier + self.classifier.build(current_shape) + + def call( + self, + inputs: Dict[str, tf.Tensor], + training: bool = None, + return_features: bool = False, + return_covmat: bool = False, + measure_inference_time: bool = False, + **kwargs, + ): + """ + Forward pass of the SNGP model. + + Args: + inputs: Input tensors + training: Whether in training mode + return_features: Whether to return features + return_covmat: Whether to return covariance matrix + measure_inference_time: Whether to measure inference time + **kwargs: Additional arguments + + Returns: + logits: Output logits + features: (Optional) Features before classification + covmat: (Optional) Covariance matrix + """ + start_time = time.time() if measure_inference_time else None + + # Extract inputs + input_ids = inputs["input_ids"] + attention_mask = inputs["attention_mask"] + + # Pass through transformer + transformer_outputs = self.transformer( + input_ids=input_ids, attention_mask=attention_mask, training=training + ) + + # Use the [CLS] token representation + pooled_output = ( + transformer_outputs[1] + if hasattr(transformer_outputs, "__getitem__") + and len(transformer_outputs) > 1 + else transformer_outputs.last_hidden_state[:, 0, :] # Use CLS token + ) + + # Apply hidden layers with spectral normalization + features = pooled_output + for layer in self.hidden_layers: + features = layer(features, training=training) + + # Apply GP layer + logits, covmat = self.classifier(features, training=training) + + if measure_inference_time: + end_time = time.time() + if not hasattr(self, "inference_times"): + self.inference_times = [] + self.inference_times.append(end_time - start_time) + + if return_features and return_covmat: + return logits, features, covmat + elif return_features: + return logits, features + elif return_covmat: + return logits, covmat + + return logits + + def reset_covmat(self): + """Reset the covariance matrix of the GP layer.""" + self.classifier.reset_covariance_matrix() + + def predict_with_uncertainty( + self, inputs: Dict[str, tf.Tensor], **kwargs + ) -> Tuple[tf.Tensor, tf.Tensor]: + """ + Predict with uncertainty estimation using SNGP. + + Args: + inputs: Input tensors + **kwargs: Additional arguments + + Returns: + predictions: Class predictions + uncertainty: Uncertainty scores based on predictive variance + """ + # Get logits and covariance matrix + logits, covmat = self( + inputs, training=False, return_covmat=True, measure_inference_time=True + ) + + # Extract variance from the diagonal of the covariance matrix + # covmat shape: [batch_size, batch_size] - diagonal gives per-sample variance + variance = tf.linalg.diag_part(covmat) + + # Ensure variance is always a vector (batch_size,) + if len(variance.shape) == 0: # scalar case + batch_size = tf.shape(logits)[0] + variance = tf.fill([batch_size], variance) + elif len(variance.shape) > 1: # multi-dimensional case + variance = tf.reduce_mean(variance, axis=-1) + + # Mean-field approximation to get uncertainty-adjusted logits + mean_field_factor = np.pi / 8.0 + variance_expanded = tf.expand_dims(variance, -1) # [batch_size, 1] + logits_adjusted = logits / tf.sqrt(1.0 + mean_field_factor * variance_expanded) + + # Get predictions from adjusted logits + probabilities = tf.nn.softmax(logits_adjusted, axis=-1) + predictions = tf.argmax(probabilities, axis=-1) + + # Return variance as uncertainty measure + uncertainty = variance + + return predictions, uncertainty + + def get_config(self): + """Get model configuration.""" + config = super().get_config() + config.update( + { + "num_labels": self.num_labels, + "spec_norm_bound": self.spec_norm_bound, + } + ) + return config + + +# Callback for resetting covariance matrix at the beginning of each epoch +class ResetCovarianceCallback(tf.keras.callbacks.Callback): + """Callback to reset GP covariance matrix at the beginning of each epoch.""" + + def on_epoch_begin(self, epoch, logs=None): + """Reset covariance matrix when a new epoch begins.""" + if epoch > 0: # Skip the first epoch + if hasattr(self.model, "reset_covmat"): + self.model.reset_covmat() + elif hasattr(self.model, "classifier") and hasattr( + self.model.classifier, "reset_covariance_matrix" + ): + self.model.classifier.reset_covariance_matrix() diff --git a/experiments/ood_detection/models/softmax_model.py b/experiments/ood_detection/models/softmax_model.py new file mode 100644 index 00000000..ab2e665e --- /dev/null +++ b/experiments/ood_detection/models/softmax_model.py @@ -0,0 +1,291 @@ +""" +Softmax threshold model for OOD detection. +""" + +import tensorflow as tf +from typing import Dict, Tuple, List +import logging +from models.base_model import BaseModel + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class SoftmaxModel(BaseModel): + """ + Softmax threshold model for OOD detection. + + This model uses the maximum softmax probability (MSP) or + entropy of the softmax distribution as an uncertainty measure. + It may also use temperature scaling for improved calibration. + """ + + def __init__( + self, + pretrained_model: str = "xlm-roberta-base", + num_labels: int = 2, + hidden_dims: List[int] = None, + dropout_rate: float = 0.1, + temperature: float = 1.0, + use_entropy: bool = False, # Whether to use entropy instead of MSP + **kwargs, + ): + """ + Initialize the softmax threshold model. + + Args: + pretrained_model: Pre-trained model name + num_labels: Number of classes + hidden_dims: Hidden layer dimensions + dropout_rate: Dropout rate + temperature: Temperature scaling parameter + use_entropy: Whether to use entropy instead of MSP + **kwargs: Additional arguments + """ + super().__init__( + pretrained_model=pretrained_model, + num_labels=num_labels, + hidden_dims=hidden_dims, + dropout_rate=dropout_rate, + **kwargs, + ) + + self.temperature = temperature + self.use_entropy = use_entropy + + def compute_entropy(self, probabilities: tf.Tensor) -> tf.Tensor: + """ + Compute entropy of probability distribution. + + Args: + probabilities: Softmax probabilities + + Returns: + entropy: Entropy values + """ + # Add small epsilon to avoid log(0) + probabilities = tf.clip_by_value(probabilities, 1e-10, 1.0) + + # Compute entropy: -sum(p_i * log(p_i)) + entropy = -tf.reduce_sum(probabilities * tf.math.log(probabilities), axis=-1) + + return entropy + + def predict_with_uncertainty( + self, inputs: Dict[str, tf.Tensor], **kwargs + ) -> Tuple[tf.Tensor, tf.Tensor]: + """ + Predict with uncertainty estimation using softmax threshold. + + Args: + inputs: Input tensors + **kwargs: Additional arguments + + Returns: + predictions: Class predictions + uncertainty: Uncertainty scores based on softmax + """ + logits = self(inputs, training=False, measure_inference_time=True) + + # Apply temperature scaling + logits_scaled = logits / self.temperature + probabilities = tf.nn.softmax(logits_scaled, axis=-1) + predictions = tf.argmax(probabilities, axis=-1) + + if self.use_entropy: + # Compute entropy as uncertainty + uncertainty = self.compute_entropy(probabilities) + else: + # 1 - max softmax probability as uncertainty + max_probs = tf.reduce_max(probabilities, axis=-1) + uncertainty = 1.0 - max_probs + + return predictions, uncertainty + + def get_config(self): + """Get model configuration.""" + config = super().get_config() + config.update( + { + "temperature": self.temperature, + "use_entropy": self.use_entropy, + } + ) + return config + + +class TemperatureScaling(tf.keras.layers.Layer): + """ + Temperature scaling layer for model calibration. + + This layer learns a single temperature parameter to scale + the logits, which can improve the calibration of the model. + """ + + def __init__(self, temperature: float = 1.0, **kwargs): + """ + Initialize temperature scaling layer. + + Args: + temperature: Initial temperature value + **kwargs: Additional arguments + """ + super().__init__(**kwargs) + self.temperature = temperature + + def build(self, input_shape): + """Build the layer.""" + # Temperature parameter (learned during calibration) + self.temp = self.add_weight( + name="temperature", + shape=[], + initializer=tf.keras.initializers.Constant(self.temperature), + trainable=True, + ) + + super().build(input_shape) + + def call(self, inputs, training=None): + """Forward pass.""" + # Scale logits by temperature + return inputs / self.temp + + def get_config(self): + """Get configuration.""" + config = {"temperature": self.temperature} + base_config = super().get_config() + return {**base_config, **config} + + +class CalibrationCallback(tf.keras.callbacks.Callback): + """ + Callback for temperature scaling calibration. + + This callback performs post-training calibration using + temperature scaling on a validation dataset. + """ + + def __init__( + self, + calibration_data: tf.data.Dataset, + patience: int = 5, + min_delta: float = 1e-4, + verbose: int = 1, + ): + """ + Initialize calibration callback. + + Args: + calibration_data: Dataset for calibration + patience: Patience for early stopping + min_delta: Minimum delta for improvement + verbose: Verbosity level + """ + super().__init__() + self.calibration_data = calibration_data + self.patience = patience + self.min_delta = min_delta + self.verbose = verbose + + self.best_nll = float("inf") + self.wait = 0 + + def on_train_end(self, logs=None): + """Perform calibration when training ends.""" + if not hasattr(self.model, "temperature_layer"): + logger.warning( + "Model does not have a temperature_layer attribute. Skipping calibration." + ) + return + + if self.verbose > 0: + logger.info("Starting temperature calibration...") + + # Create optimizer and loss for calibration + optimizer = tf.keras.optimizers.legacy.Adam(learning_rate=0.01) + loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) + + # Initial evaluation + initial_nll = self._evaluate_nll(loss_fn) + if self.verbose > 0: + logger.info( + f"Initial NLL: {initial_nll:.4f}, Initial temperature: {self.model.temperature_layer.temp.numpy():.4f}" + ) + + # Calibration loop + for epoch in range(50): # Max 50 epochs for calibration + self._calibration_step(optimizer, loss_fn) + + # Evaluate current NLL + current_nll = self._evaluate_nll(loss_fn) + + if self.verbose > 0: + logger.info( + f"Epoch {epoch + 1}, NLL: {current_nll:.4f}, Temperature: {self.model.temperature_layer.temp.numpy():.4f}" + ) + + # Check for improvement + if current_nll < self.best_nll - self.min_delta: + self.best_nll = current_nll + self.wait = 0 + else: + self.wait += 1 + if self.wait >= self.patience: + if self.verbose > 0: + logger.info(f"Early stopping at epoch {epoch + 1}") + break + + if self.verbose > 0: + final_temp = self.model.temperature_layer.temp.numpy() + logger.info(f"Calibration complete. Final temperature: {final_temp:.4f}") + improvement = (initial_nll - self.best_nll) / initial_nll * 100 + logger.info( + f"NLL improved from {initial_nll:.4f} to {self.best_nll:.4f} ({improvement:.2f}%)" + ) + + def _calibration_step(self, optimizer, loss_fn): + """Perform one step of calibration.""" + for x, y in self.calibration_data: + with tf.GradientTape() as tape: + # Forward pass through the model + logits = self.model(x, training=False) + + # Apply temperature scaling (only temperature is trainable) + scaled_logits = self.model.temperature_layer(logits, training=True) + + # Compute loss + loss = loss_fn(y, scaled_logits) + + # Compute gradients + grads = tape.gradient(loss, [self.model.temperature_layer.temp]) + + # Apply gradients + optimizer.apply_gradients(zip(grads, [self.model.temperature_layer.temp])) + + # Ensure temperature is positive + self.model.temperature_layer.temp.assign( + tf.maximum(self.model.temperature_layer.temp, 0.01) + ) + + def _evaluate_nll(self, loss_fn): + """Evaluate negative log-likelihood on calibration data.""" + total_loss = 0.0 + total_samples = 0 + + for x, y in self.calibration_data: + # Forward pass + logits = self.model(x, training=False) + scaled_logits = self.model.temperature_layer(logits, training=False) + + # Compute NLL + loss = loss_fn(y, scaled_logits) + + # Accumulate weighted loss + batch_size = tf.shape(y)[0] + total_loss += loss * tf.cast(batch_size, tf.float32) + total_samples += batch_size + + # Compute average NLL + avg_nll = total_loss / tf.cast(total_samples, tf.float32) + + return avg_nll.numpy() diff --git a/experiments/ood_detection/outputs/ANALYSIS.md b/experiments/ood_detection/outputs/ANALYSIS.md new file mode 100644 index 00000000..06432505 --- /dev/null +++ b/experiments/ood_detection/outputs/ANALYSIS.md @@ -0,0 +1,67 @@ +# OOD Detection Analysis Report + +*Date: 2025-05-03 03:36:56* + +## Overview + +This report analyzes the performance of various OOD detection methods for conversation classification. The goal is to identify the most effective approach for detecting out-of-distribution examples while maintaining high accuracy on in-distribution data. + +## Methods Evaluated + +1. **SNGP (Spectral-normalized Neural Gaussian Process)**: Enhances distance awareness through spectral normalization and Gaussian process output layer. +2. **Energy-based OOD Detection**: Uses energy scores as uncertainty measures. +3. **SNGP + Energy**: Combines SNGP with energy-based detection. +4. **OOD as a Class**: Treats OOD examples as an additional class during training. +5. **Softmax Threshold**: Uses softmax probabilities or entropy for uncertainty estimation. + +## Performance Metrics + +### OOD Detection Performance + +| Method | AUROC | AUPR | FPR@95%TPR | Detection Error | +|--------|-------|------|------------|----------------| +| sngp | 0.0000 | 0.1000 | 1.0000 | 0.5000 | +| energy | 0.4142 | 0.1594 | 0.9298 | 0.4746 | +| sngp_energy | 0.6109 | 0.2633 | 0.7982 | 0.3669 | +| softmax | 0.3452 | 0.1410 | 0.8947 | 0.4518 | + +### Classification Performance + +| Method | Accuracy | F1 Score | Precision | Recall | +|--------|----------|----------|-----------|--------| +| sngp | 0.6053 | 0.5062 | 0.4352 | 0.6053 | +| energy | 0.4298 | 0.3387 | 0.3068 | 0.4298 | +| sngp_energy | 0.5175 | 0.4299 | 0.3760 | 0.5175 | +| softmax | 0.4737 | 0.3933 | 0.3426 | 0.4737 | + +### Inference Performance + +| Method | Mean Time (ms) - Batch Size 1 | Throughput (examples/s) - Batch Size 32 | +|--------|------------------------------|----------------------------------------| +| sngp | 565.33 | 2.79 | +| energy | 441.98 | 2.83 | +| sngp_energy | 583.02 | 3.03 | +| softmax | 447.11 | 2.91 | + +## Analysis + +### OOD Detection Capability + +Analysis of how well each method detects out-of-distribution examples... + +### Impact on Classification Accuracy + +Analysis of how OOD detection methods affect classification accuracy... + +### Computational Efficiency + +Analysis of computational requirements and inference speed... + +## Conclusion + +Based on the evaluation results, the recommended approach for OOD detection is... + +## Recommendations + +Specific recommendations for implementing OOD detection in production... + diff --git a/experiments/ood_detection/outputs/DECISION.md b/experiments/ood_detection/outputs/DECISION.md new file mode 100644 index 00000000..b5216d63 --- /dev/null +++ b/experiments/ood_detection/outputs/DECISION.md @@ -0,0 +1,143 @@ +# OOD Detection Decision Document + +*Date: 2025-05-03 03:36:56* + +## Decision Summary + +Based on comprehensive evaluation of multiple OOD detection approaches, we have selected **sngp_energy** as the optimal method for detecting out-of-distribution examples in conversation classification. + +## Selection Criteria + +The following criteria were used to evaluate and compare OOD detection methods: + +1. **OOD Detection Performance**: Measured using AUROC, AUPR, and FPR@95%TPR metrics. +2. **Classification Accuracy**: Impact on in-distribution classification performance. +3. **Computational Efficiency**: Inference time and resource requirements. +4. **Implementation Complexity**: Ease of integration with existing system. + +## Method Evaluation + +### sngp + +**OOD Detection Performance**: +- AUROC: 0.0000 +- AUPR: 0.1000 +- FPR@95%TPR: 1.0000 + +**Classification Performance**: +- Accuracy: 0.6053 +- F1 Score: 0.5062 + +**Computational Performance**: +- Inference Time (Batch Size 1): 565.33 ms +- Inference Time (Batch Size 32): 11451.04 ms + +**Pros**: +- Strong uncertainty estimation through distance awareness +- Single model without ensemble averaging, efficient inference +- Compatible with modern deep learning architectures + +**Cons**: +- More complex implementation than baseline methods +- Requires careful handling of covariance reset during training +- May require tuning of spectral normalization parameters + + +### energy + +**OOD Detection Performance**: +- AUROC: 0.4142 +- AUPR: 0.1594 +- FPR@95%TPR: 0.9298 + +**Classification Performance**: +- Accuracy: 0.4298 +- F1 Score: 0.3387 + +**Computational Performance**: +- Inference Time (Batch Size 1): 441.98 ms +- Inference Time (Batch Size 32): 11309.84 ms + +**Pros**: +- Simple to implement on top of existing models +- No architectural changes required +- Theoretically well-founded for OOD detection + +**Cons**: +- Performance can be sensitive to energy temperature parameter +- May require additional training with energy-based loss +- Less effective for detecting certain types of OOD examples + + +### sngp_energy + +**OOD Detection Performance**: +- AUROC: 0.6109 +- AUPR: 0.2633 +- FPR@95%TPR: 0.7982 + +**Classification Performance**: +- Accuracy: 0.5175 +- F1 Score: 0.4299 + +**Computational Performance**: +- Inference Time (Batch Size 1): 583.02 ms +- Inference Time (Batch Size 32): 10552.23 ms + +**Pros**: +- Combines benefits of both SNGP and energy-based methods +- Better OOD detection through complementary approaches +- Robust to different types of OOD examples + +**Cons**: +- Most complex implementation among all methods +- Higher computational overhead during training +- Requires tuning of multiple hyperparameters + + +### softmax + +**OOD Detection Performance**: +- AUROC: 0.3452 +- AUPR: 0.1410 +- FPR@95%TPR: 0.8947 + +**Classification Performance**: +- Accuracy: 0.4737 +- F1 Score: 0.3933 + +**Computational Performance**: +- Inference Time (Batch Size 1): 447.11 ms +- Inference Time (Batch Size 32): 10981.72 ms + +**Pros**: +- Simplest approach with minimal changes to existing models +- Fast inference with no additional computation +- Well-understood calibration techniques available + +**Cons**: +- Often less effective than more sophisticated methods +- Requires proper calibration for reliable uncertainty +- Tends to be overconfident far from the decision boundary + + +## Final Decision + +We have selected **sngp_energy** as our OOD detection approach based on its strong performance across multiple metrics. The combined SNGP+Energy approach provides the most robust OOD detection by leveraging both distance awareness and energy scores. Despite its higher complexity, the superior detection performance justifies the implementation effort. + +## Implementation Plan + +### Integration Steps + +1. Implement the selected OOD detection method within the existing classification model +2. Establish appropriate uncertainty thresholds based on validation data +3. Set up monitoring for OOD detection performance in production +4. Create fallback mechanisms for handling detected OOD examples + +### Threshold Selection + +We recommend setting the uncertainty threshold to achieve a 95% true positive rate (TPR) on validation data. This corresponds to correctly identifying 95% of in-distribution examples, with the remaining 5% incorrectly flagged as OOD. Based on our experiments, this threshold provides a good balance between catching OOD examples and minimizing false alarms. + +## Conclusion + +The selected OOD detection approach will enable the system to identify conversations that fall outside the trained distribution, allowing for more reliable classification and appropriate handling of uncertain cases. This implementation will improve the overall robustness of the conversation classification system and enhance user experience by avoiding incorrect classifications when the model is uncertain. diff --git a/experiments/ood_detection/outputs/all_results.json b/experiments/ood_detection/outputs/all_results.json new file mode 100644 index 00000000..fb5819e2 --- /dev/null +++ b/experiments/ood_detection/outputs/all_results.json @@ -0,0 +1,194 @@ +{ + "sngp": { + "auroc": 0.0, + "aupr": 0.1, + "fpr_at_95_tpr": 1.0, + "detection_error": 0.5, + "accuracy": 0.6052631578947368, + "f1": 0.5062061403508772, + "precision": 0.43523639607493314, + "recall": 0.6052631578947368, + "inference_performance": { + "1": { + "mean": 0.5653338956832886, + "std": 0.03632612477177071, + "median": 0.5570024251937866, + "p95": 0.6165968537330627, + "throughput": 1.7688661649968005 + }, + "4": { + "mean": 1.58567129611969, + "std": 0.08316845234017256, + "median": 1.5769438743591309, + "p95": 1.7068063497543335, + "throughput": 2.5225909113625473 + }, + "8": { + "mean": 3.128090014457703, + "std": 0.06960497184678484, + "median": 3.1290825605392456, + "p95": 3.2246095299720765, + "throughput": 2.5574711606842646 + }, + "16": { + "mean": 5.2573831748962405, + "std": 0.07349068220341584, + "median": 5.254237174987793, + "p95": 5.38769474029541, + "throughput": 3.043339141875611 + }, + "32": { + "mean": 11.451043148040771, + "std": 0.11335574839632633, + "median": 11.475481629371643, + "p95": 11.588285684585571, + "throughput": 2.7945052329555735 + } + }, + "training_time": 1248.081405878067 + }, + "energy": { + "auroc": 0.4141604010025063, + "aupr": 0.15938910454945654, + "fpr_at_95_tpr": 0.9298245614035088, + "detection_error": 0.4746240601503759, + "accuracy": 0.4298245614035088, + "f1": 0.3387018172671823, + "precision": 0.3068129905019058, + "recall": 0.4298245614035088, + "inference_performance": { + "1": { + "mean": 0.44198337078094485, + "std": 0.03890354951060363, + "median": 0.44147658348083496, + "p95": 0.5050738930702209, + "throughput": 2.2625285612739003 + }, + "4": { + "mean": 1.4414128684997558, + "std": 0.11240492103983447, + "median": 1.4490411281585693, + "p95": 1.6173357725143434, + "throughput": 2.775055008467671 + }, + "8": { + "mean": 3.039941945075989, + "std": 0.06899306223444922, + "median": 3.0450851917266846, + "p95": 3.1399591088294985, + "throughput": 2.631629203629422 + }, + "16": { + "mean": 5.089843339920044, + "std": 0.08004480020755828, + "median": 5.09161102771759, + "p95": 5.217811453342438, + "throughput": 3.143515218731927 + }, + "32": { + "mean": 11.309839057922364, + "std": 0.1413031546142975, + "median": 11.306654572486877, + "p95": 11.539428162574769, + "throughput": 2.829394815975255 + } + }, + "training_time": 1140.0389032363892 + }, + "sngp_energy": { + "auroc": 0.6109022556390977, + "aupr": 0.2632667452474731, + "fpr_at_95_tpr": 0.7982456140350878, + "detection_error": 0.36685463659147866, + "accuracy": 0.5175438596491229, + "f1": 0.42990172835374074, + "precision": 0.3759968102073365, + "recall": 0.5175438596491229, + "inference_performance": { + "1": { + "mean": 0.5830196619033814, + "std": 0.037912666595294624, + "median": 0.5780673027038574, + "p95": 0.6468373298645019, + "throughput": 1.7152080201468762 + }, + "4": { + "mean": 1.1503483295440673, + "std": 0.04287870075431241, + "median": 1.1457637548446655, + "p95": 1.2317700147628785, + "throughput": 3.4772076398679803 + }, + "8": { + "mean": 2.728489866256714, + "std": 0.17961506230025276, + "median": 2.7045446634292603, + "p95": 3.0523144006729126, + "throughput": 2.9320248167076417 + }, + "16": { + "mean": 5.400506286621094, + "std": 0.05737283111249048, + "median": 5.403199553489685, + "p95": 5.476700556278229, + "throughput": 2.962685191134299 + }, + "32": { + "mean": 10.552234711647033, + "std": 0.09468020676085033, + "median": 10.561939239501953, + "p95": 10.708769345283509, + "throughput": 3.0325330012494875 + } + }, + "training_time": 1276.3299078941345 + }, + "softmax": { + "auroc": 0.34523809523809523, + "aupr": 0.14095933646734277, + "fpr_at_95_tpr": 0.8947368421052632, + "detection_error": 0.4517543859649123, + "accuracy": 0.47368421052631576, + "f1": 0.3932569266718593, + "precision": 0.3426222561234003, + "recall": 0.47368421052631576, + "inference_performance": { + "1": { + "mean": 0.4471066617965698, + "std": 0.03739638365542985, + "median": 0.4523564577102661, + "p95": 0.5054608583450317, + "throughput": 2.2366027738924465 + }, + "4": { + "mean": 1.5074847316741944, + "std": 0.11208719314757529, + "median": 1.5021862983703613, + "p95": 1.6777991771697998, + "throughput": 2.6534265428729404 + }, + "8": { + "mean": 3.1288017416000367, + "std": 0.05569942714643005, + "median": 3.132371425628662, + "p95": 3.2240431666374207, + "throughput": 2.556889397507457 + }, + "16": { + "mean": 5.216333742141724, + "std": 0.07646626084283274, + "median": 5.220181465148926, + "p95": 5.325281310081482, + "throughput": 3.0672884042558817 + }, + "32": { + "mean": 10.981720747947692, + "std": 0.10138486757175662, + "median": 10.97679615020752, + "p95": 11.123787391185761, + "throughput": 2.9139331380266875 + } + }, + "training_time": 1141.5909054279327 + } +} \ No newline at end of file diff --git a/experiments/ood_detection/outputs/energy/evaluation_results.json b/experiments/ood_detection/outputs/energy/evaluation_results.json new file mode 100644 index 00000000..8a3c55db --- /dev/null +++ b/experiments/ood_detection/outputs/energy/evaluation_results.json @@ -0,0 +1,48 @@ +{ + "auroc": 0.4141604010025063, + "aupr": 0.15938910454945654, + "fpr_at_95_tpr": 0.9298245614035088, + "detection_error": 0.4746240601503759, + "accuracy": 0.4298245614035088, + "f1": 0.3387018172671823, + "precision": 0.3068129905019058, + "recall": 0.4298245614035088, + "inference_performance": { + "1": { + "mean": 0.44198337078094485, + "std": 0.03890354951060363, + "median": 0.44147658348083496, + "p95": 0.5050738930702209, + "throughput": 2.2625285612739003 + }, + "4": { + "mean": 1.4414128684997558, + "std": 0.11240492103983447, + "median": 1.4490411281585693, + "p95": 1.6173357725143434, + "throughput": 2.775055008467671 + }, + "8": { + "mean": 3.039941945075989, + "std": 0.06899306223444922, + "median": 3.0450851917266846, + "p95": 3.1399591088294985, + "throughput": 2.631629203629422 + }, + "16": { + "mean": 5.089843339920044, + "std": 0.08004480020755828, + "median": 5.09161102771759, + "p95": 5.217811453342438, + "throughput": 3.143515218731927 + }, + "32": { + "mean": 11.309839057922364, + "std": 0.1413031546142975, + "median": 11.306654572486877, + "p95": 11.539428162574769, + "throughput": 2.829394815975255 + } + }, + "training_time": 1140.0389032363892 +} \ No newline at end of file diff --git a/experiments/ood_detection/outputs/energy/visualizations/energy-based_confusion_matrix.png b/experiments/ood_detection/outputs/energy/visualizations/energy-based_confusion_matrix.png new file mode 100644 index 00000000..fa14c255 Binary files /dev/null and b/experiments/ood_detection/outputs/energy/visualizations/energy-based_confusion_matrix.png differ diff --git a/experiments/ood_detection/outputs/energy/visualizations/energy-based_pr_curve.png b/experiments/ood_detection/outputs/energy/visualizations/energy-based_pr_curve.png new file mode 100644 index 00000000..52c734ed Binary files /dev/null and b/experiments/ood_detection/outputs/energy/visualizations/energy-based_pr_curve.png differ diff --git a/experiments/ood_detection/outputs/energy/visualizations/energy-based_roc_curve.png b/experiments/ood_detection/outputs/energy/visualizations/energy-based_roc_curve.png new file mode 100644 index 00000000..961113c8 Binary files /dev/null and b/experiments/ood_detection/outputs/energy/visualizations/energy-based_roc_curve.png differ diff --git a/experiments/ood_detection/outputs/energy/visualizations/energy-based_score_distributions.png b/experiments/ood_detection/outputs/energy/visualizations/energy-based_score_distributions.png new file mode 100644 index 00000000..1586cc0d Binary files /dev/null and b/experiments/ood_detection/outputs/energy/visualizations/energy-based_score_distributions.png differ diff --git a/experiments/ood_detection/outputs/energy/visualizations/energy-based_threshold_impact.png b/experiments/ood_detection/outputs/energy/visualizations/energy-based_threshold_impact.png new file mode 100644 index 00000000..36f2ef29 Binary files /dev/null and b/experiments/ood_detection/outputs/energy/visualizations/energy-based_threshold_impact.png differ diff --git a/experiments/ood_detection/outputs/sngp/evaluation_results.json b/experiments/ood_detection/outputs/sngp/evaluation_results.json new file mode 100644 index 00000000..c138d16d --- /dev/null +++ b/experiments/ood_detection/outputs/sngp/evaluation_results.json @@ -0,0 +1,48 @@ +{ + "auroc": 0.0, + "aupr": 0.1, + "fpr_at_95_tpr": 1.0, + "detection_error": 0.5, + "accuracy": 0.6052631578947368, + "f1": 0.5062061403508772, + "precision": 0.43523639607493314, + "recall": 0.6052631578947368, + "inference_performance": { + "1": { + "mean": 0.5653338956832886, + "std": 0.03632612477177071, + "median": 0.5570024251937866, + "p95": 0.6165968537330627, + "throughput": 1.7688661649968005 + }, + "4": { + "mean": 1.58567129611969, + "std": 0.08316845234017256, + "median": 1.5769438743591309, + "p95": 1.7068063497543335, + "throughput": 2.5225909113625473 + }, + "8": { + "mean": 3.128090014457703, + "std": 0.06960497184678484, + "median": 3.1290825605392456, + "p95": 3.2246095299720765, + "throughput": 2.5574711606842646 + }, + "16": { + "mean": 5.2573831748962405, + "std": 0.07349068220341584, + "median": 5.254237174987793, + "p95": 5.38769474029541, + "throughput": 3.043339141875611 + }, + "32": { + "mean": 11.451043148040771, + "std": 0.11335574839632633, + "median": 11.475481629371643, + "p95": 11.588285684585571, + "throughput": 2.7945052329555735 + } + }, + "training_time": 1248.081405878067 +} \ No newline at end of file diff --git a/experiments/ood_detection/outputs/sngp/visualizations/sngp_confusion_matrix.png b/experiments/ood_detection/outputs/sngp/visualizations/sngp_confusion_matrix.png new file mode 100644 index 00000000..327e2666 Binary files /dev/null and b/experiments/ood_detection/outputs/sngp/visualizations/sngp_confusion_matrix.png differ diff --git a/experiments/ood_detection/outputs/sngp/visualizations/sngp_pr_curve.png b/experiments/ood_detection/outputs/sngp/visualizations/sngp_pr_curve.png new file mode 100644 index 00000000..b7fd4621 Binary files /dev/null and b/experiments/ood_detection/outputs/sngp/visualizations/sngp_pr_curve.png differ diff --git a/experiments/ood_detection/outputs/sngp/visualizations/sngp_roc_curve.png b/experiments/ood_detection/outputs/sngp/visualizations/sngp_roc_curve.png new file mode 100644 index 00000000..986e675b Binary files /dev/null and b/experiments/ood_detection/outputs/sngp/visualizations/sngp_roc_curve.png differ diff --git a/experiments/ood_detection/outputs/sngp/visualizations/sngp_score_distributions.png b/experiments/ood_detection/outputs/sngp/visualizations/sngp_score_distributions.png new file mode 100644 index 00000000..11774d9d Binary files /dev/null and b/experiments/ood_detection/outputs/sngp/visualizations/sngp_score_distributions.png differ diff --git a/experiments/ood_detection/outputs/sngp_energy/evaluation_results.json b/experiments/ood_detection/outputs/sngp_energy/evaluation_results.json new file mode 100644 index 00000000..e54c8171 --- /dev/null +++ b/experiments/ood_detection/outputs/sngp_energy/evaluation_results.json @@ -0,0 +1,48 @@ +{ + "auroc": 0.6109022556390977, + "aupr": 0.2632667452474731, + "fpr_at_95_tpr": 0.7982456140350878, + "detection_error": 0.36685463659147866, + "accuracy": 0.5175438596491229, + "f1": 0.42990172835374074, + "precision": 0.3759968102073365, + "recall": 0.5175438596491229, + "inference_performance": { + "1": { + "mean": 0.5830196619033814, + "std": 0.037912666595294624, + "median": 0.5780673027038574, + "p95": 0.6468373298645019, + "throughput": 1.7152080201468762 + }, + "4": { + "mean": 1.1503483295440673, + "std": 0.04287870075431241, + "median": 1.1457637548446655, + "p95": 1.2317700147628785, + "throughput": 3.4772076398679803 + }, + "8": { + "mean": 2.728489866256714, + "std": 0.17961506230025276, + "median": 2.7045446634292603, + "p95": 3.0523144006729126, + "throughput": 2.9320248167076417 + }, + "16": { + "mean": 5.400506286621094, + "std": 0.05737283111249048, + "median": 5.403199553489685, + "p95": 5.476700556278229, + "throughput": 2.962685191134299 + }, + "32": { + "mean": 10.552234711647033, + "std": 0.09468020676085033, + "median": 10.561939239501953, + "p95": 10.708769345283509, + "throughput": 3.0325330012494875 + } + }, + "training_time": 1276.3299078941345 +} \ No newline at end of file diff --git a/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_confusion_matrix.png b/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_confusion_matrix.png new file mode 100644 index 00000000..bc33855b Binary files /dev/null and b/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_confusion_matrix.png differ diff --git a/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_pr_curve.png b/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_pr_curve.png new file mode 100644 index 00000000..9049878c Binary files /dev/null and b/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_pr_curve.png differ diff --git a/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_roc_curve.png b/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_roc_curve.png new file mode 100644 index 00000000..a1cafbc7 Binary files /dev/null and b/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_roc_curve.png differ diff --git a/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_score_distributions.png b/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_score_distributions.png new file mode 100644 index 00000000..781bacfb Binary files /dev/null and b/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_score_distributions.png differ diff --git a/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_threshold_impact.png b/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_threshold_impact.png new file mode 100644 index 00000000..5bc6a0a1 Binary files /dev/null and b/experiments/ood_detection/outputs/sngp_energy/visualizations/sngp+energy_threshold_impact.png differ diff --git a/experiments/ood_detection/outputs/softmax/evaluation_results.json b/experiments/ood_detection/outputs/softmax/evaluation_results.json new file mode 100644 index 00000000..89aa79fe --- /dev/null +++ b/experiments/ood_detection/outputs/softmax/evaluation_results.json @@ -0,0 +1,48 @@ +{ + "auroc": 0.34523809523809523, + "aupr": 0.14095933646734277, + "fpr_at_95_tpr": 0.8947368421052632, + "detection_error": 0.4517543859649123, + "accuracy": 0.47368421052631576, + "f1": 0.3932569266718593, + "precision": 0.3426222561234003, + "recall": 0.47368421052631576, + "inference_performance": { + "1": { + "mean": 0.4471066617965698, + "std": 0.03739638365542985, + "median": 0.4523564577102661, + "p95": 0.5054608583450317, + "throughput": 2.2366027738924465 + }, + "4": { + "mean": 1.5074847316741944, + "std": 0.11208719314757529, + "median": 1.5021862983703613, + "p95": 1.6777991771697998, + "throughput": 2.6534265428729404 + }, + "8": { + "mean": 3.1288017416000367, + "std": 0.05569942714643005, + "median": 3.132371425628662, + "p95": 3.2240431666374207, + "throughput": 2.556889397507457 + }, + "16": { + "mean": 5.216333742141724, + "std": 0.07646626084283274, + "median": 5.220181465148926, + "p95": 5.325281310081482, + "throughput": 3.0672884042558817 + }, + "32": { + "mean": 10.981720747947692, + "std": 0.10138486757175662, + "median": 10.97679615020752, + "p95": 11.123787391185761, + "throughput": 2.9139331380266875 + } + }, + "training_time": 1141.5909054279327 +} \ No newline at end of file diff --git a/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_confusion_matrix.png b/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_confusion_matrix.png new file mode 100644 index 00000000..ee9eea7a Binary files /dev/null and b/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_confusion_matrix.png differ diff --git a/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_pr_curve.png b/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_pr_curve.png new file mode 100644 index 00000000..019c837a Binary files /dev/null and b/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_pr_curve.png differ diff --git a/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_roc_curve.png b/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_roc_curve.png new file mode 100644 index 00000000..a6c9cd57 Binary files /dev/null and b/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_roc_curve.png differ diff --git a/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_score_distributions.png b/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_score_distributions.png new file mode 100644 index 00000000..8b333989 Binary files /dev/null and b/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_score_distributions.png differ diff --git a/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_threshold_impact.png b/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_threshold_impact.png new file mode 100644 index 00000000..22592722 Binary files /dev/null and b/experiments/ood_detection/outputs/softmax/visualizations/softmax_(max_prob)_threshold_impact.png differ diff --git a/experiments/ood_detection/outputs/visualizations/inference_time_comparison.png b/experiments/ood_detection/outputs/visualizations/inference_time_comparison.png new file mode 100644 index 00000000..2f07fcbc Binary files /dev/null and b/experiments/ood_detection/outputs/visualizations/inference_time_comparison.png differ diff --git a/experiments/ood_detection/outputs/visualizations/model_comparison_accuracy.png b/experiments/ood_detection/outputs/visualizations/model_comparison_accuracy.png new file mode 100644 index 00000000..bd046169 Binary files /dev/null and b/experiments/ood_detection/outputs/visualizations/model_comparison_accuracy.png differ diff --git a/experiments/ood_detection/outputs/visualizations/model_comparison_aupr.png b/experiments/ood_detection/outputs/visualizations/model_comparison_aupr.png new file mode 100644 index 00000000..5ecf4926 Binary files /dev/null and b/experiments/ood_detection/outputs/visualizations/model_comparison_aupr.png differ diff --git a/experiments/ood_detection/outputs/visualizations/model_comparison_auroc.png b/experiments/ood_detection/outputs/visualizations/model_comparison_auroc.png new file mode 100644 index 00000000..395fc7b6 Binary files /dev/null and b/experiments/ood_detection/outputs/visualizations/model_comparison_auroc.png differ diff --git a/experiments/ood_detection/outputs/visualizations/model_comparison_detection_error.png b/experiments/ood_detection/outputs/visualizations/model_comparison_detection_error.png new file mode 100644 index 00000000..320131e2 Binary files /dev/null and b/experiments/ood_detection/outputs/visualizations/model_comparison_detection_error.png differ diff --git a/experiments/ood_detection/outputs/visualizations/model_comparison_f1.png b/experiments/ood_detection/outputs/visualizations/model_comparison_f1.png new file mode 100644 index 00000000..0d1165ce Binary files /dev/null and b/experiments/ood_detection/outputs/visualizations/model_comparison_f1.png differ diff --git a/experiments/ood_detection/outputs/visualizations/model_comparison_fpr_at_95_tpr.png b/experiments/ood_detection/outputs/visualizations/model_comparison_fpr_at_95_tpr.png new file mode 100644 index 00000000..67a98d8c Binary files /dev/null and b/experiments/ood_detection/outputs/visualizations/model_comparison_fpr_at_95_tpr.png differ diff --git a/experiments/ood_detection/outputs/visualizations/multi_metric_model_comparison.png b/experiments/ood_detection/outputs/visualizations/multi_metric_model_comparison.png new file mode 100644 index 00000000..e4ec18e8 Binary files /dev/null and b/experiments/ood_detection/outputs/visualizations/multi_metric_model_comparison.png differ diff --git a/experiments/ood_detection/outputs/visualizations/throughput_comparison.png b/experiments/ood_detection/outputs/visualizations/throughput_comparison.png new file mode 100644 index 00000000..f4542bdb Binary files /dev/null and b/experiments/ood_detection/outputs/visualizations/throughput_comparison.png differ diff --git a/experiments/ood_detection/requirements.txt b/experiments/ood_detection/requirements.txt new file mode 100644 index 00000000..2372cbc6 --- /dev/null +++ b/experiments/ood_detection/requirements.txt @@ -0,0 +1,18 @@ +tensorflow>=2.7.0 +tensorflow-hub>=0.12.0 +transformers>=4.11.3 +scikit-learn>=1.0.2 +pandas>=1.3.5 +numpy>=1.21.4 +matplotlib>=3.5.1 +seaborn>=0.11.2 +mlflow>=1.23.1 +tqdm>=4.62.3 +pillow>=9.0.1 +scipy>=1.7.3 +ipython>=7.31.0 +tensorboard>=2.8.0 +# Additional packages that might be missing +protobuf>=3.20.0 +packaging +importlib-metadata \ No newline at end of file diff --git a/experiments/ood_detection/scripts/evaluate_models.sh b/experiments/ood_detection/scripts/evaluate_models.sh new file mode 100644 index 00000000..19a51ca2 --- /dev/null +++ b/experiments/ood_detection/scripts/evaluate_models.sh @@ -0,0 +1,61 @@ +""" +Evaluate models on test and OOD data. +""" + +# This script evaluates trained models on test and OOD datasets, +# generating visualizations and metrics for comparison. + +set -e # Exit on any error + +# Set parameters +TEST_FILE="data/test.csv" +OOD_FILE="data/ood_test.csv" +OUTPUT_DIR="outputs/evaluation" +USE_GPU="--use-gpu" # Leave empty to use CPU, set to "--use-gpu" to use GPU + +# Check for GPU flag +if [ "$1" == "--use-gpu" ]; then + USE_GPU="--use-gpu" + echo "Using GPU for evaluation" +else + echo "Using CPU for evaluation" +fi + +# Create output directory +mkdir -p $OUTPUT_DIR + +# Models to evaluate +MODELS=("sngp" "energy" "sngp_energy" "ood_class" "softmax") + +# Evaluate each model +for MODEL in "${MODELS[@]}"; do + echo "=== Evaluating $MODEL model ===" + + # Path to model directory + MODEL_DIR="outputs/$MODEL/model/final_model.keras" + + # Check if model exists + if [ ! -d "$MODEL_DIR" ]; then + echo "Model not found: $MODEL_DIR" + continue + fi + + # Evaluate on test and OOD data + python experiments/evaluate_model.py \ + --model-path $MODEL_DIR \ + --model-type $MODEL \ + --test-file $TEST_FILE \ + --ood-file $OOD_FILE \ + --output-dir $OUTPUT_DIR/$MODEL \ + $USE_GPU + + echo "Evaluation complete for $MODEL" + echo "Results saved to $OUTPUT_DIR/$MODEL" +done + +# Generate comparison visualizations +echo "=== Generating model comparisons ===" +python utils/comparison_viz.py --results-dir $OUTPUT_DIR --output-dir $OUTPUT_DIR/comparison + +echo "=== Evaluation complete ===" +echo "Results saved to $OUTPUT_DIR" \ No newline at end of file diff --git a/experiments/ood_detection/scripts/run_all_experiments.sh b/experiments/ood_detection/scripts/run_all_experiments.sh new file mode 100644 index 00000000..7b0cb60e --- /dev/null +++ b/experiments/ood_detection/scripts/run_all_experiments.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Run all experiments and generate analysis reports. +# This script runs all OOD detection experiments sequentially +# and then generates the analysis and decision documents. + +set -e # Exit on any error + +# Setup environment first +echo "Setting up environment..." +python setup_environment.py + +# Set PYTHONPATH to include current directory +export PYTHONPATH="${PWD}:${PYTHONPATH}" + +# Create output directory +mkdir -p outputs + +# Set parameters +TRAIN_FILE="data/train.csv" +DEV_FILE="data/dev.csv" +TEST_FILE="data/test.csv" +OOD_FILE="data/ood_test.csv" +BATCH_SIZE=32 +EPOCHS=10 +SEED=42 +OUTPUT_DIR="outputs" +USE_GPU="--use-gpu" # Leave empty to use CPU, set to "--use-gpu" to use GPU + +# Check for GPU flag +if [ "$1" = "--use-gpu" ]; then + USE_GPU="--use-gpu" + echo "Using GPU for training" +else + echo "Using CPU for training" +fi + +# Run all experiments +echo "=== Running all OOD detection experiments ===" +python main.py --experiment all \ + --train-file $TRAIN_FILE \ + --dev-file $DEV_FILE \ + --test-file $TEST_FILE \ + --ood-file $OOD_FILE \ + --output-dir $OUTPUT_DIR \ + --batch-size $BATCH_SIZE \ + --epochs $EPOCHS \ + --seed $SEED \ + $USE_GPU \ + --log-performance \ + --analyze + +echo "=== All experiments completed ===" +echo "Results saved to $OUTPUT_DIR" +echo "Analysis report: $OUTPUT_DIR/ANALYSIS.md" +echo "Decision document: $OUTPUT_DIR/DECISION.md" \ No newline at end of file diff --git a/experiments/ood_detection/utils/__init__.py b/experiments/ood_detection/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/experiments/ood_detection/utils/mlflow_logger.py b/experiments/ood_detection/utils/mlflow_logger.py new file mode 100644 index 00000000..75d1fd15 --- /dev/null +++ b/experiments/ood_detection/utils/mlflow_logger.py @@ -0,0 +1,508 @@ +""" +MLflow logging utilities for OOD detection experiments. +""" + +import mlflow +import os +import json +import logging +from typing import Dict, List, Any, Optional +import tensorflow as tf +import numpy as np +import matplotlib.pyplot as plt +from PIL import Image +import io + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class MLflowLogger: + """ + MLflow logger for OOD detection experiments. + + This class provides methods for logging metrics, parameters, + artifacts, and model information to MLflow. + """ + + def __init__( + self, + experiment_name: str, + tracking_uri: Optional[str] = None, + model_dir: str = "models", + artifact_dir: str = "artifacts", + create_dirs: bool = True, + ): + """ + Initialize MLflow logger. + + Args: + experiment_name: Name of the MLflow experiment + tracking_uri: MLflow tracking URI (None for local) + model_dir: Directory to save models + artifact_dir: Directory to save artifacts + create_dirs: Whether to create directories + """ + self.experiment_name = experiment_name + + # Set tracking URI if provided + if tracking_uri: + mlflow.set_tracking_uri(tracking_uri) + + # Set up directories + self.model_dir = model_dir + self.artifact_dir = artifact_dir + + if create_dirs: + os.makedirs(model_dir, exist_ok=True) + os.makedirs(artifact_dir, exist_ok=True) + + # Get or create experiment + try: + self.experiment_id = mlflow.create_experiment(experiment_name) + logger.info(f"Created new experiment: {experiment_name}") + except Exception as e: + # If experiment already exists, get its ID + logger.warning(f"Experiment '{experiment_name}' already exists: {e}") + self.experiment_id = mlflow.get_experiment_by_name( + experiment_name + ).experiment_id + logger.info(f"Using existing experiment: {experiment_name}") + + def start_run( + self, run_name: Optional[str] = None, tags: Optional[Dict[str, str]] = None + ) -> str: + """ + Start a new MLflow run. + + Args: + run_name: Name of the run + tags: Additional tags for the run + + Returns: + run_id: The ID of the run + """ + # End any existing active run first + if mlflow.active_run() is not None: + logger.warning("Ending existing active MLflow run") + mlflow.end_run() + + mlflow.start_run(experiment_id=self.experiment_id, run_name=run_name, tags=tags) + + run_id = mlflow.active_run().info.run_id + logger.info(f"Started MLflow run: {run_id}") + + return run_id + + def end_run(self): + """End the current MLflow run.""" + try: + if mlflow.active_run() is not None: + mlflow.end_run() + logger.info("Ended MLflow run") + else: + logger.info("No active MLflow run to end") + except Exception as e: + logger.warning(f"Error ending MLflow run: {e}") + + def log_params(self, params: Dict[str, Any]): + """ + Log parameters to MLflow. + + Args: + params: Dictionary of parameters + """ + # Convert non-string parameters to strings + params_str = {} + for k, v in params.items(): + if isinstance(v, (dict, list, tuple)): + params_str[k] = json.dumps(v) + else: + params_str[k] = str(v) + + mlflow.log_params(params_str) + logger.info(f"Logged {len(params)} parameters") + + def log_metrics(self, metrics: Dict[str, float], step: Optional[int] = None): + """ + Log metrics to MLflow. + + Args: + metrics: Dictionary of metrics + step: Step number + """ + mlflow.log_metrics(metrics, step=step) + logger.info(f"Logged {len(metrics)} metrics") + + def log_model( + self, + model: tf.keras.Model, + model_name: str, + signature: Optional[Any] = None, + conda_env: Optional[Dict[str, Any]] = None, + save_format: str = "tf", + ): + """ + Log model to MLflow. + + Args: + model: TensorFlow model + model_name: Name of the model + signature: MLflow model signature + conda_env: Conda environment specification + save_format: Format to save the model + """ + # Save model locally first + model_path = os.path.join(self.model_dir, model_name) + + if save_format == "tf": + # Save as TensorFlow SavedModel + model.save(model_path, save_format="tf") + + # Log model to MLflow + mlflow.tensorflow.log_model( + tf_saved_model_dir=model_path, + tf_meta_graph_tags=None, + tf_signature_def_key="serving_default", + artifact_path=model_name, + signature=signature, + conda_env=conda_env, + ) + elif save_format == "h5": + # Save as Keras H5 model + model.save(model_path + ".h5", save_format="h5") + + # Log model to MLflow + mlflow.keras.log_model( + keras_model=model, + artifact_path=model_name, + signature=signature, + conda_env=conda_env, + ) + else: + raise ValueError(f"Unsupported save format: {save_format}") + + logger.info(f"Logged model: {model_name}") + + def log_figure(self, figure: plt.Figure, artifact_path: str): + """ + Log a matplotlib figure to MLflow. + + Args: + figure: Matplotlib figure + artifact_path: Path where the figure will be saved + """ + # Save figure to a buffer + buf = io.BytesIO() + figure.savefig(buf, format="png", dpi=300, bbox_inches="tight") + buf.seek(0) + + # Convert to PIL Image + img = Image.open(buf) + + # Save temporarily + tmp_path = os.path.join(self.artifact_dir, os.path.basename(artifact_path)) + img.save(tmp_path) + + # Log to MLflow + mlflow.log_artifact(tmp_path, os.path.dirname(artifact_path)) + + # Clean up + plt.close(figure) + os.remove(tmp_path) + + logger.info(f"Logged figure: {artifact_path}") + + def log_artifact(self, local_path: str, artifact_path: Optional[str] = None): + """ + Log an artifact to MLflow. + + Args: + local_path: Local path to the artifact + artifact_path: Path within MLflow where the artifact will be saved + """ + mlflow.log_artifact(local_path, artifact_path) + logger.info(f"Logged artifact: {local_path}") + + def log_dict(self, dictionary: Dict[str, Any], artifact_path: str): + """ + Log a dictionary to MLflow as a JSON file. + + Args: + dictionary: Dictionary to log + artifact_path: Path where the JSON will be saved + """ + mlflow.log_dict(dictionary, artifact_path) + logger.info(f"Logged dictionary: {artifact_path}") + + def log_confusion_matrix( + self, + confusion_matrix: np.ndarray, + class_names: List[str], + title: str = "Confusion Matrix", + artifact_path: str = "confusion_matrix.png", + ): + """ + Log a confusion matrix to MLflow. + + Args: + confusion_matrix: Numpy array of confusion matrix + class_names: List of class names + title: Title of the plot + artifact_path: Path where the figure will be saved + """ + # Create figure + fig, ax = plt.subplots(figsize=(10, 8)) + + import seaborn as sns + + sns.heatmap( + confusion_matrix, + annot=True, + fmt="d", + cmap="Blues", + xticklabels=class_names, + yticklabels=class_names, + ax=ax, + ) + + ax.set_xlabel("Predicted Labels") + ax.set_ylabel("True Labels") + ax.set_title(title) + + # Log the figure + self.log_figure(fig, artifact_path) + + def log_roc_curve( + self, + fpr: np.ndarray, + tpr: np.ndarray, + auroc: float, + title: str = "ROC Curve", + artifact_path: str = "roc_curve.png", + ): + """ + Log a ROC curve to MLflow. + + Args: + fpr: False positive rates + tpr: True positive rates + auroc: Area under ROC curve + title: Title of the plot + artifact_path: Path where the figure will be saved + """ + # Create figure + fig, ax = plt.subplots(figsize=(8, 6)) + + ax.plot(fpr, tpr, lw=2, label=f"AUROC = {auroc:.3f}") + ax.plot([0, 1], [0, 1], "k--", lw=2) + + ax.set_xlim([0.0, 1.0]) + ax.set_ylim([0.0, 1.05]) + ax.set_xlabel("False Positive Rate") + ax.set_ylabel("True Positive Rate") + ax.set_title(title) + ax.legend(loc="lower right") + ax.grid(True, alpha=0.3) + + # Log the figure + self.log_figure(fig, artifact_path) + + def log_pr_curve( + self, + precision: np.ndarray, + recall: np.ndarray, + aupr: float, + title: str = "Precision-Recall Curve", + artifact_path: str = "pr_curve.png", + ): + """ + Log a Precision-Recall curve to MLflow. + + Args: + precision: Precision values + recall: Recall values + aupr: Area under PR curve + title: Title of the plot + artifact_path: Path where the figure will be saved + """ + # Create figure + fig, ax = plt.subplots(figsize=(8, 6)) + + ax.plot(recall, precision, lw=2, label=f"AUPR = {aupr:.3f}") + + ax.set_xlim([0.0, 1.0]) + ax.set_ylim([0.0, 1.05]) + ax.set_xlabel("Recall") + ax.set_ylabel("Precision") + ax.set_title(title) + ax.legend(loc="lower left") + ax.grid(True, alpha=0.3) + + # Log the figure + self.log_figure(fig, artifact_path) + + def log_score_distributions( + self, + id_scores: np.ndarray, + ood_scores: np.ndarray, + title: str = "Uncertainty Score Distributions", + artifact_path: str = "score_distributions.png", + ): + """ + Log distributions of uncertainty scores for ID and OOD examples. + + Args: + id_scores: Uncertainty scores for ID examples + ood_scores: Uncertainty scores for OOD examples + title: Title of the plot + artifact_path: Path where the figure will be saved + """ + # Create figure + fig, ax = plt.subplots(figsize=(10, 6)) + + import seaborn as sns + + sns.histplot( + id_scores, + color="blue", + alpha=0.6, + label="In-Distribution", + kde=True, + stat="density", + ax=ax, + ) + sns.histplot( + ood_scores, + color="red", + alpha=0.6, + label="Out-of-Distribution", + kde=True, + stat="density", + ax=ax, + ) + + # Compute the threshold for 95% TPR + n_id = len(id_scores) + n_ood = len(ood_scores) + + y_true = np.concatenate( + [np.zeros(n_id, dtype=np.int32), np.ones(n_ood, dtype=np.int32)] + ) + + y_score = np.concatenate([id_scores, ood_scores]) + + from sklearn.metrics import roc_curve + + fpr, tpr, thresholds = roc_curve(y_true, y_score) + + # Find the threshold for 95% TPR + target_tpr = 0.95 + idx = np.argmin(np.abs(tpr - target_tpr)) + threshold = thresholds[idx] + + # Add threshold line + ax.axvline( + threshold, color="green", linestyle="--", label="Threshold at 95% TPR" + ) + + ax.set_xlabel("Uncertainty Score") + ax.set_ylabel("Density") + ax.set_title(title) + ax.legend() + + # Log the figure + self.log_figure(fig, artifact_path) + + def log_training_history( + self, + history: Dict[str, List[float]], + metrics: List[str] = None, + title: str = "Training History", + artifact_path: str = "training_history.png", + ): + """ + Log training history to MLflow. + + Args: + history: Dictionary of training history + metrics: List of metrics to plot + title: Title of the plot + artifact_path: Path where the figure will be saved + """ + if metrics is None: + # Use all metrics except validation metrics + metrics = [m for m in history.keys() if not m.startswith("val_")] + + # Create figure + fig, ax = plt.subplots(figsize=(10, 6)) + + epochs = range(1, len(history[metrics[0]]) + 1) + + for metric in metrics: + ax.plot(epochs, history[metric], label=f"Training {metric}") + + # Plot validation metrics if available + val_metric = f"val_{metric}" + if val_metric in history: + ax.plot( + epochs, + history[val_metric], + linestyle="--", + label=f"Validation {metric}", + ) + + ax.set_xlabel("Epoch") + ax.set_ylabel("Value") + ax.set_title(title) + ax.legend() + ax.grid(True, alpha=0.3) + + # Log the figure + self.log_figure(fig, artifact_path) + + def log_model_summary( + self, model: tf.keras.Model, artifact_path: str = "model_summary.txt" + ): + """ + Log model summary to MLflow. + + Args: + model: TensorFlow model + artifact_path: Path where the summary will be saved + """ + # Create a string IO to capture the summary + import io + + summary_io = io.StringIO() + + # Write model summary + model.summary(print_fn=lambda x: summary_io.write(x + "\n")) + + # Save to a temp file + tmp_path = os.path.join(self.artifact_dir, os.path.basename(artifact_path)) + with open(tmp_path, "w") as f: + f.write(summary_io.getvalue()) + + # Log to MLflow + mlflow.log_artifact(tmp_path, os.path.dirname(artifact_path)) + + # Clean up + os.remove(tmp_path) + + logger.info(f"Logged model summary: {artifact_path}") + + def log_experiment_summary( + self, + experiment_results: Dict[str, Dict[str, float]], + artifact_path: str = "experiment_summary.json", + ): + """ + Log experiment summary to MLflow. + + Args: + experiment_results: Dictionary of results for different models + artifact_path: Path where the summary will be saved + """ + mlflow.log_dict(experiment_results, artifact_path) + logger.info(f"Logged experiment summary: {artifact_path}") diff --git a/experiments/ood_detection/utils/visualization.py b/experiments/ood_detection/utils/visualization.py new file mode 100644 index 00000000..69f7db12 --- /dev/null +++ b/experiments/ood_detection/utils/visualization.py @@ -0,0 +1,657 @@ +""" +Visualization utilities for OOD detection results. +""" + +import matplotlib.pyplot as plt +import seaborn as sns +import numpy as np +import pandas as pd +from typing import Dict, List +import os +import logging +from sklearn.metrics import roc_curve, precision_recall_curve, confusion_matrix +from sklearn.manifold import TSNE +from sklearn.decomposition import PCA + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class Visualizer: + """ + Visualization utilities for OOD detection results. + """ + + def __init__(self, output_dir: str = "visualizations"): + """ + Initialize visualizer. + + Args: + output_dir: Directory to save visualizations + """ + self.output_dir = output_dir + os.makedirs(output_dir, exist_ok=True) + + def plot_roc_curve( + self, + id_uncertainties: np.ndarray, + ood_uncertainties: np.ndarray, + model_name: str = "Model", + title: str = "ROC Curve", + save: bool = True, + ) -> plt.Figure: + """ + Plot ROC curve for OOD detection. + + Args: + id_uncertainties: Uncertainty scores for ID examples + ood_uncertainties: Uncertainty scores for OOD examples + model_name: Name of the model + title: Title of the plot + save: Whether to save the plot + + Returns: + fig: Figure object + """ + # Create binary labels + n_id = len(id_uncertainties) + n_ood = len(ood_uncertainties) + + y_true = np.concatenate( + [ + np.zeros(n_id, dtype=np.int32), # ID examples + np.ones(n_ood, dtype=np.int32), # OOD examples + ] + ) + + # Concatenate uncertainties + y_score = np.concatenate([id_uncertainties, ood_uncertainties]) + + # Compute ROC curve + fpr, tpr, _ = roc_curve(y_true, y_score) + + # Compute AUROC + auroc = self._calculate_auroc(fpr, tpr) + + # Create figure + fig, ax = plt.subplots(figsize=(8, 6)) + + ax.plot(fpr, tpr, lw=2, label=f"AUROC = {auroc:.3f}") + ax.plot([0, 1], [0, 1], "k--", lw=2) + + ax.set_xlim([0.0, 1.0]) + ax.set_ylim([0.0, 1.05]) + ax.set_xlabel("False Positive Rate") + ax.set_ylabel("True Positive Rate") + ax.set_title(f"{title} - {model_name}") + ax.legend(loc="lower right") + ax.grid(True, alpha=0.3) + + if save: + save_path = os.path.join( + self.output_dir, f"{model_name.lower().replace(' ', '_')}_roc_curve.png" + ) + plt.savefig(save_path, bbox_inches="tight", dpi=300) + logger.info(f"Saved ROC curve to {save_path}") + + return fig + + def plot_pr_curve( + self, + id_uncertainties: np.ndarray, + ood_uncertainties: np.ndarray, + model_name: str = "Model", + title: str = "Precision-Recall Curve", + save: bool = True, + ) -> plt.Figure: + """ + Plot Precision-Recall curve for OOD detection. + + Args: + id_uncertainties: Uncertainty scores for ID examples + ood_uncertainties: Uncertainty scores for OOD examples + model_name: Name of the model + title: Title of the plot + save: Whether to save the plot + + Returns: + fig: Figure object + """ + # Create binary labels + n_id = len(id_uncertainties) + n_ood = len(ood_uncertainties) + + y_true = np.concatenate( + [np.zeros(n_id, dtype=np.int32), np.ones(n_ood, dtype=np.int32)] + ) + + # Concatenate uncertainties + y_score = np.concatenate([id_uncertainties, ood_uncertainties]) + + # Compute PR curve + precision, recall, _ = precision_recall_curve(y_true, y_score) + + # Compute AUPR + aupr = self._calculate_aupr(precision, recall) + + # Create figure + fig, ax = plt.subplots(figsize=(8, 6)) + + ax.plot(recall, precision, lw=2, label=f"AUPR = {aupr:.3f}") + + ax.set_xlim([0.0, 1.0]) + ax.set_ylim([0.0, 1.05]) + ax.set_xlabel("Recall") + ax.set_ylabel("Precision") + ax.set_title(f"{title} - {model_name}") + ax.legend(loc="lower left") + ax.grid(True, alpha=0.3) + + if save: + save_path = os.path.join( + self.output_dir, f"{model_name.lower().replace(' ', '_')}_pr_curve.png" + ) + plt.savefig(save_path, bbox_inches="tight", dpi=300) + logger.info(f"Saved PR curve to {save_path}") + + return fig + + def plot_score_distributions( + self, + id_uncertainties: np.ndarray, + ood_uncertainties: np.ndarray, + model_name: str = "Model", + title: str = "Uncertainty Score Distributions", + save: bool = True, + ) -> plt.Figure: + """ + Plot distributions of uncertainty scores. + + Args: + id_uncertainties: Uncertainty scores for ID examples + ood_uncertainties: Uncertainty scores for OOD examples + model_name: Name of the model + title: Title of the plot + save: Whether to save the plot + + Returns: + fig: Figure object + """ + # Create figure + fig, ax = plt.subplots(figsize=(10, 6)) + + # Plot ID and OOD distributions + sns.histplot( + id_uncertainties, + color="blue", + alpha=0.6, + label="In-Distribution", + kde=True, + stat="density", + ax=ax, + ) + sns.histplot( + ood_uncertainties, + color="red", + alpha=0.6, + label="Out-of-Distribution", + kde=True, + stat="density", + ax=ax, + ) + + # Compute the threshold for 95% TPR + n_id = len(id_uncertainties) + n_ood = len(ood_uncertainties) + + y_true = np.concatenate( + [np.zeros(n_id, dtype=np.int32), np.ones(n_ood, dtype=np.int32)] + ) + + y_score = np.concatenate([id_uncertainties, ood_uncertainties]) + + fpr, tpr, thresholds = roc_curve(y_true, y_score) + + # Find the threshold for 95% TPR + target_tpr = 0.95 + idx = np.argmin(np.abs(tpr - target_tpr)) + threshold = thresholds[idx] + + # Calculate FPR at this threshold + fpr_at_threshold = fpr[idx] + + # Add threshold line + ax.axvline( + threshold, + color="green", + linestyle="--", + label=f"Threshold at 95% TPR (FPR={fpr_at_threshold:.3f})", + ) + + ax.set_xlabel("Uncertainty Score") + ax.set_ylabel("Density") + ax.set_title(f"{title} - {model_name}") + ax.legend() + + if save: + save_path = os.path.join( + self.output_dir, + f"{model_name.lower().replace(' ', '_')}_score_distributions.png", + ) + plt.savefig(save_path, bbox_inches="tight", dpi=300) + logger.info(f"Saved score distributions to {save_path}") + + return fig + + def plot_confusion_matrix( + self, + y_true: np.ndarray, + y_pred: np.ndarray, + class_names: List[str], + model_name: str = "Model", + title: str = "Confusion Matrix", + save: bool = True, + ) -> plt.Figure: + """ + Plot confusion matrix. + + Args: + y_true: True labels + y_pred: Predicted labels + class_names: Names of classes + model_name: Name of the model + title: Title of the plot + save: Whether to save the plot + + Returns: + fig: Figure object + """ + # Compute confusion matrix + cm = confusion_matrix(y_true, y_pred) + + # Create figure + fig, ax = plt.subplots(figsize=(10, 8)) + + # Plot confusion matrix + sns.heatmap( + cm, + annot=True, + fmt="d", + cmap="Blues", + xticklabels=class_names, + yticklabels=class_names, + ax=ax, + ) + + ax.set_xlabel("Predicted Labels") + ax.set_ylabel("True Labels") + ax.set_title(f"{title} - {model_name}") + + if save: + save_path = os.path.join( + self.output_dir, + f"{model_name.lower().replace(' ', '_')}_confusion_matrix.png", + ) + plt.savefig(save_path, bbox_inches="tight", dpi=300) + logger.info(f"Saved confusion matrix to {save_path}") + + return fig + + def plot_feature_embeddings( + self, + id_features: np.ndarray, + ood_features: np.ndarray, + method: str = "tsne", + perplexity: int = 30, + n_components: int = 2, + model_name: str = "Model", + title: str = "Feature Embeddings", + save: bool = True, + ) -> plt.Figure: + """ + Plot feature embeddings using dimensionality reduction. + + Args: + id_features: Features for ID examples + ood_features: Features for OOD examples + method: Dimensionality reduction method ('tsne' or 'pca') + perplexity: Perplexity parameter for t-SNE + n_components: Number of components for dimensionality reduction + model_name: Name of the model + title: Title of the plot + save: Whether to save the plot + + Returns: + fig: Figure object + """ + # Combine features + all_features = np.vstack([id_features, ood_features]) + + # Create labels (0 for ID, 1 for OOD) + labels = np.concatenate( + [ + np.zeros(len(id_features), dtype=np.int32), + np.ones(len(ood_features), dtype=np.int32), + ] + ) + + # Apply dimensionality reduction + if method.lower() == "tsne": + reducer = TSNE( + n_components=n_components, perplexity=perplexity, random_state=42 + ) + reduced_features = reducer.fit_transform(all_features) + method_name = f"t-SNE (perplexity={perplexity})" + elif method.lower() == "pca": + reducer = PCA(n_components=n_components, random_state=42) + reduced_features = reducer.fit_transform(all_features) + method_name = f"PCA (n_components={n_components})" + else: + raise ValueError(f"Unsupported method: {method}") + + # Create figure + fig, ax = plt.subplots(figsize=(10, 8)) + + # Scatter plot + scatter = ax.scatter( + reduced_features[:, 0], + reduced_features[:, 1], + c=labels, + cmap="coolwarm", + alpha=0.7, + s=30, + ) + + # Add legend + ax.legend( + handles=scatter.legend_elements()[0], + labels=["In-Distribution", "Out-of-Distribution"], + loc="upper right", + ) + + ax.set_xlabel("Component 1") + ax.set_ylabel("Component 2") + ax.set_title(f"{title} - {model_name}\n({method_name})") + ax.grid(True, alpha=0.3) + + if save: + save_path = os.path.join( + self.output_dir, + f"{model_name.lower().replace(' ', '_')}_{method.lower()}_embeddings.png", + ) + plt.savefig(save_path, bbox_inches="tight", dpi=300) + logger.info(f"Saved feature embeddings to {save_path}") + + return fig + + def plot_model_comparison( + self, + model_metrics: Dict[str, Dict[str, float]], + metric_name: str = "auroc", + title: str = "Model Comparison", + save: bool = True, + ) -> plt.Figure: + """ + Plot comparison of different models based on a metric. + + Args: + model_metrics: Dictionary of metrics for different models + metric_name: Name of the metric to compare + title: Title of the plot + save: Whether to save the plot + + Returns: + fig: Figure object + """ + # Extract model names and metric values + models = list(model_metrics.keys()) + values = [model_metrics[model].get(metric_name, 0) for model in models] + + # Create figure + fig, ax = plt.subplots(figsize=(10, 6)) + + # Bar plot + bars = ax.bar(models, values, color="skyblue", alpha=0.7) + + # Add value labels on top of bars + for bar in bars: + height = bar.get_height() + ax.text( + bar.get_x() + bar.get_width() / 2.0, + height + 0.01, + f"{height:.3f}", + ha="center", + va="bottom", + fontsize=10, + ) + + ax.set_xlabel("Model") + ax.set_ylabel(metric_name.upper()) + ax.set_title(f"{title} - {metric_name.upper()}") + ax.set_ylim(0, 1.05) + ax.grid(True, alpha=0.3, axis="y") + + # Add line at y=0.5 for reference (random classifier) + if metric_name.lower() in [ + "auroc", + "aupr", + "accuracy", + "f1", + "precision", + "recall", + ]: + ax.axhline( + 0.5, color="red", linestyle="--", alpha=0.5, label="Random Classifier" + ) + ax.legend() + + if save: + save_path = os.path.join( + self.output_dir, f"model_comparison_{metric_name.lower()}.png" + ) + plt.savefig(save_path, bbox_inches="tight", dpi=300) + logger.info(f"Saved model comparison to {save_path}") + + return fig + + def plot_multi_metric_comparison( + self, + model_metrics: Dict[str, Dict[str, float]], + metrics: List[str] = ["auroc", "aupr", "fpr_at_95_tpr", "accuracy"], + title: str = "Multi-Metric Model Comparison", + save: bool = True, + ) -> plt.Figure: + """ + Plot comparison of different models based on multiple metrics. + + Args: + model_metrics: Dictionary of metrics for different models + metrics: List of metrics to compare + title: Title of the plot + save: Whether to save the plot + + Returns: + fig: Figure object + """ + # Extract model names + models = list(model_metrics.keys()) + + # Create a DataFrame for easier plotting + data = [] + for model in models: + for metric in metrics: + value = model_metrics[model].get(metric, 0) + data.append({"Model": model, "Metric": metric.upper(), "Value": value}) + + df = pd.DataFrame(data) + + # Create figure + fig, ax = plt.subplots(figsize=(12, 8)) + + # Bar plot + sns.barplot(x="Model", y="Value", hue="Metric", data=df, ax=ax) + + ax.set_xlabel("Model") + ax.set_ylabel("Value") + ax.set_title(title) + ax.set_ylim(0, 1.05) + ax.grid(True, alpha=0.3, axis="y") + + # Adjust legend + ax.legend(title="Metric", bbox_to_anchor=(1.05, 1), loc="upper left") + + plt.tight_layout() + + if save: + save_path = os.path.join( + self.output_dir, "multi_metric_model_comparison.png" + ) + plt.savefig(save_path, bbox_inches="tight", dpi=300) + logger.info(f"Saved multi-metric model comparison to {save_path}") + + return fig + + def plot_uncertainty_threshold_impact( + self, + id_uncertainties: np.ndarray, + ood_uncertainties: np.ndarray, + model_name: str = "Model", + title: str = "Impact of Uncertainty Threshold", + num_thresholds: int = 100, + save: bool = True, + ) -> plt.Figure: + """ + Plot the impact of different uncertainty thresholds. + + Args: + id_uncertainties: Uncertainty scores for ID examples + ood_uncertainties: Uncertainty scores for OOD examples + model_name: Name of the model + title: Title of the plot + num_thresholds: Number of thresholds to evaluate + save: Whether to save the plot + + Returns: + fig: Figure object + """ + # Create binary labels + n_id = len(id_uncertainties) + n_ood = len(ood_uncertainties) + + y_true = np.concatenate( + [np.zeros(n_id, dtype=np.int32), np.ones(n_ood, dtype=np.int32)] + ) + + # Concatenate uncertainties + y_score = np.concatenate([id_uncertainties, ood_uncertainties]) + + # Compute ROC curve + fpr, tpr, thresholds = roc_curve(y_true, y_score) + + # Calculate Detection Error = 0.5 * (1 - TPR + FPR) + detection_error = 0.5 * (1 - tpr + fpr) + + # Find minimum detection error + min_idx = np.argmin(detection_error) + min_threshold = thresholds[min_idx] + min_error = detection_error[min_idx] + + # Find threshold for 95% TPR + target_tpr = 0.95 + tpr95_idx = np.argmin(np.abs(tpr - target_tpr)) + tpr95_threshold = thresholds[tpr95_idx] + tpr95_fpr = fpr[tpr95_idx] + + # Create figure + fig, ax1 = plt.subplots(figsize=(12, 8)) + + # Plot TPR and FPR + ax1.plot(thresholds, tpr, "b-", label="TPR") + ax1.plot(thresholds, fpr, "r-", label="FPR") + ax1.set_xlabel("Uncertainty Threshold") + ax1.set_ylabel("Rate") + ax1.set_title(f"{title} - {model_name}") + + # Add detection error on secondary y-axis + ax2 = ax1.twinx() + ax2.plot(thresholds, detection_error, "g-", label="Detection Error") + ax2.set_ylabel("Detection Error", color="g") + + # Mark minimum detection error + ax1.axvline(min_threshold, color="black", linestyle="--") + ax1.text( + min_threshold, + 0.5, + f"Min Error: {min_error:.3f}\nThreshold: {min_threshold:.3f}", + ha="right", + va="center", + bbox=dict(facecolor="white", alpha=0.8), + ) + + # Mark 95% TPR threshold + ax1.axvline(tpr95_threshold, color="purple", linestyle=":") + ax1.text( + tpr95_threshold, + 0.3, + f"95% TPR\nFPR: {tpr95_fpr:.3f}\nThreshold: {tpr95_threshold:.3f}", + ha="left", + va="center", + bbox=dict(facecolor="white", alpha=0.8), + ) + + # Combine legends + lines1, labels1 = ax1.get_legend_handles_labels() + lines2, labels2 = ax2.get_legend_handles_labels() + ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper right") + + if save: + save_path = os.path.join( + self.output_dir, + f"{model_name.lower().replace(' ', '_')}_threshold_impact.png", + ) + plt.savefig(save_path, bbox_inches="tight", dpi=300) + logger.info(f"Saved threshold impact plot to {save_path}") + + return fig + + def _calculate_auroc(self, fpr: np.ndarray, tpr: np.ndarray) -> float: + """ + Calculate AUROC from FPR and TPR. + + Args: + fpr: False positive rates + tpr: True positive rates + + Returns: + auroc: Area under ROC curve + """ + # Sort by increasing FPR + idx = np.argsort(fpr) + fpr_sorted = fpr[idx] + tpr_sorted = tpr[idx] + + # Calculate AUROC using trapezoidal rule + auroc = np.trapz(tpr_sorted, fpr_sorted) + + return auroc + + def _calculate_aupr(self, precision: np.ndarray, recall: np.ndarray) -> float: + """ + Calculate AUPR from precision and recall. + + Args: + precision: Precision values + recall: Recall values + + Returns: + aupr: Area under PR curve + """ + # Sort by increasing recall + idx = np.argsort(recall) + recall_sorted = recall[idx] + precision_sorted = precision[idx] + + # Calculate AUPR using trapezoidal rule + aupr = np.trapz(precision_sorted, recall_sorted) + + return aupr diff --git a/grafana-configs/README.md b/grafana-configs/README.md new file mode 100644 index 00000000..97b7275b --- /dev/null +++ b/grafana-configs/README.md @@ -0,0 +1,157 @@ +# Grafana Loki Logging Configuration + +This directory contains the configuration files and components for centralized logging using Grafana and Loki in the Global Classifier project. + +## Overview + +We use **Grafana Loki** for centralized log aggregation and **Grafana** for log visualization and monitoring. This setup provides: + +- **Centralized Logging**: All application logs are sent to Loki for storage and indexing +- **Real-time Monitoring**: Grafana dashboards provide real-time views of logs and metrics +- **Advanced Filtering**: Filter logs by service, log level, model ID, and other labels +- **Alerting**: Monitor error rates and system health +- **API-based Logging**: Direct HTTP API calls to Loki (no file dependencies) + +## Architecture + +``` +┌─────────────────┐ HTTP API ┌──────────┐ Query API ┌─────────┐ +│ Python Services │ ──────────────> │ Loki │ <─────────────── │ Grafana │ +│ (LokiLogger) │ │ :3100 │ │ :4005 │ +└─────────────────┘ └──────────┘ └─────────┘ + │ + ▼ + ┌──────────┐ + │ File │ + │ Storage │ + └──────────┘ +``` + +## Components + +### 1. **LokiLogger Class** (`loki_logger.py`) +- **Purpose**: Python logging class that sends logs directly to Loki API +- **Features**: + - Direct HTTP API calls (no file dependencies) + - Automatic label generation (service, level, hostname, model_id) + - Non-blocking, fire-and-forget logging + - Console output for immediate feedback + - Graceful error handling + +### 2. **Loki Configuration** (`loki-config.yaml`) +- **Purpose**: Loki server configuration +- **Storage**: Filesystem-based with chunks and rules directories +- **Schema**: BoltDB shipper with filesystem object store +- **Port**: 3100 (HTTP), 9096 (gRPC) + +### 3. **Grafana Datasources** (`grafana-datasources.yaml`) +- **Purpose**: Auto-provisioning of Loki as Grafana datasource +- **Connection**: Points to `http://loki:3100` (container network) +- **Default**: Set as the default datasource for new panels + +### 4. **Grafana Dashboards** (`grafana-dashboards.yaml`) +- **Purpose**: Auto-provisioning of dashboards +- **Location**: Dashboards placed in `/etc/grafana/provisioning/dashboards` +- **Folder**: Organized under "Model Deployment" folder + +### 5. **Model Deployment Dashboard** (`grafana-dashboard-deployment.json`) +- **Purpose**: Monitoring dashboard for model deployment processes +- **Features**: + - Time series graph showing log counts by level and service + - Real-time log viewer with filtering + - Template variables for service and log level filtering + - 30-second auto-refresh + +### 6. **Docker Compose** (`docker-compose-logging.yml`) +- **Purpose**: Container orchestration for logging stack +- **Services**: Grafana (port 4005) and Loki (port 3100) +- **Network**: Connected to `bykstack` network +- **Volumes**: Persistent storage for Grafana data and Loki chunks + +## Usage + +### Starting the Logging Stack +```bash +# Start logging services +docker-compose -f grafana-configs/docker-compose-logging.yml up -d + +# Or include in development stack +docker-compose -f docker-compose-dev.yml up -d +``` + +### Using LokiLogger in Python +```python +from grafana_configs.loki_logger import LokiLogger + +# Initialize logger with service name +logger = LokiLogger(service_name="model-deployment-orchestrator") + +# Log with model context +logger.info("Starting deployment", model_id="model123", + current_env="testing", target_env="production") + +# Log errors with extra context +logger.error("Deployment failed", model_id="model123", + error_code=500, step="model_loading") +``` + +### Accessing Grafana +1. **URL**: http://localhost:4005 +2. **Credentials**: admin / admin123 +3. **Dashboard**: Dashboards → Model Deployment → "Model Deployment Orchestrator" + +### Log Filtering Examples +- **Service Filter**: `model-deployment-orchestrator` +- **Log Level Filter**: `ERROR`, `WARNING`, `INFO`, `DEBUG` +- **Model ID Filter**: Available in log content and labels +- **Time Range**: Last 1 hour (default), customizable + +## Integration with CronManager + +The `loki_logger.py` file is mounted into CronManager containers at the same location as the deployment scripts, allowing direct import: + +```python +# In deployment_orchestrator.py +from loki_logger import LokiLogger + +# Initialize with appropriate service name +logger = LokiLogger(service_name="model-deployment-orchestrator") +``` + +## Network Configuration + +All services run on the `bykstack` Docker network: +- **Loki**: `loki:3100` (internal network) +- **Grafana**: `grafana:3000` → `localhost:4005` (external access) +- **Logger**: Uses `http://loki:3100` for container-to-container communication + +## Log Retention + +- **Storage**: Filesystem-based in container volumes +- **Retention**: Configurable in `loki-config.yaml` +- **Backup**: Consider backing up `/loki/chunks` for long-term retention + +## Monitoring Best Practices + +1. **Use Descriptive Service Names**: Help identify log sources +2. **Include Model Context**: Always pass `model_id` when available +3. **Structured Extra Fields**: Use consistent field names across services +4. **Error Context**: Include error codes, step information for debugging +5. **Log Levels**: Use appropriate levels (ERROR for failures, INFO for progress) + +## Troubleshooting + +### Loki Connection Issues +- Check if Loki container is running: `docker ps | grep loki` +- Verify network connectivity: `docker network inspect bykstack` +- Check Loki logs: `docker logs loki` + +### Grafana Dashboard Issues +- Restart Grafana to reload dashboards: `docker restart grafana` +- Check provisioning: `docker logs grafana | grep provisioning` +- Validate JSON: `python -m json.tool grafana-dashboard-deployment.json` + +### No Logs Appearing +- Verify LokiLogger is using correct URL (`http://loki:3100`) +- Check if services are on the same Docker network +- Test Loki API directly: `curl http://localhost:3100/ready` diff --git a/grafana-configs/docker-compose-logging.yml b/grafana-configs/docker-compose-logging.yml new file mode 100644 index 00000000..2c58c29d --- /dev/null +++ b/grafana-configs/docker-compose-logging.yml @@ -0,0 +1,45 @@ +services: + # Loki - Log aggregation system + loki: + image: grafana/loki:2.9.0 + container_name: loki + ports: + - "3100:3100" + command: -config.file=/etc/loki/local-config.yaml + volumes: + - ./loki-config.yaml:/etc/loki/local-config.yaml + - loki-data:/loki + networks: + - bykstack + restart: unless-stopped + + # Grafana - Visualization and dashboards + grafana: + image: grafana/grafana:10.0.0 + container_name: grafana + ports: + - "4005:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin123 + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - grafana-data:/var/lib/grafana + - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + - ./grafana-dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml + - ./grafana-dashboard-deployment.json:/etc/grafana/dashboards/deployment.json + networks: + - bykstack + depends_on: + - loki + restart: unless-stopped + +volumes: + loki-data: + driver: local + grafana-data: + driver: local + +networks: + bykstack: + external: true diff --git a/grafana-configs/grafana-dashboard-deployment.json b/grafana-configs/grafana-dashboard-deployment.json new file mode 100644 index 00000000..8aa53461 --- /dev/null +++ b/grafana-configs/grafana-dashboard-deployment.json @@ -0,0 +1,167 @@ +{ + "id": null, + "title": "Model Deployment Orchestrator", + "tags": ["deployment", "models", "triton"], + "timezone": "browser", + "refresh": "30s", + "time": { + "from": "now-1h", + "to": "now" + }, + "templating": { + "list": [ + { + "name": "service_name", + "type": "query", + "label": "Service Name", + "refresh": 1, + "query": "label_values(service)", + "datasource": { + "type": "loki", + "uid": "loki-datasource" + }, + "multi": true, + "includeAll": true, + "allValue": ".*", + "current": { + "selected": true, + "text": "All", + "value": "$__all" + }, + "options": [], + "regex": "", + "sort": 0, + "skipUrlSync": false, + "hide": 0 + }, + { + "name": "log_level", + "type": "custom", + "label": "Log Level", + "multi": true, + "includeAll": true, + "allValue": "ERROR|INFO|WARNING|DEBUG", + "current": { + "selected": true, + "text": "All", + "value": "$__all" + }, + "options": [ + { + "text": "All", + "value": "$__all", + "selected": true + }, + { + "text": "ERROR", + "value": "ERROR", + "selected": false + }, + { + "text": "WARNING", + "value": "WARNING", + "selected": false + }, + { + "text": "INFO", + "value": "INFO", + "selected": false + }, + { + "text": "DEBUG", + "value": "DEBUG", + "selected": false + } + ], + "query": "ERROR,INFO,WARNING,DEBUG", + "queryType": "", + "refresh": 0, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "hide": 0 + } + ] + }, + "panels": [ + { + "id": 1, + "title": "Log Messages Over Time by Level", + "type": "graph", + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "targets": [ + { + "expr": "sum by (service, level) (count_over_time({service=~\"$service_name\", level=~\"$log_level\"}[5m]))", + "refId": "A", + "legendFormat": "{{service}} - {{level}}", + "datasource": { + "type": "loki", + "uid": "loki-datasource" + } + } + ], + "yAxes": [ + { + "label": "Log Count", + "min": 0 + } + ], + "xAxis": { + "show": true + }, + "legend": { + "show": true, + "values": true, + "current": true, + "total": true + }, + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "fill": 1, + "linewidth": 2, + "pointradius": 2, + "bars": false, + "lines": true, + "points": false, + "stack": false, + "percentage": false, + "nullPointMode": "null as zero" + }, + { + "id": 2, + "title": "Deployment Logs", + "type": "logs", + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 8 + }, + "targets": [ + { + "expr": "{service=~\"$service_name\", level=~\"$log_level\"}", + "refId": "A", + "datasource": { + "type": "loki", + "uid": "loki-datasource" + } + } + ], + "options": { + "showTime": true, + "showLabels": true, + "showCommonLabels": false, + "wrapLogMessage": true, + "sortOrder": "Descending" + } + } + ] +} diff --git a/grafana-configs/grafana-dashboards.yaml b/grafana-configs/grafana-dashboards.yaml new file mode 100644 index 00000000..bb047595 --- /dev/null +++ b/grafana-configs/grafana-dashboards.yaml @@ -0,0 +1,8 @@ +apiVersion: 1 + +providers: + - name: 'deployment-dashboards' + type: file + folder: 'Model Deployment' + options: + path: /etc/grafana/dashboards diff --git a/grafana-configs/grafana-datasources.yaml b/grafana-configs/grafana-datasources.yaml new file mode 100644 index 00000000..7f4eceba --- /dev/null +++ b/grafana-configs/grafana-datasources.yaml @@ -0,0 +1,10 @@ +apiVersion: 1 + +datasources: + - name: Loki + type: loki + uid: loki-datasource + access: proxy + url: http://loki:3100 + isDefault: true + editable: true diff --git a/grafana-configs/loki-config.yaml b/grafana-configs/loki-config.yaml new file mode 100644 index 00000000..5129c81b --- /dev/null +++ b/grafana-configs/loki-config.yaml @@ -0,0 +1,50 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + grpc_listen_port: 9096 + +common: + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory + +query_range: + results_cache: + cache: + embedded_cache: + enabled: true + max_size_mb: 100 + +schema_config: + configs: + - from: 2020-10-24 + store: boltdb-shipper + object_store: filesystem + schema: v11 + index: + prefix: index_ + period: 24h + +ruler: + alertmanager_url: http://localhost:9093 + +# By default, Loki will send anonymous, but uniquely-identifiable usage and configuration +# analytics to Grafana Labs. These statistics are sent to https://stats.grafana.org/ +# +# Statistics help us better understand how Loki is used, and they show us performance +# levels for most users. This helps us prioritize features and documentation. +# For more information on what's sent, look at +# https://github.com/grafana/loki/blob/main/pkg/usagestats/stats.go +# Refer to the buildReport method to see what goes into a report. +# +# If you would like to disable reporting, uncomment the following lines: +analytics: + reporting_enabled: false diff --git a/grafana-configs/loki_logger.py b/grafana-configs/loki_logger.py new file mode 100644 index 00000000..3cac164b --- /dev/null +++ b/grafana-configs/loki_logger.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Loki Logger for Global Classifier +Sends logs directly to Loki API for centralized logging +""" + +import json +import socket +import time +from datetime import datetime + +import requests + + +class LokiLogger: + """Simple logger that sends logs directly to Loki API""" + + def __init__(self, loki_url: str = "http://loki:3100", service_name: str = "default"): + """ + Initialize LokiLogger + + Args: + loki_url: URL for Loki service (default: container URL in bykstack network) + service_name: Name of the service for labeling logs + """ + self.loki_url = loki_url + self.service_name = service_name + self.hostname = socket.gethostname() + self.session = requests.Session() + # Set default timeout for all requests + self.timeout = 5 + + def _send_to_loki(self, level: str, message: str, **extra_fields): + """Send log entry directly to Loki API""" + try: + # Create timestamp in nanoseconds (Loki requirement) + timestamp_ns = str(int(time.time() * 1_000_000_000)) + + # Prepare labels for Loki + labels = { + "service": self.service_name, + "level": level, + "hostname": self.hostname, + } + + # Add extra fields as labels, filtering out None values except for model_id + for key, value in extra_fields.items(): + if key == "model_id": + # Always include model_id, default to "None" if not provided + labels[key] = str(value) if value is not None else "None" + elif value is not None: + labels[key] = str(value) + + # Create log entry + log_entry = { + "timestamp": datetime.now().isoformat(), + "level": level, + "message": message, + "hostname": self.hostname, + "service": self.service_name, + **extra_fields + } + + # Prepare Loki payload + payload = { + "streams": [ + { + "stream": labels, + "values": [ + [timestamp_ns, json.dumps(log_entry)] + ] + } + ] + } + + # Send to Loki (non-blocking, fire-and-forget) + self.session.post( + f"{self.loki_url}/loki/api/v1/push", + json=payload, + headers={"Content-Type": "application/json"}, + timeout=self.timeout + ) + + except Exception: + # Silently ignore logging errors to not affect main application + pass + + # Also print to console for immediate feedback + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + model_info = f" [Model: {extra_fields.get('model_id', 'N/A')}]" if extra_fields.get('model_id') else "" + print(f"[{timestamp}] {level: <8}{model_info} | {message}") + + def info(self, message: str, model_id: str | None = None, **extra_fields): + if model_id: + extra_fields["model_id"] = model_id + self._send_to_loki("INFO", message, **extra_fields) + + def error(self, message: str, model_id: str | None = None, **extra_fields): + if model_id: + extra_fields["model_id"] = model_id + self._send_to_loki("ERROR", message, **extra_fields) + + def warning(self, message: str, model_id: str | None = None, **extra_fields): + if model_id: + extra_fields["model_id"] = model_id + self._send_to_loki("WARNING", message, **extra_fields) + + def debug(self, message: str, model_id: str | None = None, **extra_fields): + if model_id: + extra_fields["model_id"] = model_id + self._send_to_loki("DEBUG", message, **extra_fields) diff --git a/migrate.sh b/migrate.sh old mode 100644 new mode 100755 index 1e1b715e..e6927aa3 --- a/migrate.sh +++ b/migrate.sh @@ -12,4 +12,4 @@ INI_FILE="constants.ini" DB_PASSWORD=$(get_ini_value "$INI_FILE" "DB_PASSWORD") -docker run --rm --network bykstack-gc -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/global-classifier?user=postgres --password=dbadmin update \ No newline at end of file +docker run --rm --network bykstack -v `pwd`/DSL/Liquibase/changelog:/liquibase/changelog -v `pwd`/DSL/Liquibase/master.yml:/liquibase/master.yml -v `pwd`/DSL/Liquibase/data:/liquibase/data liquibase/liquibase --defaultsFile=/liquibase/changelog/liquibase.properties --changelog-file=master.yml --url=jdbc:postgresql://users_db:5432/global-classifier?user=postgres --password=dbadmin update diff --git a/notification-server/.env b/notification-server/.env new file mode 100644 index 00000000..1cf1a9bc --- /dev/null +++ b/notification-server/.env @@ -0,0 +1,9 @@ +OPENSEARCH_PROTOCOL=http +OPENSEARCH_HOST=opensearch-node +OPENSEARCH_PORT=9200 +OPENSEARCH_USERNAME=admin +OPENSEARCH_PASSWORD=admin +PORT=4040 +REFRESH_INTERVAL=1000 +CORS_WHITELIST_ORIGINS=http://localhost:3001,http://localhost:3002,http://localhost:3003,http://localhost:3004,http://localhost:8080,http://localhost:8088 +RUUTER_URL=http://localhost:8086 diff --git a/notification-server/Dockerfile b/notification-server/Dockerfile new file mode 100644 index 00000000..6769c0ce --- /dev/null +++ b/notification-server/Dockerfile @@ -0,0 +1,13 @@ +FROM node:22.0.0-alpine + +WORKDIR /app + +COPY package.json package-lock.json /app/ + +RUN npm install + +COPY . /app/ + +EXPOSE 4040 + +CMD ["npm", "run", "dev"] diff --git a/notification-server/index.js b/notification-server/index.js new file mode 100644 index 00000000..2952b006 --- /dev/null +++ b/notification-server/index.js @@ -0,0 +1,16 @@ +require('dotenv').config(); +const { client } = require('./src/openSearch'); + +(async () => { + try { + await client.indices.putSettings({ + index: 'dataset_progress_sessions', + body: { + refresh_interval: '5s', + }, + }); + require('./src/server'); + } catch (error) { + console.error('Error:', error); + } +})(); diff --git a/notification-server/package-lock.json b/notification-server/package-lock.json new file mode 100644 index 00000000..21905e70 --- /dev/null +++ b/notification-server/package-lock.json @@ -0,0 +1,1354 @@ +{ + "name": "notification-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "notification-service", + "version": "1.0.0", + "dependencies": { + "@opensearch-project/opensearch": "^2.4.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "csurf": "^1.11.0", + "dotenv": "^16.3.1", + "express": "^4.19.2", + "helmet": "^7.1.0", + "uuid": "^9.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "node_modules/@opensearch-project/opensearch": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.4.0.tgz", + "integrity": "sha512-r0ZNIlDxAua1ZecOBJ8qOXshf2ZQhNKmfly7o0aNuACf0pDa6Et/8mWMZuaFOu7xlNEeRNB7IjDQUYFy2SPElw==", + "dependencies": { + "aws4": "^1.11.0", + "debug": "^4.3.1", + "hpagent": "^1.2.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0" + }, + "engines": { + "node": ">=10", + "yarn": "^1.22.10" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csurf": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", + "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", + "deprecated": "Please use another csrf package", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/csurf/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/csurf/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz", + "integrity": "sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } +} diff --git a/notification-server/package.json b/notification-server/package.json new file mode 100644 index 00000000..4dc73421 --- /dev/null +++ b/notification-server/package.json @@ -0,0 +1,21 @@ +{ + "name": "notification-service", + "version": "1.0.0", + "scripts": { + "start": "node ./src/server.js", + "dev": "nodemon ./src/server.js" + }, + "dependencies": { + "@opensearch-project/opensearch": "^2.4.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "csurf": "^1.11.0", + "dotenv": "^16.3.1", + "express": "^4.19.2", + "helmet": "^7.1.0", + "uuid": "^9.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/notification-server/src/addOns.js b/notification-server/src/addOns.js new file mode 100644 index 00000000..42c212b2 --- /dev/null +++ b/notification-server/src/addOns.js @@ -0,0 +1,43 @@ +const { searchDatasetGroupNotification, searchModelNotification } = require("./openSearch"); +const { serverConfig } = require("./config"); + +function buildDatasetGroupNotificationSearchInterval({ + sessionId, + interval = serverConfig.refreshInterval, +}) { + return ({ connectionId, sender }) => { + const intervalHandle = setInterval( + () => + searchDatasetGroupNotification({ + connectionId, + sessionId, + sender, + }), + interval + ); + return () => clearInterval(intervalHandle); + }; +} + +function buildModelNotificationSearchInterval({ + sessionId, + interval = serverConfig.refreshInterval, +}) { + return ({ connectionId, sender }) => { + const intervalHandle = setInterval( + () => + searchModelNotification({ + connectionId, + sessionId, + sender, + }), + interval + ); + return () => clearInterval(intervalHandle); + }; +} + +module.exports = { + buildDatasetGroupNotificationSearchInterval, + buildModelNotificationSearchInterval, +}; diff --git a/notification-server/src/config.js b/notification-server/src/config.js new file mode 100644 index 00000000..340a14c0 --- /dev/null +++ b/notification-server/src/config.js @@ -0,0 +1,25 @@ +require("dotenv").config(); + +module.exports = { + openSearchConfig: { + datasetGroupProgress: "dataset_progress_sessions", + dataModelProgress: "data_model_progress_sessions", + ssl: { + rejectUnauthorized: false, + }, + getUrl: () => { + const protocol = process.env.OPENSEARCH_PROTOCOL || "https"; + const username = process.env.OPENSEARCH_USERNAME || "admin"; + const password = process.env.OPENSEARCH_PASSWORD || "admin"; + const host = process.env.OPENSEARCH_HOST || "host.docker.internal"; + const port = process.env.OPENSEARCH_PORT || "9200"; + + return `${protocol}://${username}:${password}@${host}:${port}`; + }, + retry_on_conflict: 6, + }, + serverConfig: { + port: process.env.PORT || 4040, + refreshInterval: process.env.REFRESH_INTERVAL || 1000, + }, +}; diff --git a/notification-server/src/openSearch.js b/notification-server/src/openSearch.js new file mode 100644 index 00000000..25a716d1 --- /dev/null +++ b/notification-server/src/openSearch.js @@ -0,0 +1,140 @@ +const { Client } = require("@opensearch-project/opensearch"); +const { openSearchConfig } = require("./config"); + +const client = new Client({ + node: openSearchConfig.getUrl(), + ssl: openSearchConfig.ssl, +}); + +async function searchDatasetGroupNotification({ + sessionId, + connectionId, + sender, +}) { + try { + const response = await client.search({ + index: openSearchConfig.datasetGroupProgress, + body: { + query: { + bool: { + must: { match: { sessionId } }, + }, + }, + sort: { timestamp: { order: "desc" } }, + size: 1, + }, + }); + for (const hit of response.body.hits.hits) { + if (!hit._source.sentTo?.includes(connectionId)) { + console.log(`hit: ${JSON.stringify(hit)}`); + const sessionJson = { + sessionId: hit._source.sessionId, + progressPercentage: hit._source.progressPercentage, + generationStatus: hit._source.generationStatus, + generationMessage: hit._source.generationMessage, + }; + await sender(sessionJson); + await markAsSent(hit, connectionId); + } + } + } catch (e) { + console.error(e); + await sender({}); + } +} + +async function searchModelNotification({ sessionId, connectionId, sender }) { + try { + const response = await client.search({ + index: openSearchConfig.dataModelProgress, + body: { + query: { + bool: { + must: { match: { sessionId } }, + }, + }, + sort: { timestamp: { order: "desc" } }, + size: 1, + }, + }); + + for (const hit of response.body.hits.hits) { + if (!hit._source.sentTo?.includes(connectionId)) { + console.log(`hit: ${JSON.stringify(hit)}`); + const sessionJson = { + sessionId: hit._source.sessionId, + progressPercentage: hit._source.progressPercentage, + trainingStatus: hit._source.trainingStatus, + trainingMessage: hit._source.trainingMessage, + }; + await sender(sessionJson); + await markAsSent(hit, connectionId); + } + } + } catch (e) { + console.error(e); + await sender({}); + } +} + +async function markAsSent({ _index, _id }, connectionId) { + await client.update({ + index: _index, + id: _id, + retry_on_conflict: openSearchConfig.retry_on_conflict, + body: { + script: { + source: `if (ctx._source.sentTo == null) { + ctx._source.sentTo = [params.connectionId]; + } else { + ctx._source.sentTo.add(params.connectionId); + }`, + lang: "painless", + params: { connectionId }, + }, + }, + }); +} + +async function updateDatasetGroupProgress( + sessionId, + progressPercentage, + generationStatus, + generationMessage +) { + await client.index({ + index: "dataset_progress_sessions", + body: { + sessionId, + generationStatus, + progressPercentage, + generationMessage, + timestamp: Date.now(), + }, + }); +} + +async function updateModelProgress( + sessionId, + progressPercentage, + trainingStatus, + trainingMessage +) { + await client.index({ + index: "data_model_progress_sessions", + body: { + sessionId, + trainingStatus, + progressPercentage, + trainingMessage, + timestamp: Date.now(), + }, + }); +} + +module.exports = { + searchDatasetGroupNotification, + searchModelNotification, + updateDatasetGroupProgress, + updateModelProgress, +}; \ No newline at end of file diff --git a/notification-server/src/server.js b/notification-server/src/server.js new file mode 100644 index 00000000..edd9634b --- /dev/null +++ b/notification-server/src/server.js @@ -0,0 +1,97 @@ +const express = require("express"); +const cors = require("cors"); +const { buildSSEResponse } = require("./sseUtil"); +const { serverConfig } = require("./config"); +const { + buildDatasetGroupNotificationSearchInterval, + buildModelNotificationSearchInterval, +} = require("./addOns"); +const { updateDatasetGroupProgress, updateModelProgress } = require("./openSearch"); +const helmet = require("helmet"); +const cookieParser = require("cookie-parser"); +const csurf = require("csurf"); + +const app = express(); + +app.use(cors()); +app.use(helmet.hidePoweredBy()); +app.use(express.json({ extended: false })); +app.use(cookieParser()); +app.use(csurf({ cookie: true })); + +app.get("/sse/dataset/notifications/:sessionId", (req, res) => { + const { sessionId } = req.params; + console.log(`session id: ${sessionId}`); + buildSSEResponse({ + req, + res, + buildCallbackFunction: buildDatasetGroupNotificationSearchInterval({ sessionId }), + }); +}); + +app.get("/sse/model/notifications/:sessionId", (req, res) => { + const { sessionId } = req.params; + console.log(`session id: ${sessionId}`); + buildSSEResponse({ + req, + res, + buildCallbackFunction: buildModelNotificationSearchInterval({ sessionId }), + }); +}); + +app.get("/csrf-token", (req, res) => { + console.log(`Cookies: ${JSON.stringify(req.cookies)}`); + res.json({ csrfToken: req.csrfToken() }); +}); + +// Endpoint to update the dataset_progress_sessions index +app.post("/dataset/progress", async (req, res) => { + const { sessionId, progressPercentage, generationStatus, generationMessage } = + req.body; + + if (!sessionId || progressPercentage === undefined || !generationStatus) { + return res.status(400).json({ error: "Missing required fields" }); + } + + try { + await updateDatasetGroupProgress( + sessionId, + progressPercentage, + generationStatus, + generationMessage + ); + res.status(201).json({ message: "Document created successfully" }); + } catch (error) { + console.error("Error creating document:", error); + res.status(500).json({ error: "Failed to create document" }); + } +}); + +// Endpoint to update the dataset_progress_sessions index +app.post("/model/progress", async (req, res) => { + const { sessionId, progressPercentage, trainingStatus, trainingMessage } = + req.body; + + if (!sessionId || progressPercentage === undefined || !trainingStatus) { + return res.status(400).json({ error: "Missing required fields" }); + } + + try { + await updateModelProgress( + sessionId, + progressPercentage, + trainingStatus, + trainingMessage + ); + res.status(201).json({ message: "Document created successfully" }); + } catch (error) { + console.error("Error creating document:", error); + res.status(500).json({ error: "Failed to create document" }); + } +}); + +const server = app.listen(serverConfig.port, () => { + console.log(`Server running on port ${serverConfig.port}`); +}); + +module.exports = server; diff --git a/notification-server/src/sseUtil.js b/notification-server/src/sseUtil.js new file mode 100644 index 00000000..1e698e48 --- /dev/null +++ b/notification-server/src/sseUtil.js @@ -0,0 +1,54 @@ +const { v4: uuidv4 } = require("uuid"); + +function buildSSEResponse({ res, req, buildCallbackFunction }) { + addSSEHeader(req, res); + keepStreamAlive(res); + const connectionId = generateConnectionID(); + const sender = buildSender(res); + + const cleanUp = buildCallbackFunction({ connectionId, sender }); + + req.on("close", () => { + console.log("Client disconnected from SSE"); + cleanUp?.(); + }); +} + +function addSSEHeader(req, res) { + const origin = extractOrigin(req.headers.origin); + + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": true, + "Access-Control-Expose-Headers": + "Origin, X-Requested-With, Content-Type, Cache-Control, Connection, Accept", + }); +} + +function extractOrigin(reqOrigin) { +const corsWhitelist = process.env.CORS_WHITELIST_ORIGINS + ? process.env.CORS_WHITELIST_ORIGINS.split(",") + : []; const whitelisted = corsWhitelist.indexOf(reqOrigin) !== -1; + return whitelisted ? reqOrigin : "*"; +} + +function keepStreamAlive(res) { + res.write(""); +} + +function generateConnectionID() { + const connectionId = uuidv4(); + console.log(`New client connected with connectionId: ${connectionId}`); + return connectionId; +} + +function buildSender(res) { + return (data) => res.write(`data: ${JSON.stringify(data)}\n\n`); +} + +module.exports = { + buildSSEResponse, +}; diff --git a/pyproject.toml b/pyproject.toml index 64349163..21007e0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,11 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [ + "litserve>=0.2.13", + "loguru>=0.7.2", "numpy>=2.2.6", + "pytest>=8.4.1", + "requests>=2.32.0", "ruff>=0.11.11", "torch>=2.7.0", ] diff --git a/.env b/sidecar.env similarity index 70% rename from .env rename to sidecar.env index 4ee43bfe..95b34ce4 100644 --- a/.env +++ b/sidecar.env @@ -9,4 +9,7 @@ MLFLOW_BACKEND_STORE_URI=sqlite:////mlflow/mlflow_data/mlflow.db MLFLOW_DEFAULT_ARTIFACT_ROOT=file:///mlflow/mlflow_artifacts MLFLOW_HOST_CONFIG_PATH=./mlflow/config MLFLOW_CONT_CONFIG_PATH=/mlflow/config -MLFLOW_FLASK_SERVER_SECRET_KEY=value \ No newline at end of file +MLFLOW_FLASK_SERVER_SECRET_KEY=byk-mlflow-secret +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin +MINIO_BROWSER_REDIRECT_URL=http://localhost:9001 \ No newline at end of file diff --git a/src/dataset-generation/data_source_ingestion.py b/src/dataset-generation/data_source_ingestion.py deleted file mode 100644 index 68282b22..00000000 --- a/src/dataset-generation/data_source_ingestion.py +++ /dev/null @@ -1,266 +0,0 @@ -import os -import json -import sys -from loguru import logger -import datetime -from typing import Dict, List, Any, Optional - - -# remove the default stderr handler -logger.remove() -# add stout handler -logger.add(sys.stdout, format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}") - - -class AgencyDataParser: - """ - Parser for chunked agency data from JSON files. - Processes each JSON file and outputs a corresponding text file. - """ - - def __init__(self, input_dir: str, output_dir: str): - """ - Initialize the parser with input and output directories. - - Args: - input_dir: Directory containing JSON files with chunked agency data - output_dir: Directory where the output text files will be saved - """ - self.input_dir = input_dir - self.output_dir = output_dir - - if not os.path.exists(output_dir): - os.makedirs(output_dir) - logger.info(f"Created output directory: {output_dir}") - - def _parse_json_file(self, file_path: str) -> Optional[List[Dict[str, Any]]]: - """ - Parse a JSON file and return its contents. - - Args: - file_path: Path to the JSON file - - Returns: - Parsed JSON data or None if an error occurs - """ - try: - with open(file_path, "r", encoding="utf-8") as file: - data = json.load(file) - logger.info(f"Successfully parsed JSON file: {file_path}") - return data - except FileNotFoundError: - logger.error(f"File not found: {file_path}") - except json.JSONDecodeError: - logger.error(f"Invalid JSON format in file: {file_path}") - except Exception as e: - logger.error(f"Error parsing file {file_path}: {str(e)}") - - return None - - def _clean_content(self, content: str) -> str: - """ - Clean the content by: - 1. Removing lines that have only one character - 2. Replacing multiple consecutive empty lines with a single empty line - - Args: - content: The raw content string - - Returns: - Cleaned content string - """ - # Split content into lines - lines = content.split("\n") - - # Filter out lines with only one character - filtered_lines = [ - line for line in lines if len(line.strip()) > 1 or not line.strip() - ] - - joined_content = "\n".join(filtered_lines) - - # Replace multiple consecutive empty lines with a single empty line - import re - - cleaned_content = re.sub(r"\n\s*\n\s*\n+", "\n\n", joined_content) - - return cleaned_content.strip() - - def _extract_agency_info( - self, data: List[Dict[str, Any]], file_name: str - ) -> Dict[str, Any]: - """ - Extract and structure agency information from parsed data into a standardized - internal representation. - - Note: This internal structure should be replaced with the official structure - defined in Task 1 when it becomes available. - - Args: - data: Parsed JSON data containing agency information chunks - file_name: Name of the source file for reference - - Returns: - Standardized internal representation of agency information - """ - # Define the standardized internal representation - # This is a placeholder until the structure from Task 1 is available - agency_data = { - "title": "", # Agency/document title - "content": "", # Full concatenated content - "source_url": "", # Original source URL - "source_file": file_name, # Source file name - "metadata": { - "description": "", # Document description if available - "chunk_count": 0, # Number of chunks processed - "processed_date": datetime.datetime.now().isoformat(), - }, - } - - # Extract title, source_url, and description from the first chunk - if data and len(data) > 0 and "content" in data[0]: - first_chunk = data[0]["content"] - agency_data["title"] = first_chunk.get("title", "") - agency_data["source_url"] = first_chunk.get("source_url", "") - agency_data["metadata"]["description"] = first_chunk.get("description", "") - - # Concatenate all content chunks - content_text = "" - chunk_count = 0 - - for item in data: - if "content" in item and "chunk" in item["content"]: - content_text += item["content"]["chunk"] + "\n" - chunk_count += 1 - - # Clean the content - cleaned_content = self._clean_content(content_text) - - agency_data["content"] = cleaned_content - agency_data["metadata"]["chunk_count"] = chunk_count - - logger.debug( - f"Created standardized internal representation with {chunk_count} chunks for {file_name}" - ) - return agency_data - - def process_file(self, file_name: str) -> Optional[Dict[str, Any]]: - """ - Process a single JSON file - - Args: - file_name: Name of the JSON file - - Returns: - Processed agency data or None if an error occurs - """ - file_path = os.path.join(self.input_dir, file_name) - - data = self._parse_json_file(file_path) - if not data: - return None - - try: - agency_data = self._extract_agency_info(data, file_name) - logger.info(f"Successfully extracted agency information from {file_name}") - return agency_data - except Exception as e: - logger.error(f"Error extracting agency info from {file_name}: {str(e)}") - return None - - def save_to_text_file(self, agency_data: Dict[str, Any], file_name: str) -> bool: - """ - Save agency data to an individual text file. - - Args: - agency_data: Structured agency data (in standardized internal representation) - file_name: Name of the JSON file (used to derive output file name) - - Returns: - True if successful, False otherwise - """ - try: - # Generate output file name (replacing extension) - output_file_name = os.path.splitext(file_name)[0] + ".txt" - output_path = os.path.join(self.output_dir, output_file_name) - - with open(output_path, "w", encoding="utf-8") as file: - # Write title - file.write(f"# {agency_data['title']}\n\n") - - # Write content - file.write(agency_data["content"]) - - logger.info(f"Successfully saved agency data to {output_path}") - return True - except Exception as e: - logger.error(f"Error saving agency data: {str(e)}") - return False - - def process_directory(self) -> int: - """ - Process all JSON files in the input directory and create individual text files. - - Returns: - Number of successfully processed files - """ - success_count = 0 - - try: - # Get all JSON files in the input directory - json_files = [ - f - for f in os.listdir(self.input_dir) - if f.endswith(".json") or f.endswith(".txt") - ] - - if not json_files: - logger.warning(f"No JSON files found in {self.input_dir}") - return 0 - - logger.info(f"Found {len(json_files)} files to process") - - # Process each file and create an individual output file - for file_name in json_files: - logger.info(f"Processing file: {file_name}") - - # Get agency data - agency_data = self.process_file(file_name) - if not agency_data: - continue - - # Save to individual text file - if self.save_to_text_file(agency_data, file_name): - success_count += 1 - - logger.info( - f"Successfully processed {success_count} out of {len(json_files)} files" - ) - - except Exception as e: - logger.error(f"Error processing directory {self.input_dir}: {str(e)}") - - return success_count - - -def main(): - # Define the data directories, input_dir is where the agencies JSON files are located and output_dir is where the output files will be saved - input_dir = ( - "data/Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet" # Directory with JSON files - ) - output_dir = "data/output_Tarbijakaitse_ja_Tehnilise_Jarelevalve_Amet" # Directory for output text files - # input_dir = "data/ID.ee" - # output_dir = "data/output_ID.ee" - # input_dir = "data/Politsei-_ja_Piirivalveamet" - # output_dir = "data/output_Politsei-_ja_Piirivalveamet" - parser = AgencyDataParser(input_dir, output_dir) - - processed_count = parser.process_directory() - - logger.info(f"Processed {processed_count} agency files.") - logger.info(f"Output files saved to {output_dir}") - logger.info("Check logs for details.") - - -if __name__ == "__main__": - main() diff --git a/src/dataset_file_handler/Dockerfile b/src/dataset_file_handler/Dockerfile new file mode 100644 index 00000000..5773ab35 --- /dev/null +++ b/src/dataset_file_handler/Dockerfile @@ -0,0 +1,29 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy source code +COPY . /app/src + +# Set Python path +ENV PYTHONPATH=/app/src + + + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Run the FastAPI application +CMD ["uvicorn", "chunks_handler_api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file diff --git a/src/dataset_file_handler/__init__.py b/src/dataset_file_handler/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/dataset_file_handler/chunks_handler_api.py b/src/dataset_file_handler/chunks_handler_api.py new file mode 100644 index 00000000..387a20e1 --- /dev/null +++ b/src/dataset_file_handler/chunks_handler_api.py @@ -0,0 +1,103 @@ +"""FastAPI endpoints for chunk download operations.""" + +from fastapi import FastAPI, HTTPException +from loguru import logger + +from models.schemas import ( + ChunkDownloadRequest, + ChunkDownloadResponse, + MultiChunkDownloadRequest, + MultiChunkDownloadResponse, +) +from single_chunk_handler import ChunkService +from multiple_chunk_handler import MultiChunkService + +# Create FastAPI app +app = FastAPI( + title="Dataset File Handler API", + description="API for handling dataset file operations including chunk downloads", + version="1.0.0", +) + +# Initialize services +chunk_service = ChunkService() +multi_chunk_service = MultiChunkService() + + +@app.post("/download-chunk", response_model=ChunkDownloadResponse) +async def download_chunk(request: ChunkDownloadRequest): + """ + Download a single chunk from S3. + + Args: + request: Chunk download request containing dataset_id and page_num + + Returns: + Chunk data or error information + """ + try: + logger.info( + f"Chunk download request - Dataset: {request.datasetId}, Page: {request.pageNum}" + ) + + result = chunk_service.download_chunk_from_s3( + request.datasetId, request.pageNum + ) + + if not result["success"]: + raise HTTPException(status_code=400, detail=result) + + return ChunkDownloadResponse(**result) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Internal error during chunk download: {str(e)}") + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + + +@app.post("/download-multiple-chunks", response_model=MultiChunkDownloadResponse) +async def download_multiple_chunks(request: MultiChunkDownloadRequest): + """ + Download and aggregate multiple chunks from S3. + + Args: + request: Multi-chunk download request containing dataset_id and chunk_ids + + Returns: + Aggregated chunk data or error information + """ + try: + logger.info( + f"Multi-chunk download request - Dataset: {request.datasetId}, Chunks: {request.chunkIds}" + ) + + if not request.chunkIds: + raise HTTPException(status_code=400, detail="No chunk IDs provided") + + result = multi_chunk_service.download_multiple_chunks( + request.datasetId, request.chunkIds + ) + + if not result["success"]: + raise HTTPException(status_code=400, detail=result) + + return MultiChunkDownloadResponse(**result) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Internal error during multi-chunk download: {str(e)}") + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + + +@app.get("/health") +async def chunk_health_check(): + """Health check endpoint for chunk services.""" + return {"status": "healthy", "service": "chunk-download"} + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/src/dataset_file_handler/config/__init__.py b/src/dataset_file_handler/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/dataset_file_handler/config/settings.py b/src/dataset_file_handler/config/settings.py new file mode 100644 index 00000000..9ccc2b67 --- /dev/null +++ b/src/dataset_file_handler/config/settings.py @@ -0,0 +1,27 @@ +"""Application configuration settings.""" + +import os + + +class Settings: + """Application settings and configuration.""" + + # API Configuration + API_TITLE = "S3 Dataset Processor API" + API_DESCRIPTION = "API for decoding and processing S3 presigned URLs" + API_VERSION = "1.0.0" + + # Directory Configuration + DATA_DIR = "/app/data" + + # Download Configuration + DOWNLOAD_TIMEOUT = 300 # 5 minutes + CHUNK_SIZE = 8192 + + def __init__(self): + """Initialize settings and create necessary directories.""" + os.makedirs(self.DATA_DIR, exist_ok=True) + + +# Global settings instance +settings = Settings() diff --git a/src/dataset_file_handler/download_source_dataset.py b/src/dataset_file_handler/download_source_dataset.py new file mode 100644 index 00000000..bc061457 --- /dev/null +++ b/src/dataset_file_handler/download_source_dataset.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# filepath: c:\Users\charith.bimsara_root\Rootcode\Estonian-Gov-AI\New\Global-Classifier\src\s3_dataset_processor\download_source_dataset.py +""" +Direct Python script for downloading datasets from S3 signed URLs. +Replaces the FastAPI /download-datasets endpoint for CronManager execution. +""" + +import sys +import json +import argparse +import logging +from pathlib import Path +import traceback + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[logging.StreamHandler(sys.stdout)], +) +logger = logging.getLogger(__name__) + +# Add the s3_dataset_processor to Python path to import modules FIRST +# This path corresponds to the volume mount in docker-compose.yml +script_dir = Path("/app/src/s3_dataset_processor") +sys.path.insert(0, str(script_dir)) + +# Now import the services AFTER adding to path +try: + from services.url_decoder_service import URLDecoderService + from services.download_service import DownloadService + from services.extraction_service import ExtractionService + from handlers.response_handler import ResponseHandler + + logger.info("✅ Successfully imported all required modules") +except ImportError as e: + logger.error(f"❌ Failed to import required modules: {e}") + logger.error(f"Python path: {sys.path}") + logger.error(f"Script directory exists: {script_dir.exists()}") + if script_dir.exists(): + logger.error(f"Contents of script directory: {list(script_dir.iterdir())}") + sys.exit(1) + + +def main(): + """Main function to handle dataset download process.""" + parser = argparse.ArgumentParser( + description="Download datasets from S3 signed URLs" + ) + parser.add_argument( + "--encoded-data", required=True, help="Base64 encoded signed URLs data" + ) + parser.add_argument( + "--extract-files", + action="store_true", + default=True, + help="Extract downloaded files", + ) + parser.add_argument("--output-json", help="Output file path for results JSON") + + args = parser.parse_args() + + try: + if not args.encoded_data or not isinstance(args.encoded_data, str): + logger.error("'encoded_data' must be a non-empty string") + sys.exit(1) + + logger.info("Initializing services...") + # Initialize services + url_decoder_service = URLDecoderService() + download_service = DownloadService() + extraction_service = ExtractionService() + response_handler = ResponseHandler() + + logger.info("Decoding signed URLs...") + # Decode the data using service + decoded_data = url_decoder_service.decode_signed_urls(args.encoded_data) + logger.info(f"Starting download for {len(decoded_data)} files") + + logger.info("Processing downloads...") + # Process downloads using service + downloaded_files, successful_downloads, failed_downloads = ( + download_service.process_downloads(decoded_data) + ) + + logger.info("Processing extractions...") + # Process extractions using service + extracted_folders = extraction_service.process_extractions( + downloaded_files, args.extract_files + ) + + logger.info("Formatting response...") + # Format response using handler + response = response_handler.format_download_response( + decoded_data, + downloaded_files, + successful_downloads, + failed_downloads, + extracted_folders, + ) + + # Output results + if args.output_json: + with open(args.output_json, "w") as f: + json.dump(response.dict(), f, indent=2) + logger.info(f"Results written to {args.output_json}") + else: + print(json.dumps(response.dict(), indent=2)) + + # Log summary + logger.info( + f"Download completed: {successful_downloads} successful, {failed_downloads} failed" + ) + logger.info(f"Extracted folders: {len(extracted_folders)}") + + # Exit with appropriate code + sys.exit(0 if response.success else 1) + + except ValueError as e: + logger.error(f"Decoding error: {str(e)}") + traceback.print_exc() + sys.exit(1) + except Exception as e: + logger.error(f"Internal error: {str(e)}") + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/dataset_file_handler/fetch_chunk_without_filter.py b/src/dataset_file_handler/fetch_chunk_without_filter.py new file mode 100644 index 00000000..44814d4a --- /dev/null +++ b/src/dataset_file_handler/fetch_chunk_without_filter.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +""" +Python script to download a specific chunk from S3 bucket and return as JSON. +Used by CronManager endpoint to fetch individual chunks. +""" + +import sys +import json +import argparse +import logging +import os +import tempfile +from pathlib import Path +import traceback + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[logging.StreamHandler(sys.stderr)], # Log to stderr to keep stdout clean +) +logger = logging.getLogger(__name__) + +# Add the s3_dataset_processor to Python path to import modules +script_dir = Path("/app/src/s3_dataset_processor") +sys.path.insert(0, str(script_dir)) + + +def log(message): + """Log to stderr to keep stdout clean for JSON output""" + logger.info(f"🔍 [CHUNK DOWNLOAD] {message}") + + +try: + from services.s3_ferry_service import S3Ferry + + s3_ferry_service = S3Ferry() + log("Successfully imported S3FerryService") +except ImportError as e: + log(f"Failed to import S3FerryService: {e}") + sys.exit(1) + + +def download_chunk_from_s3(dataset_id: str, page_num: int) -> dict: + """ + Download a specific chunk from S3 bucket. + + Args: + dataset_id: Dataset ID + page_num: Page number (chunk number) + + Returns: + Dictionary containing chunk data or error information + """ + try: + log(f"Starting chunk download - Dataset ID: {dataset_id}, Page: {page_num}") + + # Create temporary directory for download + temp_dir = tempfile.mkdtemp(prefix="chunk_download_") + log(f"Created temporary directory: {temp_dir}") + + # Define S3 source path and local destination + chunk_filename = f"{page_num}.json" + s3_source_path = f"{dataset_id}/{chunk_filename}" + local_dest_path = f"temp_chunks/{chunk_filename}" + + # Create the temp_chunks directory if it doesn't exist + temp_chunks_dir = "temp_chunks" + os.makedirs(temp_chunks_dir, exist_ok=True) + log(f"Created/verified temp directory: {temp_chunks_dir}") + + log(f"S3 source path: {s3_source_path}") + log(f"Local destination: {local_dest_path}") + + # Download chunk from S3 using S3Ferry service + response = s3_ferry_service.transfer_file( + destination_file_path=local_dest_path, + destination_storage_type="FS", + source_file_path=s3_source_path, + source_storage_type="S3", + ) + + log(f"S3Ferry response status: {response.status_code}") + log(f"S3Ferry response body: {response.text}") + + if response.status_code in [200, 201]: + # Read the downloaded chunk file + local_file_path = f"/app/{local_dest_path}" + + if os.path.exists(local_file_path): + log(f"Successfully downloaded chunk to: {local_file_path}") + + # Read and parse the chunk data + with open(local_file_path, "r", encoding="utf-8") as f: + chunk_data = json.load(f) + + # Clean up the downloaded file + os.remove(local_file_path) + log(f"Cleaned up downloaded file: {local_file_path}") + + # Remove empty directory if it exists + try: + os.rmdir(os.path.dirname(local_file_path)) + except OSError: + pass # Directory not empty or doesn't exist + + return { + "success": True, + "dataset_id": dataset_id, + "page_num": page_num, + "chunk_data": chunk_data, + "message": f"Successfully downloaded chunk {page_num} for dataset {dataset_id}", + } + else: + return { + "success": False, + "dataset_id": dataset_id, + "page_num": page_num, + "error": f"Downloaded file not found at: {local_file_path}", + "message": "File download completed but file not accessible", + } + else: + return { + "success": False, + "dataset_id": dataset_id, + "page_num": page_num, + "error": f"S3 download failed: HTTP {response.status_code}", + "response_body": response.text, + "message": f"Failed to download chunk {page_num} from S3", + } + + except Exception as e: + log(f"Error during chunk download: {str(e)}") + traceback.print_exc() + return { + "success": False, + "dataset_id": dataset_id, + "page_num": page_num, + "error": str(e), + "message": "Internal error during chunk download", + } + + +def main(): + """Main function to handle chunk download process.""" + parser = argparse.ArgumentParser(description="Download a specific chunk from S3") + parser.add_argument("--dataset-id", required=True, help="Dataset ID") + parser.add_argument( + "--page-num", required=True, type=int, help="Page number (chunk number)" + ) + parser.add_argument("--output-json", help="Output file path for results JSON") + + args = parser.parse_args() + + try: + log( + f"Processing chunk download request - Dataset: {args.dataset_id}, Page: {args.page_num}" + ) + + # Download the chunk + result = download_chunk_from_s3(args.dataset_id, args.page_num) + + # Output results + if args.output_json: + with open(args.output_json, "w") as f: + json.dump(result, f, indent=2) + log(f"Results written to {args.output_json}") + else: + # Output ONLY the JSON to stdout (this goes to CronManager) + print(json.dumps(result)) + + log(f"Chunk download completed - Success: {result['success']}") + + # Exit with appropriate code + sys.exit(0 if result["success"] else 1) + + except Exception as e: + log(f"Internal error: {str(e)}") + traceback.print_exc() + + error_result = { + "success": False, + "dataset_id": args.dataset_id, + "page_num": args.page_num, + "error": str(e), + "message": "Script execution failed", + } + + if args.output_json: + with open(args.output_json, "w") as f: + json.dump(error_result, f, indent=2) + else: + print(json.dumps(error_result)) + + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/dataset_file_handler/fetch_multi_chunk.py b/src/dataset_file_handler/fetch_multi_chunk.py new file mode 100644 index 00000000..8ad5f24a --- /dev/null +++ b/src/dataset_file_handler/fetch_multi_chunk.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +""" +Python script to download multiple chunks from S3 bucket, aggregate them, and return as JSON. +Used by CronManager endpoint to fetch and combine multiple chunks. +""" + +import sys +import json +import argparse +import logging +import os +from pathlib import Path +import traceback +from typing import List, Dict, Any + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[logging.StreamHandler(sys.stderr)], # Log to stderr to keep stdout clean +) +logger = logging.getLogger(__name__) + +# Add the s3_dataset_processor to Python path to import modules +script_dir = Path("/app/src/s3_dataset_processor") +sys.path.insert(0, str(script_dir)) + + +def log(message): + """Log to stderr to keep stdout clean for JSON output""" + logger.info(f"📦 [MULTI CHUNK] {message}") + + +try: + from services.s3_ferry_service import S3Ferry + + s3_ferry_service = S3Ferry() + log("Successfully imported S3FerryService") +except ImportError as e: + log(f"Failed to import S3FerryService: {e}") + sys.exit(1) + + +def download_single_chunk_from_s3(dataset_id: str, chunk_id: int) -> Dict[str, Any]: + """ + Download a single chunk from S3 bucket. + + Args: + dataset_id: Dataset ID + chunk_id: Chunk ID/number + + Returns: + Dictionary containing chunk data or error information + """ + try: + log(f"Downloading chunk {chunk_id} from dataset {dataset_id}") + + # Define S3 source path and local destination + chunk_filename = f"{chunk_id}.json" + s3_source_path = f"{dataset_id}/{chunk_filename}" + local_dest_path = f"temp_chunks/{chunk_filename}" + + # Create the temp_chunks directory if it doesn't exist + temp_chunks_dir = "/app/temp_chunks" + os.makedirs(temp_chunks_dir, exist_ok=True) + + log(f"S3 source path: {s3_source_path}") + log(f"Local destination: {local_dest_path}") + + # Download chunk from S3 using S3Ferry service + response = s3_ferry_service.transfer_file( + destination_file_path=local_dest_path, + destination_storage_type="FS", + source_file_path=s3_source_path, + source_storage_type="S3", + ) + + log(f"S3Ferry response status for chunk {chunk_id}: {response.status_code}") + + if response.status_code in [200, 201]: + # Read the downloaded chunk file + local_file_path = f"/app/{local_dest_path}" + + if os.path.exists(local_file_path): + log(f"Successfully downloaded chunk {chunk_id} to: {local_file_path}") + + # Read and parse the chunk data + with open(local_file_path, "r", encoding="utf-8") as f: + chunk_data = json.load(f) + + # Clean up the downloaded file + os.remove(local_file_path) + log(f"Cleaned up downloaded file: {local_file_path}") + + return { + "success": True, + "chunk_id": chunk_id, + "chunk_data": chunk_data, + "message": f"Successfully downloaded chunk {chunk_id}", + } + else: + return { + "success": False, + "chunk_id": chunk_id, + "error": f"Downloaded file not found at: {local_file_path}", + "message": f"Chunk {chunk_id} download completed but file not accessible", + } + else: + return { + "success": False, + "chunk_id": chunk_id, + "error": f"S3 download failed: HTTP {response.status_code}", + "response_body": response.text, + "message": f"Failed to download chunk {chunk_id} from S3", + } + + except Exception as e: + log(f"Error downloading chunk {chunk_id}: {str(e)}") + traceback.print_exc() + return { + "success": False, + "chunk_id": chunk_id, + "error": str(e), + "message": f"Internal error during chunk {chunk_id} download", + } + + +def download_multiple_chunks(dataset_id: str, chunk_ids: List[int]) -> Dict[str, Any]: + """ + Download multiple chunks from S3 and aggregate them. + + Args: + dataset_id: Dataset ID + chunk_ids: List of chunk IDs to download + + Returns: + Dictionary containing aggregated chunk data or error information + """ + try: + log( + f"Starting multi-chunk download - Dataset ID: {dataset_id}, Chunks: {chunk_ids}" + ) + + download_results = [] + successful_chunks = [] + failed_chunks = [] + aggregated_data = [] + total_items = 0 + + # Download each chunk + for chunk_id in chunk_ids: + result = download_single_chunk_from_s3(dataset_id, chunk_id) + download_results.append(result) + + if result["success"]: + successful_chunks.append(chunk_id) + chunk_data = result["chunk_data"] + + # Extract data array from chunk + chunk_items = chunk_data.get("data", []) + aggregated_data.extend(chunk_items) + total_items += len(chunk_items) + + log( + f"✅ Chunk {chunk_id}: {len(chunk_items)} items added to aggregation" + ) + else: + failed_chunks.append(chunk_id) + log( + f"❌ Chunk {chunk_id}: Download failed - {result.get('error', 'Unknown error')}" + ) + + # Prepare chunk info from the first successful chunk (if any) + chunk_info = {} + if successful_chunks and download_results: + first_successful = next((r for r in download_results if r["success"]), None) + if first_successful: + original_chunk_info = first_successful["chunk_data"].get( + "chunk_info", {} + ) + chunk_info = { + "original_dataset": original_chunk_info.get( + "original_dataset", dataset_id + ), + "requested_chunks": chunk_ids, + "successful_chunks": successful_chunks, + "failed_chunks": failed_chunks, + "total_chunks_requested": len(chunk_ids), + "successful_downloads": len(successful_chunks), + "failed_downloads": len(failed_chunks), + "total_aggregated_items": total_items, + "aggregation_range": f"chunks {min(successful_chunks)}-{max(successful_chunks)}" + if successful_chunks + else "none", + } + + # Prepare the final aggregated payload + if successful_chunks: + aggregated_payload = { + "success": True, + "dataset_id": dataset_id, + "chunk_info": chunk_info, + "aggregated_data": aggregated_data, + "download_summary": { + "total_requested": len(chunk_ids), + "successful_downloads": len(successful_chunks), + "failed_downloads": len(failed_chunks), + "successful_chunk_ids": successful_chunks, + "failed_chunk_ids": failed_chunks, + "total_items_aggregated": total_items, + }, + "download_details": download_results, + "message": f"Successfully aggregated {len(successful_chunks)} out of {len(chunk_ids)} requested chunks", + } + else: + aggregated_payload = { + "success": False, + "dataset_id": dataset_id, + "chunk_info": chunk_info, + "aggregated_data": [], + "download_summary": { + "total_requested": len(chunk_ids), + "successful_downloads": 0, + "failed_downloads": len(failed_chunks), + "successful_chunk_ids": [], + "failed_chunk_ids": failed_chunks, + "total_items_aggregated": 0, + }, + "download_details": download_results, + "error": "All chunk downloads failed", + "message": f"Failed to download any of the {len(chunk_ids)} requested chunks", + } + + log( + f"Multi-chunk aggregation completed - Success: {aggregated_payload['success']}" + ) + log(f"Total items aggregated: {total_items}") + + return aggregated_payload + + except Exception as e: + log(f"Error during multi-chunk aggregation: {str(e)}") + traceback.print_exc() + return { + "success": False, + "dataset_id": dataset_id, + "chunk_info": {}, + "aggregated_data": [], + "download_summary": { + "total_requested": len(chunk_ids), + "successful_downloads": 0, + "failed_downloads": len(chunk_ids), + "successful_chunk_ids": [], + "failed_chunk_ids": chunk_ids, + "total_items_aggregated": 0, + }, + "error": str(e), + "message": "Internal error during multi-chunk aggregation", + } + + +def parse_chunk_ids(chunk_ids_str: str) -> List[int]: + """ + Parse chunk IDs from string format "1 2 3" to list [1, 2, 3]. + + Args: + chunk_ids_str: String containing space-separated chunk IDs + + Returns: + List of integer chunk IDs + """ + try: + # Split by spaces and convert to integers + chunk_ids = [ + int(chunk_id.strip()) + for chunk_id in chunk_ids_str.split() + if chunk_id.strip() + ] + log(f"Parsed chunk IDs: {chunk_ids}") + return chunk_ids + except ValueError as e: + log(f"Error parsing chunk IDs '{chunk_ids_str}': {str(e)}") + raise ValueError( + f"Invalid chunk IDs format. Expected space-separated integers, got: '{chunk_ids_str}'" + ) + + +def main(): + """Main function to handle multi-chunk download and aggregation process.""" + parser = argparse.ArgumentParser( + description="Download and aggregate multiple chunks from S3" + ) + parser.add_argument("--dataset-id", required=True, help="Dataset ID") + parser.add_argument( + "--chunk-ids", required=True, help='Space-separated chunk IDs (e.g., "1 2 3")' + ) + parser.add_argument("--output-json", help="Output file path for results JSON") + + args = parser.parse_args() + + try: + log( + f"Processing multi-chunk request - Dataset: {args.dataset_id}, Chunk IDs: {args.chunk_ids}" + ) + + # Parse chunk IDs + chunk_ids = parse_chunk_ids(args.chunk_ids) + + if not chunk_ids: + raise ValueError("No valid chunk IDs provided") + + # Download and aggregate chunks + result = download_multiple_chunks(args.dataset_id, chunk_ids) + + # Output results + if args.output_json: + with open(args.output_json, "w") as f: + json.dump(result, f, indent=2) + log(f"Results written to {args.output_json}") + else: + # Output ONLY the JSON to stdout (this goes to CronManager) + print(json.dumps(result)) + + log(f"Multi-chunk processing completed - Success: {result['success']}") + + # Exit with appropriate code + sys.exit(0 if result["success"] else 1) + + except Exception as e: + log(f"Internal error: {str(e)}") + traceback.print_exc() + + error_result = { + "success": False, + "dataset_id": args.dataset_id, + "chunk_ids": args.chunk_ids, + "error": str(e), + "message": "Script execution failed", + } + + if args.output_json: + with open(args.output_json, "w") as f: + json.dump(error_result, f, indent=2) + else: + print(json.dumps(error_result)) + + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/dataset_file_handler/handlers/__init__.py b/src/dataset_file_handler/handlers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/dataset_file_handler/handlers/response_handler.py b/src/dataset_file_handler/handlers/response_handler.py new file mode 100644 index 00000000..19264ea7 --- /dev/null +++ b/src/dataset_file_handler/handlers/response_handler.py @@ -0,0 +1,41 @@ +"""Handler for API response formatting.""" + +from typing import List, Dict, Any + +from models.schemas import DownloadResponse, DownloadedFile + + +class ResponseHandler: + """Handler class for formatting API responses.""" + + @staticmethod + def format_download_response( + decoded_data: List[Dict[str, Any]], + downloaded_files: List[DownloadedFile], + successful_downloads: int, + failed_downloads: int, + extracted_folders: List[Dict[str, str]], + ) -> DownloadResponse: + """ + Format download response. + + Args: + decoded_data: Original decoded data + downloaded_files: List of downloaded files + successful_downloads: Number of successful downloads + failed_downloads: Number of failed downloads + extracted_folders: List of extracted folder information + + Returns: + Formatted download response + """ + return DownloadResponse( + success=successful_downloads > 0, + message=f"Downloaded {successful_downloads} files, {failed_downloads} failed", + total_downloads=len(decoded_data), + successful_downloads=successful_downloads, + failed_downloads=failed_downloads, + downloaded_files=downloaded_files, + extracted_folders=extracted_folders, + total_extracted_folders=len(extracted_folders), + ) diff --git a/src/dataset_file_handler/models/__init__.py b/src/dataset_file_handler/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/dataset_file_handler/models/schemas.py b/src/dataset_file_handler/models/schemas.py new file mode 100644 index 00000000..30b7c272 --- /dev/null +++ b/src/dataset_file_handler/models/schemas.py @@ -0,0 +1,74 @@ +"""Pydantic models for API requests and responses.""" + +from pydantic import BaseModel +from typing import List, Dict, Optional, Any + + +class DownloadRequest(BaseModel): + """Request model for downloading files from signed URLs.""" + + encoded_data: str + extract_files: Optional[bool] = True + + +class DownloadedFile(BaseModel): + """Model for downloaded file information.""" + + agency_id: str + agency_name: str + original_filename: str + local_path: str + file_size: int + extracted_path: Optional[str] = None + extraction_success: bool = False + + +class DownloadResponse(BaseModel): + """Response model for download operation.""" + + success: bool + message: str + total_downloads: int + successful_downloads: int + failed_downloads: int + downloaded_files: List[DownloadedFile] + extracted_folders: List[Dict[str, str]] + total_extracted_folders: int + + +class ChunkDownloadRequest(BaseModel): + """Request model for downloading a single chunk.""" + + datasetId: str + pageNum: int + + +class MultiChunkDownloadRequest(BaseModel): + """Request model for downloading multiple chunks.""" + + datasetId: str + chunkIds: List[int] + + +class ChunkDownloadResponse(BaseModel): + """Response model for single chunk download.""" + + success: bool + dataset_id: str + page_num: Optional[int] = None + chunk_data: Optional[Dict[str, Any]] = None + error: Optional[str] = None + message: str + + +class MultiChunkDownloadResponse(BaseModel): + """Response model for multi-chunk download and aggregation.""" + + success: bool + dataset_id: str + chunk_info: Optional[Dict[str, Any]] = None + aggregated_data: List[Dict[str, Any]] + download_summary: Dict[str, Any] + download_details: Optional[List[Dict[str, Any]]] = None + error: Optional[str] = None + message: str diff --git a/src/dataset_file_handler/multiple_chunk_handler.py b/src/dataset_file_handler/multiple_chunk_handler.py new file mode 100644 index 00000000..9fa8aad8 --- /dev/null +++ b/src/dataset_file_handler/multiple_chunk_handler.py @@ -0,0 +1,260 @@ +"""Service for handling multiple chunk downloads and aggregation from S3.""" + +import json +import os +import traceback +from typing import List, Dict, Any +from loguru import logger + +from services.s3_ferry_service import S3Ferry + + +class MultiChunkService: + """Service class for handling multiple chunk operations.""" + + def __init__(self): + """Initialize the multi-chunk service.""" + self.s3_ferry_service = S3Ferry() + logger.info("MultiChunkService initialized") + + def download_single_chunk_from_s3( + self, dataset_id: str, chunk_id: int + ) -> Dict[str, Any]: + """ + Download a single chunk from S3 bucket. + + Args: + dataset_id: Dataset ID + chunk_id: Chunk ID to download + + Returns: + Dictionary containing download result + """ + try: + logger.info(f"Downloading chunk {chunk_id} for dataset {dataset_id}") + + # Define S3 source path and local destination + chunk_filename = f"{chunk_id}.json" + s3_source_path = f"{dataset_id}/{chunk_filename}" + local_dest_path = f"temp_chunks/{chunk_filename}" + + # Create the temp_chunks directory if it doesn't exist + os.makedirs("temp_chunks", exist_ok=True) + + # Download chunk from S3 using S3Ferry service + response = self.s3_ferry_service.transfer_file( + destination_file_path=local_dest_path, + destination_storage_type="FS", + source_file_path=s3_source_path, + source_storage_type="S3", + ) + + logger.info( + f"S3Ferry response status for chunk {chunk_id}: {response.status_code}" + ) + + if response.status_code in [200, 201]: + # Read the downloaded chunk file + local_file_path = f"/app/{local_dest_path}" + + if os.path.exists(local_file_path): + logger.info( + f"Successfully downloaded chunk {chunk_id} to: {local_file_path}" + ) + + # Read and parse the chunk data + with open(local_file_path, "r", encoding="utf-8") as f: + chunk_data = json.load(f) + + # Clean up the downloaded file + os.remove(local_file_path) + logger.info(f"Cleaned up downloaded file: {local_file_path}") + + return { + "success": True, + "chunk_id": chunk_id, + "chunk_data": chunk_data, + "message": f"Successfully downloaded chunk {chunk_id}", + } + else: + return { + "success": False, + "chunk_id": chunk_id, + "error": f"Downloaded file not found at: {local_file_path}", + "message": f"Chunk {chunk_id} download completed but file not accessible", + } + else: + return { + "success": False, + "chunk_id": chunk_id, + "error": f"S3 download failed: HTTP {response.status_code}", + "response_body": response.text, + "message": f"Failed to download chunk {chunk_id} from S3", + } + + except Exception as e: + logger.error(f"Error downloading chunk {chunk_id}: {str(e)}") + traceback.print_exc() + return { + "success": False, + "chunk_id": chunk_id, + "error": str(e), + "message": f"Internal error during chunk {chunk_id} download", + } + + def download_multiple_chunks( + self, dataset_id: str, chunk_ids: List[int] + ) -> Dict[str, Any]: + """ + Download multiple chunks from S3 and aggregate them. + + Args: + dataset_id: Dataset ID + chunk_ids: List of chunk IDs to download + + Returns: + Dictionary containing aggregated chunk data or error information + """ + try: + logger.info( + f"Starting multi-chunk download - Dataset ID: {dataset_id}, Chunks: {chunk_ids}" + ) + + download_results = [] + successful_chunks = [] + failed_chunks = [] + aggregated_data = [] + total_items = 0 + + # Download each chunk + for chunk_id in chunk_ids: + result = self.download_single_chunk_from_s3(dataset_id, chunk_id) + download_results.append(result) + + if result["success"]: + successful_chunks.append(chunk_id) + chunk_data = result["chunk_data"] + + # Extract data array from chunk + chunk_items = chunk_data.get("data", []) + aggregated_data.extend(chunk_items) + total_items += len(chunk_items) + + logger.info( + f"✅ Chunk {chunk_id}: {len(chunk_items)} items added to aggregation" + ) + else: + failed_chunks.append(chunk_id) + logger.error( + f"❌ Chunk {chunk_id}: Download failed - {result.get('error', 'Unknown error')}" + ) + + # Prepare chunk info from the first successful chunk (if any) + chunk_info = {} + if successful_chunks and download_results: + first_successful = next( + (r for r in download_results if r["success"]), None + ) + if first_successful: + original_chunk_info = first_successful["chunk_data"].get( + "chunk_info", {} + ) + chunk_info = { + "original_dataset": original_chunk_info.get( + "original_dataset", dataset_id + ), + "requested_chunks": chunk_ids, + "successful_chunks": successful_chunks, + "failed_chunks": failed_chunks, + "total_chunks_requested": len(chunk_ids), + "successful_downloads": len(successful_chunks), + "failed_downloads": len(failed_chunks), + "total_aggregated_items": total_items, + "aggregation_range": f"chunks {min(successful_chunks)}-{max(successful_chunks)}" + if successful_chunks + else "none", + } + + # Prepare the final aggregated payload + if successful_chunks: + aggregated_payload = { + "success": True, + "dataset_id": dataset_id, + "chunk_info": chunk_info, + "aggregated_data": aggregated_data, + "download_summary": { + "total_requested": len(chunk_ids), + "successful_downloads": len(successful_chunks), + "failed_downloads": len(failed_chunks), + "successful_chunk_ids": successful_chunks, + "failed_chunk_ids": failed_chunks, + "total_items_aggregated": total_items, + }, + "download_details": download_results, + "message": f"Successfully aggregated {len(successful_chunks)} out of {len(chunk_ids)} requested chunks", + } + else: + aggregated_payload = { + "success": False, + "dataset_id": dataset_id, + "chunk_info": chunk_info, + "aggregated_data": [], + "download_summary": { + "total_requested": len(chunk_ids), + "successful_downloads": 0, + "failed_downloads": len(failed_chunks), + "successful_chunk_ids": [], + "failed_chunk_ids": failed_chunks, + "total_items_aggregated": 0, + }, + "download_details": download_results, + "error": "All chunk downloads failed", + "message": f"Failed to download any of the {len(chunk_ids)} requested chunks", + } + + logger.info( + f"Multi-chunk aggregation completed - Success: {aggregated_payload['success']}" + ) + logger.info(f"Total items aggregated: {total_items}") + + return aggregated_payload + + except Exception as e: + logger.error(f"Error during multi-chunk aggregation: {str(e)}") + traceback.print_exc() + return { + "success": False, + "dataset_id": dataset_id, + "chunk_info": {}, + "aggregated_data": [], + "download_summary": { + "total_requested": len(chunk_ids), + "successful_downloads": 0, + "failed_downloads": len(chunk_ids), + "successful_chunk_ids": [], + "failed_chunk_ids": chunk_ids, + "total_items_aggregated": 0, + }, + "error": str(e), + "message": "Internal error during multi-chunk aggregation", + } + + def parse_chunk_ids(self, chunk_ids_str: str) -> List[int]: + """ + Parse chunk IDs from string format. + + Args: + chunk_ids_str: Space-separated chunk IDs (e.g., "1 2 3") + + Returns: + List of chunk IDs as integers + """ + try: + chunk_ids = [ + int(x.strip()) for x in chunk_ids_str.split() if x.strip().isdigit() + ] + logger.info(f"Parsed chunk IDs: {chunk_ids}") + return chunk_ids + except Exception as e: + logger.error(f"Error parsing chunk IDs '{chunk_ids_str}': {str(e)}") + return [] diff --git a/src/dataset_file_handler/requirements.txt b/src/dataset_file_handler/requirements.txt new file mode 100644 index 00000000..364fc7cf --- /dev/null +++ b/src/dataset_file_handler/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn[standard] +pydantic +loguru +requests +python-multipart +aiofiles \ No newline at end of file diff --git a/src/dataset_file_handler/services/__init__.py b/src/dataset_file_handler/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/dataset_file_handler/services/download_service.py b/src/dataset_file_handler/services/download_service.py new file mode 100644 index 00000000..146eaeac --- /dev/null +++ b/src/dataset_file_handler/services/download_service.py @@ -0,0 +1,117 @@ +"""Service for file download operations.""" + +import os +import requests +from typing import List, Dict, Any +from urllib.parse import urlparse +from config.settings import settings +from models.schemas import DownloadedFile +import sys +import logging + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[logging.StreamHandler(sys.stdout)], +) +logger = logging.getLogger(__name__) + + +class DownloadService: + """Service class for handling file download operations.""" + + def __init__(self): + """Initialize the download service.""" + self.data_dir = settings.DATA_DIR + self.timeout = settings.DOWNLOAD_TIMEOUT + self.chunk_size = settings.CHUNK_SIZE + + def download_file(self, url: str, local_path: str) -> bool: + """ + Download a file from URL to local path. + + Args: + url: The presigned URL to download from + local_path: Local file path to save the file + + Returns: + True if download successful, False otherwise + """ + try: + response = requests.get(url, stream=True, timeout=self.timeout) + response.raise_for_status() + + # Ensure directory exists + os.makedirs(os.path.dirname(local_path), exist_ok=True) + + with open(local_path, "wb") as f: + for chunk in response.iter_content(chunk_size=self.chunk_size): + if chunk: + f.write(chunk) + + return True + except Exception as e: + logger.error(f"Failed to download {url}: {e}") + return False + + def process_downloads(self, decoded_data: List[Dict[str, Any]]) -> tuple: + """ + Process multiple downloads from decoded data. + + Args: + decoded_data: List of decoded URL data + + Returns: + Tuple of (downloaded_files, successful_downloads, failed_downloads) + """ + downloaded_files = [] + successful_downloads = 0 + failed_downloads = 0 + try: + for entry in decoded_data: + agency_id = entry.get("agencyId", "unknown") + agency_name = entry.get("agencyName", "Unknown Agency") + signed_url = entry.get("dataUrl", "") + + if not signed_url: + failed_downloads += 1 + continue + + # Parse URL to get filename + parsed_url = urlparse(signed_url) + original_filename = ( + parsed_url.path.split("/")[-1] + if parsed_url.path + else f"{agency_id}.zip" + ) + + # Download file to data directory + local_file_path = os.path.join(self.data_dir, original_filename) + logger.info( + f"Downloading {original_filename} for agency {agency_id} with name {agency_name}" + ) + + if self.download_file(signed_url, local_file_path): + file_size = os.path.getsize(local_file_path) + + downloaded_file = DownloadedFile( + agency_id=agency_id, + agency_name=agency_name, + original_filename=original_filename, + local_path=local_file_path, + file_size=file_size, + ) + + downloaded_files.append(downloaded_file) + successful_downloads += 1 + logger.info(f"Successfully downloaded {original_filename}") + + else: + failed_downloads += 1 + logger.error(f"Failed to download {original_filename}") + except Exception as e: + logger.error(f"Error processing downloads: {e}") + return downloaded_files, successful_downloads, failed_downloads + return downloaded_files, successful_downloads, failed_downloads diff --git a/src/dataset_file_handler/services/extraction_service.py b/src/dataset_file_handler/services/extraction_service.py new file mode 100644 index 00000000..8b937234 --- /dev/null +++ b/src/dataset_file_handler/services/extraction_service.py @@ -0,0 +1,143 @@ +"""Service for file extraction operations.""" + +import os +import zipfile +import shutil +from typing import List, Dict + +from config.settings import settings +from models.schemas import DownloadedFile +import sys +import logging + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[logging.StreamHandler(sys.stdout)], +) +logger = logging.getLogger(__name__) + + +class ExtractionService: + """Service class for handling file extraction operations.""" + + def __init__(self): + """Initialize the extraction service.""" + self.data_dir = settings.DATA_DIR + + def extract_zip_file(self, zip_path: str, extract_to: str) -> bool: + """ + Extract a ZIP file to the specified directory. + + Args: + zip_path: Path to the ZIP file + extract_to: Directory to extract files to + + Returns: + True if extraction successful, False otherwise + """ + try: + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(extract_to) + return True + except Exception as e: + print(f"Failed to extract {zip_path}: {e}") + return False + + def process_extractions( + self, downloaded_files: List[DownloadedFile], extract_files: bool = True + ) -> List[Dict[str, str]]: + """ + Process extractions for downloaded files. + + Args: + downloaded_files: List of downloaded files to extract + extract_files: Whether to extract files + + Returns: + List of extracted folder information + """ + extracted_folders = [] + + if not extract_files: + return extracted_folders + + for downloaded_file in downloaded_files: + if not downloaded_file.original_filename.lower().endswith(".zip"): + continue + + # Extract agency name from filename + # agency_name = downloaded_file.original_filename.replace(".zip", "") + agency_name = downloaded_file.agency_name + agency_dir = os.path.join(self.data_dir, agency_name) + os.makedirs(agency_dir, exist_ok=True) + + # Extract to temporary directory first + temp_extract_dir = os.path.join(self.data_dir, f"temp_{agency_name}") + logger.info( + f"Extracting {downloaded_file.original_filename} to temporary directory: {temp_extract_dir}" + ) + + if self.extract_zip_file(downloaded_file.local_path, temp_extract_dir): + self._move_extracted_contents(temp_extract_dir, agency_dir) + + # Update downloaded file info + downloaded_file.extracted_path = agency_dir + downloaded_file.extraction_success = True + logger.info( + f"Successfully extracted {downloaded_file.original_filename}" + ) + + # Add to extracted folders list + extracted_folders.append( + { + "agency_id": downloaded_file.agency_id, + "agency_name": downloaded_file.agency_name, + "folder_path": agency_dir, + } + ) + + # Remove the ZIP file after successful extraction + os.remove(downloaded_file.local_path) + logger.info(f"Removed ZIP file {downloaded_file.local_path}") + else: + downloaded_file.extraction_success = False + logger.error(f"Failed to extract {downloaded_file.original_filename}") + + return extracted_folders + + def _move_extracted_contents(self, temp_extract_dir: str, agency_dir: str) -> None: + """ + Move extracted contents from temporary directory to agency directory. + + Args: + temp_extract_dir: Temporary extraction directory + agency_dir: Target agency directory + """ + extracted_items = os.listdir(temp_extract_dir) + + if len(extracted_items) == 1 and os.path.isdir( + os.path.join(temp_extract_dir, extracted_items[0]) + ): + # Single folder was extracted - move its contents to the agency directory + nested_folder = os.path.join(temp_extract_dir, extracted_items[0]) + logger.info( + f"Found nested folder structure, moving contents from {nested_folder} to {agency_dir}" + ) + + for item in os.listdir(nested_folder): + src = os.path.join(nested_folder, item) + dst = os.path.join(agency_dir, item) + shutil.move(src, dst) + else: + # Multiple items or files extracted - move everything to agency directory + logger.info(f"Moving extracted contents directly to {agency_dir}") + for item in extracted_items: + src = os.path.join(temp_extract_dir, item) + dst = os.path.join(agency_dir, item) + shutil.move(src, dst) + + # Clean up temporary directory + shutil.rmtree(temp_extract_dir, ignore_errors=True) diff --git a/src/dataset_file_handler/services/s3_ferry_service.py b/src/dataset_file_handler/services/s3_ferry_service.py new file mode 100644 index 00000000..0c0cfafe --- /dev/null +++ b/src/dataset_file_handler/services/s3_ferry_service.py @@ -0,0 +1,166 @@ +"""Service for S3Ferry file transfer operations.""" + +import requests +import logging +import traceback +from typing import Dict + +# Configure logging +logger = logging.getLogger(__name__) + + +class S3Ferry: + """Service class for handling S3Ferry file transfer operations.""" + + def __init__(self, base_url: str = "http://gc-s3-ferry:3000"): + """ + Initialize the S3Ferry service. + + Args: + base_url: Base URL for the S3Ferry service + """ + self.base_url = base_url + self.url = f"{base_url}/v1/files/copy" + logger.info(f"S3Ferry service initialized with URL: {self.url}") + + def transfer_file( + self, + destination_file_path: str, + destination_storage_type: str, + source_file_path: str, + source_storage_type: str, + ) -> requests.Response: + """ + Transfer a file using S3Ferry service. + + Args: + destination_file_path: Path where the file should be stored in destination + destination_storage_type: Type of destination storage (e.g., 's3', 'local') + source_file_path: Path of the source file + source_storage_type: Type of source storage (e.g., 'local', 's3') + + Returns: + Response object from the S3Ferry service + """ + try: + payload = self.get_s3_ferry_payload( + destination_file_path, + destination_storage_type, + source_file_path, + source_storage_type, + ) + + logger.info( + f"[S3_FERRY] Transferring file: {source_file_path} -> {destination_file_path}" + ) + logger.debug(f"[S3_FERRY] Payload: {payload}") + + response = requests.post( + self.url, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=60, + ) + + logger.info(f"[S3_FERRY] Transfer response status: {response.status_code}") + + # Accept both 200 (OK) and 201 (Created) as success + if response.status_code not in [200, 201]: + logger.error(f"[S3_FERRY] Transfer failed: {response.text}") + else: + logger.info( + f"[S3_FERRY] ✅ Transfer successful (HTTP {response.status_code})" + ) + + return response + + except Exception as e: + logger.error(f"[S3_FERRY] Error during file transfer: {str(e)}") + traceback.print_exc() + raise + + def get_s3_ferry_payload( + self, + destination_file_path: str, + destination_storage_type: str, + source_file_path: str, + source_storage_type: str, + ) -> Dict[str, str]: + """ + Generate S3Ferry payload for file transfer. + + Args: + destination_file_path: Path where the file should be stored in destination + destination_storage_type: Type of destination storage + source_file_path: Path of the source file + source_storage_type: Type of source storage + + Returns: + Dictionary containing the S3Ferry payload + """ + payload = { + "destinationFilePath": destination_file_path, + "destinationStorageType": destination_storage_type, + "sourceFilePath": source_file_path, + "sourceStorageType": source_storage_type, + } + + return payload + + def upload_to_s3( + self, local_file_path: str, s3_destination_path: str + ) -> requests.Response: + """ + Convenience method to upload a local file to S3. + + Args: + local_file_path: Path to the local file + s3_destination_path: S3 destination path (e.g., 'bucket/folder/file.json') + + Returns: + Response object from the S3Ferry service + """ + return self.transfer_file( + destination_file_path=s3_destination_path, + destination_storage_type="S3", + source_file_path=local_file_path, + source_storage_type="FS", + ) + + def download_from_s3( + self, s3_source_path: str, local_destination_path: str + ) -> requests.Response: + """ + Convenience method to download a file from S3 to local storage. + + Args: + s3_source_path: S3 source path (e.g., 'bucket/folder/file.json') + local_destination_path: Local destination path + + Returns: + Response object from the S3Ferry service + """ + return self.transfer_file( + destination_file_path=local_destination_path, + destination_storage_type="local", + source_file_path=s3_source_path, + source_storage_type="s3", + ) + + # def copy_s3_to_s3(self, source_s3_path: str, destination_s3_path: str) -> requests.Response: + # """ + # Convenience method to copy files between S3 locations. + + # Args: + # source_s3_path: Source S3 path + # destination_s3_path: Destination S3 path + + # Returns: + # Response object from the S3Ferry service + # """ + # return self.transfer_file( + # destination_file_path=destination_s3_path, + # destination_storage_type="s3", + # source_file_path=source_s3_path, + # source_storage_type="s3" + # ) diff --git a/src/dataset_file_handler/services/url_decoder_service.py b/src/dataset_file_handler/services/url_decoder_service.py new file mode 100644 index 00000000..901008b0 --- /dev/null +++ b/src/dataset_file_handler/services/url_decoder_service.py @@ -0,0 +1,38 @@ +"""Service for URL decoding operations.""" + +import urllib.parse +import json +from typing import List, Dict, Any + + +class URLDecoderService: + """Service class for handling URL decoding operations.""" + + @staticmethod + def decode_signed_urls(encoded_data: str) -> List[Dict[str, Any]]: + """ + Decode URL-encoded signed URLs data. + + Args: + encoded_data: URL-encoded JSON string containing signed URLs + + Returns: + List of decoded URL data dictionaries + + Raises: + ValueError: If decoding or JSON parsing fails + """ + try: + # URL decode the data + decoded_data = urllib.parse.unquote(encoded_data) + + # Parse JSON + parsed_data = json.loads(decoded_data) + + return parsed_data + + except json.JSONDecodeError as e: + raise ValueError(f"Failed to parse JSON: {e}") + + except Exception as e: + raise ValueError(f"Failed to decode data: {e}") diff --git a/src/dataset_file_handler/single_chunk_handler.py b/src/dataset_file_handler/single_chunk_handler.py new file mode 100644 index 00000000..5e8efa9b --- /dev/null +++ b/src/dataset_file_handler/single_chunk_handler.py @@ -0,0 +1,120 @@ +"""Service for handling single chunk downloads from S3.""" + +import json +import os +import tempfile +import traceback +from typing import Dict, Any +from loguru import logger + +from services.s3_ferry_service import S3Ferry + + +class ChunkService: + """Service class for handling single chunk operations.""" + + def __init__(self): + """Initialize the chunk service.""" + self.s3_ferry_service = S3Ferry() + logger.info("ChunkService initialized") + + def download_chunk_from_s3(self, dataset_id: str, page_num: int) -> Dict[str, Any]: + """ + Download a specific chunk from S3 bucket. + + Args: + dataset_id: Dataset ID + page_num: Page number (chunk number) + + Returns: + Dictionary containing chunk data or error information + """ + try: + logger.info( + f"Starting chunk download - Dataset ID: {dataset_id}, Page: {page_num}" + ) + + # Create temporary directory for download + temp_dir = tempfile.mkdtemp(prefix="chunk_download_") + logger.info(f"Created temporary directory: {temp_dir}") + + # Define S3 source path and local destination + chunk_filename = f"{page_num}.json" + s3_source_path = f"{dataset_id}/{chunk_filename}" + local_dest_path = f"temp_chunks/{chunk_filename}" + + # Create the temp_chunks directory if it doesn't exist + temp_chunks_dir = "temp_chunks" + os.makedirs(temp_chunks_dir, exist_ok=True) + logger.info(f"Created/verified temp directory: {temp_chunks_dir}") + + logger.info(f"S3 source path: {s3_source_path}") + logger.info(f"Local destination: {local_dest_path}") + + # Download chunk from S3 using S3Ferry service + response = self.s3_ferry_service.transfer_file( + destination_file_path=local_dest_path, + destination_storage_type="FS", + source_file_path=s3_source_path, + source_storage_type="S3", + ) + + logger.info(f"S3Ferry response status: {response.status_code}") + logger.info(f"S3Ferry response body: {response.text}") + + if response.status_code in [200, 201]: + # Read the downloaded chunk file + local_file_path = f"/app/{local_dest_path}" + + if os.path.exists(local_file_path): + logger.info(f"Successfully downloaded chunk to: {local_file_path}") + + # Read and parse the chunk data + with open(local_file_path, "r", encoding="utf-8") as f: + chunk_data = json.load(f) + + # Clean up the downloaded file + os.remove(local_file_path) + logger.info(f"Cleaned up downloaded file: {local_file_path}") + + # Remove empty directory if it exists + try: + os.rmdir(os.path.dirname(local_file_path)) + except OSError: + pass # Directory not empty or doesn't exist + + return { + "success": True, + "dataset_id": dataset_id, + "page_num": page_num, + "chunk_data": chunk_data, + "message": f"Successfully downloaded chunk {page_num} for dataset {dataset_id}", + } + else: + return { + "success": False, + "dataset_id": dataset_id, + "page_num": page_num, + "error": f"Downloaded file not found at: {local_file_path}", + "message": "File download completed but file not accessible", + } + else: + return { + "success": False, + "dataset_id": dataset_id, + "page_num": page_num, + "error": f"S3 download failed: HTTP {response.status_code}", + "response_body": response.text, + "message": f"Failed to download chunk {page_num} from S3", + } + + except Exception as e: + logger.error(f"Error during chunk download: {str(e)}") + traceback.print_exc() + return { + "success": False, + "dataset_id": dataset_id, + "page_num": page_num, + "error": str(e), + "message": "Internal error during chunk download", + } diff --git a/src/inference/Dockerfile b/src/inference/Dockerfile new file mode 100644 index 00000000..138c0afb --- /dev/null +++ b/src/inference/Dockerfile @@ -0,0 +1,6 @@ +# DOCKERFILE for Triton Python Inference Server including Python environment setup + +FROM nvcr.io/nvidia/tritonserver:25.06-py3 + +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt \ No newline at end of file diff --git a/src/inference/inference_scripts/constants.ini b/src/inference/inference_scripts/constants.ini new file mode 100644 index 00000000..e69de29b diff --git a/src/inference/inference_scripts/deployment_orchestrator.py b/src/inference/inference_scripts/deployment_orchestrator.py new file mode 100644 index 00000000..3f059bcb --- /dev/null +++ b/src/inference/inference_scripts/deployment_orchestrator.py @@ -0,0 +1,790 @@ +#!/usr/bin/env python3 +""" +Global Classifier Model Deployment Script +This script handles model deployment using S3-Ferry service +""" + +# Importing required default libraries +import argparse +import configparser +import sys +import zipfile +import shutil +from pathlib import Path + +import requests +from loki_logger import LokiLogger + +CONSTANTS_INI_FILE_PATH = "/app/inference_scripts/constants.ini" + +# Initialize global logger instance with service name +logger = LokiLogger(service_name="model-deployment-orchestrator") + + +def read_constants_ini(constants_file_path: str = CONSTANTS_INI_FILE_PATH) -> dict: + """Read configuration values from constants.ini file""" + config = configparser.ConfigParser() + + try: + logger.debug(f"Reading constants from {constants_file_path}") + config.read(constants_file_path) + except Exception as e: + logger.error(f"Error reading constants file {constants_file_path}: {e}") + sys.exit(1) + + # Check if file was actually read (configparser.read() doesn't raise exception for missing files) + if not config.sections(): + logger.error(f"Constants file {constants_file_path} not found or is empty") + sys.exit(1) + + if "DSL" not in config: + logger.error(f"No [DSL] section found in {constants_file_path}") + sys.exit(1) + + dsl_section = config["DSL"] + + # Required configuration keys + required_keys = [ + "GLOBAL_CLASSIFIER_S3_FERRY", + "GLOBAL_CLASSIFIER_RUUTER_PRIVATE", + "GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER", + "GLOBAL_CLASSIFIER_PRODUCTION_ENV_MODEL_SERVER", + ] + + logger.debug(f"Loaded DSL section: {[constant for constant in dsl_section]}") + + # Check if all required keys exist + missing_keys = [key for key in required_keys if key not in dsl_section] + if missing_keys: + logger.error( + f"Missing required configuration keys in {constants_file_path}: {missing_keys}" + ) + sys.exit(1) + + constants = { + "s3_ferry_url": dsl_section.get("GLOBAL_CLASSIFIER_S3_FERRY"), + "ruuter_private_url": dsl_section.get("GLOBAL_CLASSIFIER_RUUTER_PRIVATE"), + "triton_test_url": dsl_section.get( + "GLOBAL_CLASSIFIER_TESTING_ENV_MODEL_SERVER" + ), + "triton_prod_url": dsl_section.get( + "GLOBAL_CLASSIFIER_PRODUCTION_ENV_MODEL_SERVER" + ), + } + + logger.info(f"Successfully loaded constants from {constants_file_path}") + return constants + + +class ModelDeploymentOrchestrator: + """Handles model deployment operations using S3-Ferry service""" + + def __init__( + self, + s3_ferry_url: str, + ruuter_private_url: str, + triton_test_url: str, + triton_prod_url: str, + model_id: str, + current_env: str, + target_env: str, + first_deployment: bool, + ): + """Initialize the ModelDeployer with configuration + + Args: + s3_ferry_url: URL for S3-Ferry service + ruuter_private_url: URL for Ruuter private service + triton_test_url: URL for Triton testing environment + triton_prod_url: URL for Triton production environment + model_id: ID of the model to deploy + current_env: Current environment (e.g., 'undeployed') + target_env: Target environment (e.g., 'testing', 'production') + first_deployment: Whether this is the first deployment + """ + self.s3_ferry_url = s3_ferry_url + self.ruuter_private_url = ruuter_private_url + self.triton_test_url = triton_test_url + self.triton_prod_url = triton_prod_url + self.model_id = model_id + self.current_env = current_env + self.target_env = target_env + self.first_deployment = first_deployment + + # Set up directories based on model_id + self.extract_dir = Path( + f"/app/data/temp_extracted_models/model_id_{self.model_id}" + ) + + # S3 Ferry local file path same as above with /app removed since s3 Ferry already adds this from config + self.s3_ferry_local_download_path = ( + f"data/temp_extracted_models/model_id_{self.model_id}" + ) + + # Define model files to upload + self.model_files = [ + f"{self.model_id}-classifier-ensemble/config.pbtxt", + f"{self.model_id}-classifier-ensemble/1/README.txt", + f"{self.model_id}-pre-processing/config.pbtxt", + f"{self.model_id}-pre-processing/1/model.py", + f"{self.model_id}-pre-processing/1/label_mappings.json", + f"{self.model_id}-post-processing/config.pbtxt", + f"{self.model_id}-post-processing/1/model.py", + f"{self.model_id}-post-processing/1/label_mappings.json", + f"{self.model_id}-text-classifier/config.pbtxt", + f"{self.model_id}-text-classifier/1/model.onnx", + ] + + def log_message(self, message: str, level: str = "INFO", **extra_fields): + """Log a message using Loki API with optional extra fields""" + # Add model context to all logs + context = { + "model_id": self.model_id, + "current_env": self.current_env, + "target_env": self.target_env, + "first_deployment": str(self.first_deployment), + **extra_fields, + } + + # Send log to appropriate level + if level == "ERROR": + logger.error(message, **context) + elif level == "WARNING": + logger.warning(message, **context) + elif level == "DEBUG": + logger.debug(message, **context) + else: + logger.info(message, **context) + + def log_error(self, message: str, **extra_fields): + """Log an error message with context""" + self.log_message(message, "ERROR", **extra_fields) + + def call_s3_ferry( + self, source_path: str, source_type: str, dest_path: str, dest_type: str + ) -> bool: + """Call S3-Ferry API for file transfer""" + payload = { + "sourceFilePath": source_path, + "sourceStorageType": source_type, + "destinationFilePath": dest_path, + "destinationStorageType": dest_type, + } + + self.log_message( + f"S3-Ferry transfer: {source_path} ({source_type}) -> {dest_path} ({dest_type})" + ) + + try: + # Make the request with timeout + response = requests.post( + f"{self.s3_ferry_url}/v1/files/copy", + json=payload, + headers={"Content-Type": "application/json"}, + timeout=300, + ) + + if response.status_code in [200, 201]: + self.log_message( + f"S3-Ferry transfer successful (HTTP {response.status_code})" + ) + return True + else: + self.log_error( + f"S3-Ferry transfer failed (HTTP {response.status_code}): {response.text}" + ) + return False + + except requests.exceptions.HTTPError as e: + self.log_error(f"S3-Ferry HTTP error: {e}") + return False + except requests.exceptions.ConnectionError as e: + self.log_error(f"S3-Ferry connection error: {e}") + return False + except requests.exceptions.Timeout as e: + self.log_error(f"S3-Ferry timeout error: {e}") + return False + except requests.exceptions.RequestException as e: + self.log_error(f"S3-Ferry API call failed: {e}") + return False + except Exception as e: + self.log_error(f"Unexpected error during S3-Ferry call: {e}") + return False + + def create_working_directory(self): + """Create the working directory for model extraction""" + try: + self.extract_dir.mkdir(parents=True, exist_ok=True) + self.log_message(f"Created working directory: {self.extract_dir}") + self.log_message( + f"Path sent to S3 Ferry: {self.s3_ferry_local_download_path}" + ) + except OSError as e: + self.log_error(f"Failed to create working directory: {e}") + sys.exit(1) + + def download_model_from_undeployed(self) -> str: + """Download the model zip file from undeployed bucket""" + self.log_message("Step 1: Downloading model from undeployed bucket...") + + model_zip_name = f"{self.model_id}.zip" + local_zip_path = f"{self.s3_ferry_local_download_path}/{model_zip_name}" + s3_source_path = f"models/undeployed/{model_zip_name}" + + self.log_message(f"MODEL S3 LOCATION - {s3_source_path}") + self.log_message(f"MODEL LOCAL PATH - {local_zip_path}") + + if not self.call_s3_ferry(s3_source_path, "S3", local_zip_path, "FS"): + self.log_error("Failed to download model from undeployed bucket") + sys.exit(1) + + return local_zip_path + + def verify_and_extract_model(self, local_zip_path: str): + """Verify the zip file was downloaded and extract it""" + full_zip_path = Path(f"/app/{local_zip_path}") + + # Verify the zip file was downloaded + if not full_zip_path.exists(): + self.log_error(f"Model zip file not found at {full_zip_path}") + sys.exit(1) + + try: + # Get file size for logging + file_size = full_zip_path.stat().st_size + self.log_message( + f"Model zip file downloaded successfully: {file_size} bytes" + ) + except OSError as e: + self.log_error(f"Could not get file stats: {e}") + + # Extract model files + self.log_message("Step 2: Extracting model files...") + try: + with zipfile.ZipFile(full_zip_path, "r") as zip_ref: + zip_ref.extractall(self.extract_dir) + self.log_message("Model files extracted successfully") + except (zipfile.BadZipFile, OSError) as e: + self.log_error(f"Failed to extract model zip file: {e}") + sys.exit(1) + + def upload_model_files(self, environment: str, s3_base_path: str) -> bool: + """Upload files to specified environment""" + self.log_message(f"Uploading model files to {environment} environment...") + + for file in self.model_files: + local_file_path = f"{self.s3_ferry_local_download_path}/{file}" + s3_dest_path = f"{s3_base_path}/{file}" + + if not self.call_s3_ferry(local_file_path, "FS", s3_dest_path, "S3"): + self.log_error(f"Failed to upload {file} to {environment} environment") + return False + + self.log_message( + f"Successfully uploaded all files to {environment} environment" + ) + return True + + def upload_to_model_repository(self): + """Upload model files to both testing and production repositories""" + self.log_message("Step 3: Uploading model files to model repository...") + + # Define S3 destination base paths + s3_testing_base = "models/testing" + s3_production_base = "models/production" + + # Upload to testing environment + if not self.upload_model_files("testing", s3_testing_base): + self.log_error("Failed to upload model to testing environment") + sys.exit(1) + + # Upload to production environment + if not self.upload_model_files("production", s3_production_base): + self.log_error("Failed to upload model to production environment") + sys.exit(1) + + def cleanup_temp_files(self) -> None: + """Clean up temporary files and directories""" + try: + if self.extract_dir.exists(): + shutil.rmtree(self.extract_dir) + self.log_message(f"Cleaned up temporary directory: {self.extract_dir}") + except OSError as e: + self.log_error(f"Failed to clean up temporary files: {e}") + + def get_triton_server_url(self, environment: str) -> str: + """Get the appropriate Triton server URL for the environment""" + if environment == "testing": + return self.triton_test_url + elif environment == "production": + return self.triton_prod_url + else: + raise ValueError(f"Invalid environment: {environment}") + + def get_ensemble_model_names(self) -> list: + """Get all model names for the ensemble (including sub-models)""" + return [ + f"{self.model_id}-classifier-ensemble", + f"{self.model_id}-pre-processing", + f"{self.model_id}-post-processing", + f"{self.model_id}-text-classifier" + ] + + def check_server_health(self, server_url: str) -> bool: + """Check if Triton server is healthy and ready""" + try: + # Check server liveness + live_response = requests.get(f"{server_url}/v2/health/live", timeout=10) + if live_response.status_code != 200: + self.log_error(f"Server not live at {server_url}") + return False + + # Check server readiness + ready_response = requests.get(f"{server_url}/v2/health/ready", timeout=10) + if ready_response.status_code != 200: + self.log_error(f"Server not ready at {server_url}") + return False + + self.log_message(f"Server healthy and ready at {server_url}") + return True + + except requests.exceptions.RequestException as e: + self.log_error(f"Health check failed for {server_url}: {e}") + return False + + def load_model_to_triton(self, server_url: str, model_name: str) -> bool: + """Load a model to Triton server""" + try: + self.log_message(f"Loading model {model_name} to {server_url}") + + response = requests.post( + f"{server_url}/v2/repository/models/{model_name}/load" + ) + + if response.status_code in [200, 201]: + self.log_message(f"Successfully loaded model {model_name}") + return True + else: + self.log_error(f"Failed to load model {model_name}: HTTP {response.status_code} - {response.text}") + return False + + except requests.exceptions.RequestException as e: + self.log_error(f"Error loading model {model_name}: {e}") + return False + + def unload_model_from_triton(self, server_url: str, model_name: str, unload_dependents: bool = False) -> bool: + """Unload a model from Triton server""" + try: + self.log_message(f"Unloading model {model_name} from {server_url}") + + payload = {} + if unload_dependents: + payload["unload_dependents"] = True + + response = requests.post( + f"{server_url}/v2/repository/models/{model_name}/unload", + json=payload if payload else None, + timeout=60 + ) + + if response.status_code in [200, 201]: + self.log_message(f"Successfully unloaded model {model_name}") + return True + else: + self.log_error(f"Failed to unload model {model_name}: HTTP {response.status_code} - {response.text}") + return False + + except requests.exceptions.RequestException as e: + self.log_error(f"Error unloading model {model_name}: {e}") + return False + + def get_loaded_models(self, server_url: str) -> list: + """Get list of currently loaded models from Triton server""" + try: + # Use repository index endpoint with ready=true to get only loaded models + payload = {"ready": True} + response = requests.post(f"{server_url}/v2/repository/index", json=payload, timeout=30) + + if response.status_code == 200: + models_data = response.json() + loaded_models = [] + + # The response is an array of model objects + for model in models_data: + if model.get("state") == "READY": + loaded_models.append(model.get("name")) + + return loaded_models + else: + self.log_error(f"Failed to get models list: HTTP {response.status_code}") + return [] + + except requests.exceptions.RequestException as e: + self.log_error(f"Error getting models list: {e}") + return [] + + def test_model_inference(self, server_url: str, max_retries: int = 10, retry_delay: int = 5) -> bool: + """Test if the ensemble model can perform inference""" + ensemble_model_name = f"{self.model_id}-classifier-ensemble" + + # Simple test input for text classification + test_payload = { + "inputs": [ + { + "name": "TEXT", + "datatype": "BYTES", + "shape": [1,1], + "data": ["This is a test message for classification."] + } + ] + } + + for attempt in range(max_retries): + try: + self.log_message(f"Testing inference for {ensemble_model_name} (attempt {attempt + 1}/{max_retries})") + + response = requests.post( + f"{server_url}/v2/models/{ensemble_model_name}/infer", + json=test_payload, + timeout=30 + ) + + if response.status_code == 200: + self.log_message(f"Inference test successful for {ensemble_model_name}") + return True + else: + self.log_error(f"Inference test failed: HTTP {response.status_code} - {response.text}") + + except requests.exceptions.RequestException as e: + self.log_error(f"Inference test error (attempt {attempt + 1}): {e}") + + if attempt < max_retries - 1: + self.log_message(f"Waiting {retry_delay} seconds before retry...") + import time + time.sleep(retry_delay) + + self.log_error(f"Inference test failed after {max_retries} attempts") + return False + + def unload_existing_models(self, server_url: str) -> bool: + """Unload any existing models with the same model ID""" + model_names = self.get_ensemble_model_names() + loaded_models = self.get_loaded_models(server_url) + + models_to_unload = [model for model in loaded_models if any(model.startswith(f"{self.model_id}-") for model in model_names)] + + if not models_to_unload: + self.log_message("No existing models to unload") + return True + + self.log_message(f"Found existing models to unload: {models_to_unload}") + + # Unload ensemble first (with dependents) to avoid conflicts + ensemble_name = f"{self.model_id}-classifier-ensemble" + if ensemble_name in models_to_unload: + if not self.unload_model_from_triton(server_url, ensemble_name, unload_dependents=True): + return False + models_to_unload.remove(ensemble_name) + + # Unload remaining individual models + for model_name in models_to_unload: + if not self.unload_model_from_triton(server_url, model_name): + return False + + return True + + def load_ensemble_models(self, server_url: str) -> bool: + """Load all models for the ensemble in the correct order""" + model_names = self.get_ensemble_model_names() + + # Load individual models first (dependencies) + individual_models = [name for name in model_names if not name.endswith("-classifier-ensemble")] + ensemble_model = f"{self.model_id}-classifier-ensemble" + + # Load individual models first + for model_name in individual_models: + if not self.load_model_to_triton(server_url, model_name): + self.log_error(f"Failed to load dependency model {model_name}") + return False + + # Load ensemble model last + if not self.load_model_to_triton(server_url, ensemble_model): + self.log_error(f"Failed to load ensemble model {ensemble_model}") + return False + + return True + + def deploy_model(self) -> bool: + """ + Deploy model based on current and target environments. + + Returns: + bool: True if deployment was successful, False otherwise + """ + self.log_message(f"Starting model deployment from {self.current_env} to {self.target_env}") + + # Case 1: undeployed -> undeployed (no-op) + if self.current_env == "undeployed" and self.target_env == "undeployed": + self.log_message("Both current and target environments are 'undeployed'. No deployment needed.") + return True + + # Case 2: testing -> production + elif self.current_env == "testing" and self.target_env == "production": + self.log_message("Deploying from testing to production") + + # Check production server health + prod_url = self.get_triton_server_url("production") + if not self.check_server_health(prod_url): + self.log_error("Production server health check failed") + return False + + # Unload existing models in production + if not self.unload_existing_models(prod_url): + self.log_error("Failed to unload existing models from production") + return False + + # Load models to production + if not self.load_ensemble_models(prod_url): + self.log_error("Failed to load models to production") + return False + + # Test inference + if not self.test_model_inference(prod_url): + self.log_error("Production inference test failed") + return False + + self.log_message("Successfully deployed model from testing to production") + return True + + # Case 3: testing -> undeployed + elif self.current_env == "testing" and self.target_env == "undeployed": + self.log_message("Undeploying model from testing") + + test_url = self.get_triton_server_url("testing") + if not self.check_server_health(test_url): + self.log_error("Testing server health check failed") + return False + + # Unload models from testing + if not self.unload_existing_models(test_url): + self.log_error("Failed to unload models from testing") + return False + + self.log_message("Successfully undeployed model from testing") + return True + + # Case 4: production -> testing + elif self.current_env == "production" and self.target_env == "testing": + self.log_message("Moving model from production to testing") + + # Unload from production + prod_url = self.get_triton_server_url("production") + if not self.check_server_health(prod_url): + self.log_error("Production server health check failed") + return False + + if not self.unload_existing_models(prod_url): + self.log_error("Failed to unload models from production") + return False + + # Load to testing + test_url = self.get_triton_server_url("testing") + if not self.check_server_health(test_url): + self.log_error("Testing server health check failed") + return False + + if not self.unload_existing_models(test_url): + self.log_error("Failed to unload existing models from testing") + return False + + if not self.load_ensemble_models(test_url): + self.log_error("Failed to load models to testing") + return False + + if not self.test_model_inference(test_url): + self.log_error("Testing inference test failed") + return False + + self.log_message("Successfully moved model from production to testing") + return True + + # Case 5: undeployed -> testing + elif self.current_env == "undeployed" and self.target_env == "testing": + self.log_message("Deploying model to testing") + + test_url = self.get_triton_server_url("testing") + if not self.check_server_health(test_url): + self.log_error("Testing server health check failed") + return False + + if not self.unload_existing_models(test_url): + self.log_error("Failed to unload existing models from testing") + return False + + if not self.load_ensemble_models(test_url): + self.log_error("Failed to load models to testing") + return False + + if not self.test_model_inference(test_url): + self.log_error("Testing inference test failed") + return False + + self.log_message("Successfully deployed model to testing") + return True + + # Case 6: undeployed -> production + elif self.current_env == "undeployed" and self.target_env == "production": + self.log_message("Deploying model directly to production") + + prod_url = self.get_triton_server_url("production") + if not self.check_server_health(prod_url): + self.log_error("Production server health check failed") + return False + + if not self.unload_existing_models(prod_url): + self.log_error("Failed to unload existing models from production") + return False + + if not self.load_ensemble_models(prod_url): + self.log_error("Failed to load models to production") + return False + + if not self.test_model_inference(prod_url): + self.log_error("Production inference test failed") + return False + + self.log_message("Successfully deployed model directly to production") + return True + + # Invalid case + else: + self.log_error(f"Invalid deployment path: {self.current_env} -> {self.target_env}") + return False + + def load_model_to_repository(self): + """Main model repository upload process""" + try: + self.log_message( + f"Starting model upload for model ID: {self.model_id}" + ) + + # Step 1: Create working directory + self.create_working_directory() + + # Step 2: Download model from undeployed bucket + local_zip_path = self.download_model_from_undeployed() + + # Step 3: Verify and extract model + self.verify_and_extract_model(local_zip_path) + + # Step 4: Upload to model repository + self.upload_to_model_repository() + + self.log_message( + "Model files uploaded successfully to both environments" + ) + + except KeyboardInterrupt: + self.log_error("Model upload interrupted by user") + sys.exit(1) + except Exception as e: + self.log_error(f"Unexpected error during model upload: {e}") + sys.exit(1) + finally: + # Always attempt cleanup + self.cleanup_temp_files() + + +def main(): + """Main entry point""" + # Read constants from ini file + constants = read_constants_ini() + + parser = argparse.ArgumentParser( + description="Deploy Global Classifier models using S3-Ferry service" + ) + + # Required arguments + parser.add_argument("--model-id", required=True, help="ID of the model to deploy") + parser.add_argument( + "--current-env", required=True, help="Current environment (e.g., 'undeployed')" + ) + parser.add_argument( + "--target-env", + required=True, + help="Target environment (e.g., 'testing', 'production')", + ) + parser.add_argument( + "--first-deployment", + required=True, + choices=["true", "false"], + help="Whether this is the first deployment", + ) + + # Optional arguments with default values from constants.ini + parser.add_argument( + "--s3-ferry-url", + default=constants["s3_ferry_url"], + help=f"URL for S3-Ferry service (default: {constants['s3_ferry_url']})", + ) + parser.add_argument( + "--ruuter-private-url", + default=constants["ruuter_private_url"], + help=f"URL for Ruuter private service (default: {constants['ruuter_private_url']})", + ) + parser.add_argument( + "--triton-test-url", + default=constants["triton_test_url"], + help=f"URL for Triton testing environment (default: {constants['triton_test_url']})", + ) + parser.add_argument( + "--triton-prod-url", + default=constants["triton_prod_url"], + help=f"URL for Triton production environment (default: {constants['triton_prod_url']})", + ) + + try: + args = parser.parse_args() + except SystemExit as e: + logger.error(f"Argument parsing failed with error arparse error code: {e} - Check the INFO logs for more details.") + raise + except Exception as e: + logger.error(f"Unexpected error during argument parsing error code: {e} - Check the INFO logs for more details.") + raise + + # Log all passed arguments for debugging + logger.info(f"Starting deployment with arguments: {vars(args)}", model_id=args.model_id) + + try: + deployer = ModelDeploymentOrchestrator( + s3_ferry_url=args.s3_ferry_url, + ruuter_private_url=args.ruuter_private_url, + triton_test_url=args.triton_test_url, + triton_prod_url=args.triton_prod_url, + model_id=args.model_id, + current_env=args.current_env, + target_env=args.target_env, + first_deployment=args.first_deployment.lower() == "true", + ) + + # Step 1: Upload model files to repository (only on first deployment) + if args.first_deployment.lower() == "true": + logger.info("First deployment detected - uploading model files to repository", model_id=args.model_id) + deployer.load_model_to_repository() + else: + logger.info("Not a first deployment - skipping model repository upload", model_id=args.model_id) + + # Step 2: Deploy model to Triton inference servers + if not deployer.deploy_model(): + logger.error("Model deployment to Triton servers failed", model_id=args.model_id) + sys.exit(1) + + logger.info("Model deployment completed successfully", model_id=args.model_id) + + except Exception as e: + logger.error(f"Fatal error: {e}", model_id=getattr(args, 'model_id', None)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/inference/inference_scripts/loki_logger.py b/src/inference/inference_scripts/loki_logger.py new file mode 100644 index 00000000..e69de29b diff --git a/src/inference/inference_scripts/requirements.txt b/src/inference/inference_scripts/requirements.txt new file mode 100644 index 00000000..e0b10732 --- /dev/null +++ b/src/inference/inference_scripts/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.32.0 +loguru>=0.7.2 diff --git a/src/inference/model-repository/README.md b/src/inference/model-repository/README.md new file mode 100644 index 00000000..e91aba72 --- /dev/null +++ b/src/inference/model-repository/README.md @@ -0,0 +1,2 @@ +### Model repository template +This is is a model repository template that will be copied to the S3 compitable storage medium so that when the triton server containers (production and testing) starts all the necessary model-repository paths are configured and available \ No newline at end of file diff --git a/src/inference/model-repository/production/dummy_model/1/model.py b/src/inference/model-repository/production/dummy_model/1/model.py new file mode 100644 index 00000000..dfef8149 --- /dev/null +++ b/src/inference/model-repository/production/dummy_model/1/model.py @@ -0,0 +1,27 @@ +import numpy as np +import triton_python_backend_utils as pb_utils + + +class TritonPythonModel: + """ + Dummy model that returns a static payload. + This model is used to ensure the Triton server starts without errors. + """ + + def initialize(self, args): + """Initialize the dummy model.""" + self.logger = pb_utils.Logger + self.logger.log_info("Dummy model initialized") + + def execute(self, requests): + """Return a static response for each request.""" + responses = [] + for request in requests: + # Create a dummy output tensor with static values + output_tensor = pb_utils.Tensor.from_numpy( + "text_output", np.array([["Hello, world!"]], dtype=object) + ) + response = pb_utils.InferenceResponse(output_tensors=[output_tensor]) + responses.append(response) + + return responses diff --git a/src/inference/model-repository/production/dummy_model/README.md b/src/inference/model-repository/production/dummy_model/README.md new file mode 100644 index 00000000..283749a5 --- /dev/null +++ b/src/inference/model-repository/production/dummy_model/README.md @@ -0,0 +1,4 @@ +### Dummy Model +This is a dummy model with a config.pbtxt and model.py that returns a static payload everytime. This dummy model just exists in the model repository of both the testing and production servers so that when the containers start the triton servers have a reference config.pbtxt that doesn't cause them to fail. + +Under no circumstances should this model be deployed \ No newline at end of file diff --git a/src/inference/model-repository/production/dummy_model/config.pbtxt b/src/inference/model-repository/production/dummy_model/config.pbtxt new file mode 100644 index 00000000..303641f7 --- /dev/null +++ b/src/inference/model-repository/production/dummy_model/config.pbtxt @@ -0,0 +1,17 @@ +name: "dummy-model" +backend: "python" +max_batch_size: 0 +input [ + { + name: "text_input" + data_type: TYPE_STRING + dims: [1] + } +] +output [ + { + name: "text_output" + data_type: TYPE_STRING + dims: [ 1 ] + } +] \ No newline at end of file diff --git a/src/inference/model-repository/testing/dummy_model/1/model.py b/src/inference/model-repository/testing/dummy_model/1/model.py new file mode 100644 index 00000000..dfef8149 --- /dev/null +++ b/src/inference/model-repository/testing/dummy_model/1/model.py @@ -0,0 +1,27 @@ +import numpy as np +import triton_python_backend_utils as pb_utils + + +class TritonPythonModel: + """ + Dummy model that returns a static payload. + This model is used to ensure the Triton server starts without errors. + """ + + def initialize(self, args): + """Initialize the dummy model.""" + self.logger = pb_utils.Logger + self.logger.log_info("Dummy model initialized") + + def execute(self, requests): + """Return a static response for each request.""" + responses = [] + for request in requests: + # Create a dummy output tensor with static values + output_tensor = pb_utils.Tensor.from_numpy( + "text_output", np.array([["Hello, world!"]], dtype=object) + ) + response = pb_utils.InferenceResponse(output_tensors=[output_tensor]) + responses.append(response) + + return responses diff --git a/src/inference/model-repository/testing/dummy_model/README.md b/src/inference/model-repository/testing/dummy_model/README.md new file mode 100644 index 00000000..283749a5 --- /dev/null +++ b/src/inference/model-repository/testing/dummy_model/README.md @@ -0,0 +1,4 @@ +### Dummy Model +This is a dummy model with a config.pbtxt and model.py that returns a static payload everytime. This dummy model just exists in the model repository of both the testing and production servers so that when the containers start the triton servers have a reference config.pbtxt that doesn't cause them to fail. + +Under no circumstances should this model be deployed \ No newline at end of file diff --git a/src/inference/model-repository/testing/dummy_model/config.pbtxt b/src/inference/model-repository/testing/dummy_model/config.pbtxt new file mode 100644 index 00000000..303641f7 --- /dev/null +++ b/src/inference/model-repository/testing/dummy_model/config.pbtxt @@ -0,0 +1,17 @@ +name: "dummy-model" +backend: "python" +max_batch_size: 0 +input [ + { + name: "text_input" + data_type: TYPE_STRING + dims: [1] + } +] +output [ + { + name: "text_output" + data_type: TYPE_STRING + dims: [ 1 ] + } +] \ No newline at end of file diff --git a/src/inference/model-repository/undeployed/dummy_model.zip b/src/inference/model-repository/undeployed/dummy_model.zip new file mode 100644 index 00000000..e3b785bc Binary files /dev/null and b/src/inference/model-repository/undeployed/dummy_model.zip differ diff --git a/src/inference/requirements.txt b/src/inference/requirements.txt new file mode 100644 index 00000000..adfc6f59 --- /dev/null +++ b/src/inference/requirements.txt @@ -0,0 +1,2 @@ +transformers==4.53.3 +scipy==1.16.0 \ No newline at end of file diff --git a/src/model-training/Dockerfile.cpu b/src/model-training/Dockerfile.cpu new file mode 100644 index 00000000..a3742640 --- /dev/null +++ b/src/model-training/Dockerfile.cpu @@ -0,0 +1,57 @@ +FROM python:3.11-slim + +ENV PYTHONUNBUFFERED=1 \ + DEBIAN_FRONTEND=noninteractive \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Install system dependencies +RUN apt-get update && \ + apt-get install -y \ + curl \ + git \ + unzip \ + wget \ + build-essential \ + gcc \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy requirements first for better Docker layer caching +COPY requirements-cpu.txt /app/ + +# Upgrade pip and install Python dependencies +RUN pip install --upgrade pip + +# Install CPU-specific PyTorch and dependencies +RUN pip install --no-cache-dir -r requirements-cpu.txt --timeout=1000 + +# Copy the rest of the application +COPY . /app + +# Create required directories +RUN mkdir -p /shared /cache /app/outputs /app/model_trainer + +# Create non-root user for security +RUN useradd --create-home --shell /bin/bash app \ + && chown -R app:app /app /shared /cache +USER root + +# Expose the port the FastAPI app will run on +EXPOSE 8900 + +# Set environment variables +ENV HF_HOME=/cache/ \ + PYTHONPATH=/app \ + TORCH_HOME=/cache/torch \ + TRANSFORMERS_CACHE=/cache/transformers + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8900/model_checker/ || exit 1 + +# Set the entry point to run the FastAPI server +CMD ["uvicorn", "model_trainer_api:app", "--host", "0.0.0.0", "--port", "8900"] \ No newline at end of file diff --git a/src/model-training/Dockerfile.gpu b/src/model-training/Dockerfile.gpu new file mode 100644 index 00000000..b3d86c51 --- /dev/null +++ b/src/model-training/Dockerfile.gpu @@ -0,0 +1,77 @@ +FROM nvidia/cuda:12.1-devel-ubuntu20.04 + +ENV PYTHONUNBUFFERED=1 \ + DEBIAN_FRONTEND=noninteractive \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Install system dependencies +RUN apt-get update && \ + apt-get install -y \ + software-properties-common && \ + add-apt-repository ppa:deadsnakes/ppa && \ + apt-get update && \ + apt-get install -y \ + curl \ + git \ + python3.11 \ + python3.11-dev \ + python3.11-distutils \ + unzip \ + wget \ + build-essential \ + gcc \ + g++ \ + nvidia-cuda-toolkit \ + && rm -rf /var/lib/apt/lists/* + +# Install pip for Python 3.11 +RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 + +# Set Python 3.11 as the default python3 +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 +RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.11 1 + +# Set working directory +WORKDIR /app + +# Copy requirements first for better Docker layer caching +COPY requirements-gpu.txt /app/ + +# Upgrade pip and install GPU-specific dependencies +RUN python3.11 -m pip install --upgrade pip + +# Install GPU-specific PyTorch and dependencies +RUN python3.11 -m pip install --no-cache-dir -r requirements-gpu.txt --timeout=2000 + +# Copy the rest of the application +COPY . /app + +# Create required directories +RUN mkdir -p /shared /cache /app/outputs /app/model_trainer + +# Create non-root user for security (but allow GPU access) +RUN useradd --create-home --shell /bin/bash app \ + && usermod -a -G video app \ + && chown -R app:app /app /shared /cache +USER app + +# Expose the port the FastAPI app will run on +EXPOSE 8900 + +# Set environment variables for GPU optimization +ENV HF_HOME=/cache/ \ + PYTHONPATH=/app \ + TORCH_HOME=/cache/torch \ + TRANSFORMERS_CACHE=/cache/transformers \ + CUDA_VISIBLE_DEVICES=0 \ + NVIDIA_VISIBLE_DEVICES=all \ + NVIDIA_DRIVER_CAPABILITIES=compute,utility \ + TORCH_CUDA_ARCH_LIST="6.0 6.1 7.0 7.5 8.0 8.6+PTX" + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \ + CMD curl -f http://localhost:8900/model_checker/ || exit 1 + +# Set the entry point to run the FastAPI server +CMD ["uvicorn", "model_trainer_api:app", "--host", "0.0.0.0", "--port", "8900"] \ No newline at end of file diff --git a/src/model-training/constants.py b/src/model-training/constants.py new file mode 100644 index 00000000..c0ce3a16 --- /dev/null +++ b/src/model-training/constants.py @@ -0,0 +1,149 @@ +UPDATE_MODEL_TRAINING_STATUS_ENDPOINT = "http://ruuter-public:8086/global-classifier/datamodels/training/status/update" + +CREATE_TRAINING_PROGRESS_SESSION_ENDPOINT = "http://ruuter-public:8086/global-classifier/datamodels/progress/create" + +UPDATE_TRAINING_PROGRESS_SESSION_ENDPOINT = "http://ruuter-public:8086/global-classifier/datamodels/progress/update" + +DEPLOYMENT_ENDPOINT = "http://ruuter-public:8086/global-classifier/inference/deploy" + +MODEL_TRAINING_SOURCE_PATH = "/app/src/training" #path in container + +TRAINING_LOGS_PATH = "/app/src/training/training_logs.log" + +MODEL_RESULTS_PATH = "/app/shared/model_trainer/results" # stored in the shared folder which is connected to s3-ferry + +LOCAL_BASEMODEL_TRAINED_LAYERS_SAVE_PATH = "/app/shared/model_trainer/results/{model_id}/trained_base_model_layers" # stored in the shared folder which is connected to s3-ferry + +LOCAL_CLASSIFICATION_LAYER_SAVE_PATH = "/app/shared/model_trainer/results/{model_id}/classifier_layers" # stored in the shared folder which is connected to s3-ferry + +LOCAL_LABEL_ENCODER_SAVE_PATH = "/app/shared/model_trainer/results/{model_id}/label_encoders" # stored in the shared folder which is connected to s3-ferry + +S3_FERRY_MODEL_STORAGE_PATH = "/models/undeployed" # folder path in s3 bucket + +S3_FERRY_ENDPOINT = "http://gc-s3-ferry:3000/v1/files/copy" + +BASE_MODEL_FILENAME = "base_model_trainable_layers_{model_id}" + +CLASSIFIER_MODEL_FILENAME = "classifier_{model_id}.pth" + + +# MODEL TRAINING PROGRESS SESSION CONSTANTS + +INITIATING_TRAINING_PROGRESS_STATUS = "Initiating Training" + +TRAINING_IN_PROGRESS_PROGRESS_STATUS = "Training In-Progress" + +DEPLOYING_MODEL_PROGRESS_STATUS = "Deploying Model" + +MODEL_TRAINED_AND_DEPLOYED_PROGRESS_STATUS = "Model Trained And Deployed" + +TRAINING_FAILED_STATUS= "Training Failed" + +DEPLOYMENT_FAILED_STATUS = "Deployment Failed" + + +INITIATING_TRAINING_PROGRESS_MESSAGE = "Download and preparing dataset" + +TRAINING_IN_PROGRESS_PROGRESS_MESSAGE = "The dataset is being trained on all selected models" + + +DEPLOYING_MODEL_PROGRESS_MESSAGE = "Model training complete. The trained model is now being deployed" + +MODEL_TRAINED_AND_DEPLOYED_PROGRESS_MESSAGE = "The model was trained and deployed successfully to the environment" + + +TRAINING_FAILED_STATUS_MESSAGE = "Model training has failed" + + +INITIATING_TRAINING_PROGRESS_PERCENTAGE = 30 + +TRAINING_IN_PROGRESS_PROGRESS_PERCENTAGE = 50 + +DEPLOYING_MODEL_PROGRESS_PERCENTAGE = 80 + +MODEL_TRAINED_AND_DEPLOYED_PROGRESS_PERCENTAGE = 100 + +TRAINING_FAILED_PROGRESS_PERCENTAGE = 100 + + +# Supported Models for Testing +SUPPORTED_BASE_MODELS = ["estbert", "xlm-roberta", "multilingual-distilbert"] + + +SUPPORTED_BASE_MODELS = ["estbert", "xlm-roberta", "multilingual-distilbert"] + +# Model configurations +MODEL_CONFIGS = { + "estbert": { + "model_name": "tartuNLP/EstBERT", + "tokenizer_name": "tartuNLP/EstBERT", + "type": "bert", + }, + "xlm-roberta": { + "model_name": "xlm-roberta-base", + "tokenizer_name": "xlm-roberta-base", + "type": "roberta", + }, + "multilingual-distilbert": { + "model_name": "distilbert-base-multilingual-cased", + "tokenizer_name": "distilbert-base-multilingual-cased", + "type": "distilbert", + }, +} + +SEQUENCE_LENGTH = 128 + +# OOD Training configurations +SUPPORTED_OOD_METHODS = ["energy", "sngp", "softmax"] + +# OOD Default parameters +DEFAULT_OOD_CONFIGS = { + "energy": { + "energy_temp": 1.0, + "energy_margin": 10.0, + "energy_weight": 0.1, + "use_energy_loss": True, + }, + "sngp": { + "spec_norm_bound": 0.9, + "gp_hidden_dim": 128, + "gp_scale": 2.0, + "gp_bias": 0.0, + "gp_input_normalization": True, + "gp_random_feature_type": "orf", + "gp_cov_momentum": 0.999, + "gp_cov_ridge_penalty": 1e-3, + }, + "softmax": {"temperature": 1.0, "use_entropy": True, "calibrate": False}, +} +UNCERTAINTY_CONFIGS = { + "uncertainty_strategy": "sngp", + "confidence_scaling": False, + "human_handoff_threshold": 0.8, +} + + +# Training parameters +DEFAULT_TRAINING_ARGS = { + "num_train_epochs": 4, + "per_device_train_batch_size": 8, + "per_device_eval_batch_size": 8, + "learning_rate": 2e-5, + "warmup_steps": 100, + "weight_decay": 0.01, + "logging_steps": 50, + "eval_strategy": "epoch", + "save_strategy": "epoch", + "load_best_model_at_end": True, + "metric_for_best_model": "accuracy", +} + + +# Data pipeline constants +MIN_SAMPLES_PER_CLASS = 10 +TARGET_SAMPLES_FOR_SMALL_DATASETS = 50 +TEST_SIZE_RATIO = 0.2 + +# Model evaluation constants +ACCURACY_WEIGHT = 0.7 +F1_WEIGHT = 0.3 diff --git a/src/model-training/create_triton_configs.py b/src/model-training/create_triton_configs.py new file mode 100644 index 00000000..476f92dd --- /dev/null +++ b/src/model-training/create_triton_configs.py @@ -0,0 +1,503 @@ +def generate_ensemble_config( + model_name: str, + model_type: str, + sequence_length: int = 512, + max_batch_size: int = 32, + pre_processing_name: str = "pre_processing", + text_classifier_name: str = "text_classifier", + post_processing_name: str = "post_processing", + is_sngp: bool = False, +) -> str: + """ + Generate Triton ensemble config based on model type. + + Args: + model_name: Name of the ensemble model (e.g., "classifier_ensemble") + model_type: Type of model ("distilbert", "bert", "xlm-roberta", "roberta") + sequence_length: Maximum sequence length for the model + max_batch_size: Maximum batch size for inference + pre_processing_name: Name of the preprocessing model + text_classifier_name: Name of the text classifier model + post_processing_name: Name of the postprocessing model + is_sngp: Whether this is an SNGP model (affects outputs) + + Returns: + str: Complete Triton ensemble config as string + """ + + # Define which models support token_type_ids + models_with_token_type_ids = ["bert", "xlm-roberta", "roberta", "estbert"] + supports_token_type_ids = model_type.lower() in models_with_token_type_ids + + # Base config template + config = f"""name: "{model_name}" +platform: "ensemble" +max_batch_size: {max_batch_size} +input [ + {{ + name: "TEXT" + data_type: TYPE_STRING + dims: [-1] + }}, + {{ + name: "top_k" + data_type: TYPE_INT32 + dims: [ 1 ] + optional: true + }} +] +output [ + {{ + name: "TOP_K_PREDICTIONS" + data_type: TYPE_STRING + dims: [ -1 ] + }} +] +ensemble_scheduling {{ + step [ + {{ + model_name: "{pre_processing_name}" + model_version: -1 + input_map {{ + key: "TEXT" + value: "TEXT" + }} + output_map {{ + key: "input_ids" + value: "input_ids" + }} + output_map {{ + key: "attention_mask" + value: "attention_mask" + }}""" + + # Add token_type_ids output mapping if supported + if supports_token_type_ids: + config += """ + output_map { + key: "token_type_ids" + value: "token_type_ids" + }""" + + config += f""" + }}, + {{ + model_name: "{text_classifier_name}" + model_version: -1 + input_map {{ + key: "input_ids" + value: "input_ids" + }} + input_map {{ + key: "attention_mask" + value: "attention_mask" + }}""" + + # Add token_type_ids input mapping if supported + if supports_token_type_ids: + config += """ + input_map { + key: "token_type_ids" + value: "token_type_ids" + }""" + + config += f""" + output_map {{ + key: "logits" + value: "logits" + }} + + }}, + {{ + model_name: "{post_processing_name}" + model_version: -1 + input_map {{ + key: "logits" + value: "logits" + }} + input_map {{ + key: "top_k" + value: "top_k" + }} + output_map {{ + key: "TOP_K_PREDICTIONS" + value: "TOP_K_PREDICTIONS" + }} + }} + ] +}}""" + + return config + + +def generate_preprocessing_config( + model_name: str, + model_type: str, + sequence_length: int = 512, + max_batch_size: int = 32, + ood_method: str = None, + base_model_type: str = None, +) -> str: + """ + Generate Triton preprocessing config based on model type. + + Args: + model_name: Name of the preprocessing model (e.g., "pre_processing") + model_type: Type of model ("distilbert", "bert", "xlm-roberta", "roberta") + sequence_length: Maximum sequence length for the model + max_batch_size: Maximum batch size for inference + ood_method: OOD method ("sngp", "energy", "softmax", or None) + base_model_type: Base model type for parameter passing + + Returns: + str: Complete Triton preprocessing config as string + """ + + # Define which models support token_type_ids + models_with_token_type_ids = ["bert", "xlm-roberta", "roberta", "estbert"] + supports_token_type_ids = model_type.lower() in models_with_token_type_ids + + config = f"""name: "{model_name}" +backend: "python" +max_batch_size: {max_batch_size} + +input [ + {{ + name: "TEXT" + data_type: TYPE_STRING + dims: [ -1 ] + }} +] + +output [ + {{ + name: "input_ids" + data_type: TYPE_INT64 + dims: [ -1 ] + }}, + {{ + name: "attention_mask" + data_type: TYPE_INT64 + dims: [ -1 ] + }}, + """ + if supports_token_type_ids: + config += f"""{{ + name: "token_type_ids" + data_type: TYPE_INT64 + dims: [ -1 ] + }}""" + + # Note: No training flag needed for current SNGP implementation + + config += f""" +] + + + +parameters [ + {{ + key: "model_name" + value: {{ + string_value: "{model_name}" + }} + }}, + {{ + key: "sequence_length" + value: {{ + string_value: "{sequence_length}" + }} + }}, + {{ + key: "ood_method" + value: {{ + string_value: "{ood_method if ood_method else 'none'}" + }} + }}, + {{ + key: "base_model_type" + value: {{ + string_value: "{base_model_type if base_model_type else model_type}" + }} + }} +]""" + + return config + + +def generate_text_classifier_config( + model_name: str, + model_type: str, + num_labels: int, + sequence_length: int = 512, + max_batch_size: int = 32, + ood_method: str = None, +) -> str: + """ + Generate Triton text classifier config based on model type. + + Args: + model_name: Name of the text classifier model (e.g., "text_classifier") + model_type: Type of model ("distilbert", "bert", "xlm-roberta", "roberta") + num_labels: Number of output labels/classes + sequence_length: Maximum sequence length for the model + max_batch_size: Maximum batch size for inference + ood_method: OOD method ("sngp", "energy", "softmax", or None) + + Returns: + str: Complete Triton text classifier config as string + """ + + # Define which models support token_type_ids + models_with_token_type_ids = ["bert", "xlm-roberta", "roberta", "estbert"] + supports_token_type_ids = model_type.lower() in models_with_token_type_ids + + config = f"""name: "{model_name}" +platform: "onnxruntime_onnx" +max_batch_size: {max_batch_size} + +input [ + {{ + name: "input_ids" + data_type: TYPE_INT64 + dims: [ -1 ] + }}, + {{ + name: "attention_mask" + data_type: TYPE_INT64 + dims: [ -1 ] + }}""" + + # Add token_type_ids input if supported + if supports_token_type_ids: + config += f""", + {{ + name: "token_type_ids" + data_type: TYPE_INT64 + dims: [ -1 ] + }}""" + + # Note: No training flag needed for current SNGP implementation + + config += f""" +] + +output [ + {{ + name: "logits" + data_type: TYPE_FP32 + dims: [ {num_labels} ] + }}""" + + config += f""" +] + + +dynamic_batching {{ + max_queue_delay_microseconds: 100 +}}""" + + return config + + +def generate_postprocessing_config( + model_name: str, + num_labels: int, + max_batch_size: int = 32, + ood_method: str = None, + ood_threshold: float = 0.5, + uncertainty_threshold: float = 0.8, + human_handoff_threshold: float = 0.8, + uncertainty_strategy: str = None, + energy_temp: float = 1.0, + softmax_temp: float = 1.0, + confidence_scaling: bool = False, +) -> str: + """ + Generate Triton postprocessing config matching your existing implementation. + + Args: + model_name: Name of the postprocessing model (e.g., "post_processing") + num_labels: Number of output labels/classes + max_batch_size: Maximum batch size for inference + ood_method: OOD method ("sngp", "energy", "softmax", or None) + ood_threshold: Threshold for OOD detection + uncertainty_threshold: Threshold for uncertainty-based decisions + human_handoff_threshold: Threshold for human handoff decisions + uncertainty_strategy: Strategy for handling uncertainty ("inject_class", "confidence_scaling", "threshold_filter", or None) + energy_temp: Temperature for energy-based OOD detection + softmax_temp: Temperature for softmax-based OOD detection + confidence_scaling: Whether to enable confidence scaling + + Returns: + str: Complete Triton postprocessing config as string + """ + + config = f"""name: "{model_name}" +backend: "python" +max_batch_size: {max_batch_size} + +input [ + {{ + name: "logits" + data_type: TYPE_FP32 + dims: [ {num_labels} ] + }}, + {{ + name: "top_k" + data_type: TYPE_INT32 + dims: [ 1 ] + optional: true + }} +]""" + + config += f""" + + +output [ + {{ + name: "TOP_K_PREDICTIONS" + data_type: TYPE_STRING + dims: [ -1 ] + }} +] + + + +parameters [ + {{ + key: "ood_method" + value: {{ + string_value: "{ood_method if ood_method else 'none'}" + }} + }}, + {{ + key: "ood_threshold" + value: {{ + string_value: "{ood_threshold}" + }} + }}, + {{ + key: "energy_temp" + value: {{ + string_value: "{energy_temp}" + }} + }}, + {{ + key: "softmax_temp" + value: {{ + string_value: "{softmax_temp}" + }} + }}, + {{ + key: "uncertainty_strategy" + value: {{ + string_value: "{uncertainty_strategy if uncertainty_strategy else 'none'}" + }} + }}, + {{ + key: "human_handoff_threshold" + value: {{ + string_value: "{human_handoff_threshold}" + }} + }}, + {{ + key: "confidence_scaling" + value: {{ + string_value: "{str(confidence_scaling).lower()}" + }} + }} +]""" + + return config + + +def generate_all_triton_configs( + model_id: str, + model_type: str, + num_labels: int, + sequence_length: int = 512, + max_batch_size: int = 32, + ood_method: str = None, + ood_threshold: float = 0.5, + uncertainty_threshold: float = 0.8, + human_handoff_threshold: float = 0.8, + uncertainty_strategy: str = None, + energy_temp: float = 1.0, + softmax_temp: float = 1.0, + confidence_scaling: bool = False, + base_model_type: str = None, +) -> dict: + """ + Generate all Triton config files for a complete pipeline matching your implementation. + + Args: + model_id: Unique identifier for the model (used in naming) + model_type: Type of model ("distilbert", "bert", "xlm-roberta", "roberta") + num_labels: Number of output labels/classes + sequence_length: Maximum sequence length for the model + max_batch_size: Maximum batch size for inference + ood_method: OOD method ("sngp", "energy", "softmax", or None) + ood_threshold: Threshold for OOD detection + uncertainty_threshold: Threshold for uncertainty-based decisions + human_handoff_threshold: Threshold for human handoff decisions + uncertainty_strategy: Strategy for handling uncertainty + energy_temp: Temperature for energy-based OOD detection + softmax_temp: Temperature for softmax-based OOD detection + confidence_scaling: Whether to enable confidence scaling + base_model_type: Base model type for parameter passing + + Returns: + dict: Dictionary containing all config files with their names as keys + """ + + # Generate model names with model_id prefix + ensemble_name = f"{model_id}-classifier-ensemble" + preprocessing_name = f"{model_id}-pre-processing" + text_classifier_name = f"{model_id}-text-classifier" + postprocessing_name = f"{model_id}-post-processing" + + configs = { + f"{ensemble_name}/config.pbtxt": generate_ensemble_config( + model_name=ensemble_name, + model_type=model_type, + sequence_length=sequence_length, + max_batch_size=max_batch_size, + pre_processing_name=preprocessing_name, + text_classifier_name=text_classifier_name, + post_processing_name=postprocessing_name, + ), + f"{preprocessing_name}/config.pbtxt": generate_preprocessing_config( + model_name=preprocessing_name, + model_type=model_type, + sequence_length=sequence_length, + max_batch_size=max_batch_size, + ood_method=ood_method, + base_model_type=base_model_type, + ), + f"{text_classifier_name}/config.pbtxt": generate_text_classifier_config( + model_name=text_classifier_name, + model_type=model_type, + num_labels=num_labels, + sequence_length=sequence_length, + max_batch_size=max_batch_size, + ood_method=ood_method, + ), + f"{postprocessing_name}/config.pbtxt": generate_postprocessing_config( + model_name=postprocessing_name, + num_labels=num_labels, + max_batch_size=max_batch_size, + ood_method=ood_method, + ood_threshold=ood_threshold, + uncertainty_threshold=uncertainty_threshold, + human_handoff_threshold=human_handoff_threshold, + uncertainty_strategy=uncertainty_strategy, + energy_temp=energy_temp, + softmax_temp=softmax_temp, + confidence_scaling=confidence_scaling, + ), + } + + return configs + + diff --git a/src/model-training/datapipeline.py b/src/model-training/datapipeline.py new file mode 100644 index 00000000..fdb1ae3e --- /dev/null +++ b/src/model-training/datapipeline.py @@ -0,0 +1,268 @@ +import pandas as pd +from loguru import logger +import sys +from s3_ferry import S3Ferry +import os + +from loki_logger import LokiLogger +logger = LokiLogger(service_name="model-trainer") + + + +class DataPipeline: + def __init__(self, dataset_id): + logger.info(f"DOWNLOADING DATASET WITH Dataset - {dataset_id}") + + # Ensure the datasets directory exists + datasets_dir = "/app/shared/datasets" + os.makedirs(datasets_dir, exist_ok=True) + # Download dataset + s3_ferry = S3Ferry() + s3_ferry.transfer_file( + destination_file_path=f"/shared/datasets/dataset_{dataset_id}.csv", + destination_storage_type="FS", + source_file_path=f"datasets/{dataset_id}/aggregated_dataset.csv", + source_storage_type="S3", + ) + + # Load CSV directly after download + csv_path = f"/app/shared/datasets/dataset_{dataset_id}.csv" + try: + df = pd.read_csv(csv_path) + except Exception as e: + logger.error(f"Failed to read CSV file: {csv_path}") + raise RuntimeError(f"Could not read CSV: {e}") + + # Remove rowId if it exists (not expected in your CSV, but kept for compatibility) + if "rowId" in df.columns: + df = df.drop("rowId", axis=1) + + self.df = df + logger.info(f"Downloaded dataset with {len(df)} samples") + logger.info(f"Dataset columns: {list(df.columns)}") + + # Set up hierarchy metadata manually for flat classification + # agency_name is target, data_item is input + self.hierarchy = { + "name": f"Dataset {dataset_id}", + "validationCriteria": { + "validationRules": { + "data_item": {"isDataClass": False}, + "agency_name": {"isDataClass": True}, + } + }, + } + + def extract_input_columns(self): + """Extract input columns from validation rules""" + validation_rules = self.hierarchy["validationCriteria"]["validationRules"] + input_columns: list[str | Unknown] = [ + key for key, value in validation_rules.items() if not value["isDataClass"] + ] + logger.info(f"Input columns identified: {input_columns}") + return input_columns + + def extract_target_column(self): + """Extract the target column from validation rules""" + validation_rules = self.hierarchy["validationCriteria"]["validationRules"] + target_columns = [ + key for key, value in validation_rules.items() if value["isDataClass"] + ] + + if not target_columns: + logger.error("No target column found in validation rules") + raise ValueError("No target column found in validation rules") + + target_column = target_columns[0] + logger.info(f"Target column identified: {target_column}") + return target_column + + def models_and_filters(self): + """ + Create models and filters for flat classification. + This maintains compatibility with the original hierarchical interface + but returns simplified structures for flat classification. + """ + target_column = self.extract_target_column() + unique_classes = sorted(self.df[target_column].unique().tolist()) + + models = [{1: unique_classes}] + filters = [unique_classes] + + logger.info("Flat classification setup:") + logger.info(f" Target column: {target_column}") + logger.info(f" Classes ({len(unique_classes)}): {unique_classes}") + logger.info( + f" Class distribution: {self.df[target_column].value_counts().to_dict()}" + ) + + return models, filters + + def create_dataframes(self): + """Create dataframes for flat classification""" + logger.info("CREATING DATAFRAME FOR FLAT CLASSIFICATION") + + try: + input_columns = self.extract_input_columns() + target_column = self.extract_target_column() + + df = self.df.copy() + + # Create input text by combining input columns + if len(input_columns) == 1: + # Single input column - use directly + input_col = input_columns[0] + df["input"] = df[input_col].astype(str) + logger.info(f"Using single input column: {input_col}") + else: + # Multiple input columns - combine them + df["input"] = df[input_columns].apply( + lambda row: " ".join(row.dropna().astype(str)), axis=1 + ) + logger.info(f"Combined input columns: {input_columns}") + + # Set target column + df = df.rename(columns={target_column: "target"}) + + logger.info(f"Data frame before removing: {df}") + # Keep only input and target columns, remove any NaN values + df = df[["input", "target", "agency_id"]].dropna() + + logger.info(f"Data frame after removing: {df}") + + # Validate the data + if len(df) == 0: + raise ValueError("No valid data samples after preprocessing") + + # Check for empty inputs + empty_inputs = df[df["input"].str.strip() == ""].shape[0] + if empty_inputs > 0: + logger.warning( + f"Found {empty_inputs} empty input samples, removing them" + ) + df = df[df["input"].str.strip() != ""] + + # Final validation + if len(df) == 0: + raise ValueError("No valid data samples after cleaning") + + unique_classes = df["target"].unique() + class_counts = df["target"].value_counts() + + logger.info("DATAFRAME CREATED SUCCESSFULLY:") + logger.info(f" Total samples: {len(df)}") + logger.info(f" Unique classes: {len(unique_classes)}") + logger.info(f" Class distribution: {class_counts.to_dict()}") + logger.info( + f" Sample input (first 100 chars): {df['input'].iloc[0][:100]}..." + ) + + # Check for class imbalance + min_class_size = class_counts.min() + max_class_size = class_counts.max() + + if min_class_size < 5: + logger.warning( + f"Some classes have very few samples (min: {min_class_size})" + ) + + if max_class_size / min_class_size > 10: + logger.warning( + f"Significant class imbalance detected (ratio: {max_class_size / min_class_size:.1f})" + ) + + return [ + df + ] # Return as list for compatibility with multi-dataframe training + + except Exception as e: + logger.error(f"ERROR CREATING DATAFRAME: {e}") + logger.error(f"Available columns: {list(self.df.columns)}") + logger.error(f"DataFrame shape: {self.df.shape}") + logger.error(f"DataFrame info: {self.df.info()}") + raise + + def get_data_statistics(self): + """Get detailed statistics about the dataset""" + try: + target_column = self.extract_target_column() + input_columns = self.extract_input_columns() + + stats = { + "total_samples": len(self.df), + "input_columns": input_columns, + "target_column": target_column, + "unique_classes": self.df[target_column].unique().tolist(), + "class_distribution": self.df[target_column].value_counts().to_dict(), + "missing_values": self.df.isnull().sum().to_dict(), + "data_types": self.df.dtypes.to_dict(), + } + + # Add text statistics for input columns + for col in input_columns: + if col in self.df.columns: + text_lengths = self.df[col].astype(str).str.len() + stats[f"{col}_text_stats"] = { + "avg_length": text_lengths.mean(), + "min_length": text_lengths.min(), + "max_length": text_lengths.max(), + "median_length": text_lengths.median(), + } + + return stats + + except Exception as e: + logger.error(f"Error getting data statistics: {e}") + return {} + + def validate_data_quality(self): + """Validate data quality and return issues""" + issues = [] + + try: + target_column = self.extract_target_column() + input_columns = self.extract_input_columns() + + # Check for missing target values + missing_targets = self.df[target_column].isnull().sum() + if missing_targets > 0: + issues.append(f"Missing target values: {missing_targets}") + + # Check for missing input values + for col in input_columns: + if col in self.df.columns: + missing_inputs = self.df[col].isnull().sum() + if missing_inputs > 0: + issues.append(f"Missing values in {col}: {missing_inputs}") + + # Check for empty strings in input columns + for col in input_columns: + if col in self.df.columns: + empty_strings = (self.df[col].astype(str).str.strip() == "").sum() + if empty_strings > 0: + issues.append(f"Empty strings in {col}: {empty_strings}") + + # Check class distribution + class_counts = self.df[target_column].value_counts() + min_class_size = class_counts.min() + if min_class_size < 3: + issues.append(f"Classes with very few samples (min: {min_class_size})") + + # Check for duplicate samples + if len(input_columns) == 1: + duplicates = self.df.duplicated( + subset=input_columns + [target_column] + ).sum() + if duplicates > 0: + issues.append(f"Duplicate samples: {duplicates}") + + if issues: + logger.warning(f"Data quality issues found: {issues}") + else: + logger.info("Data quality validation passed") + + return issues + + except Exception as e: + logger.error(f"Error validating data quality: {e}") + return diff --git a/src/model-training/model-repository/classifier-ensemble/1/README.txt b/src/model-training/model-repository/classifier-ensemble/1/README.txt new file mode 100644 index 00000000..2f8cdca9 --- /dev/null +++ b/src/model-training/model-repository/classifier-ensemble/1/README.txt @@ -0,0 +1,2 @@ +## Placeholder file for ensemble model +This is a place holder file for the ensemble model under version "1/" folder, so that the folder gets created in object storage \ No newline at end of file diff --git a/src/model-training/model-repository/classifier-ensemble/config.pbtxt b/src/model-training/model-repository/classifier-ensemble/config.pbtxt new file mode 100644 index 00000000..d6591b9f --- /dev/null +++ b/src/model-training/model-repository/classifier-ensemble/config.pbtxt @@ -0,0 +1,84 @@ +name: "classifier_ensemble" +platform: "ensemble" +max_batch_size: 32 +input [ + { + name: "TEXT" + data_type: TYPE_STRING + dims: [-1] + }, + { + name: "top_k" + data_type: TYPE_INT32 + dims: [ 1 ] + optional: true + } +] +output [ + { + name: "TOP_K_PREDICTIONS" + data_type: TYPE_STRING + dims: [ -1 ] + } +] +ensemble_scheduling { + step [ + { + model_name: "pre_processing" + model_version: -1 + input_map { + key: "TEXT" + value: "TEXT" + } + output_map { + key: "input_ids" + value: "input_ids" + } + output_map { + key: "attention_mask" + value: "attention_mask" + } + output_map { + key: "token_type_ids" + value: "token_type_ids" + } + }, + { + model_name: "text_classifier" + model_version: -1 + input_map { + key: "input_ids" + value: "input_ids" + } + input_map { + key: "attention_mask" + value: "attention_mask" + } + input_map { + key: "token_type_ids" + value: "token_type_ids" + } + output_map { + key: "logits" + value: "logits" + } + + }, + { + model_name: "post_processing" + model_version: -1 + input_map { + key: "logits" + value: "logits" + } + input_map { + key: "top_k" + value: "top_k" + } + output_map { + key: "TOP_K_PREDICTIONS" + value: "TOP_K_PREDICTIONS" + } + } + ] +} \ No newline at end of file diff --git a/src/model-training/model-repository/post-processing/1/model.py b/src/model-training/model-repository/post-processing/1/model.py new file mode 100644 index 00000000..7bbc8a8c --- /dev/null +++ b/src/model-training/model-repository/post-processing/1/model.py @@ -0,0 +1,251 @@ +import json +import numpy as np +import triton_python_backend_utils as pb_utils +from scipy.special import softmax +import os + + +class TritonPythonModel: + """ + Text postprocessing with uncertainty-based class injection + Maintains original format but injects uncertainty information when needed + """ + + def initialize(self, args): + """Initialize label mappings and OOD detection configuration""" + + # Store args and handle different argument formats + try: + # Handle different argument formats that Triton might pass + if isinstance(args, dict): + model_path = args.get("model_repository", "") + else: + # Fallback if args is not a dict + model_path = str(args) + + except Exception as e: + + print(f"Warning: Failed to parse args in initialize: {e}") + + label_file = os.path.join(model_path, "1", "label_mappings.json") + + self.logger = pb_utils.Logger + + # Load label mappings + self.id_to_label = {} + self.num_classes = 0 + + if os.path.exists(label_file): + try: + with open(label_file, "r") as f: + self.label_mappings = json.load(f) + self.ood_method = self.label_mappings.get("ood_method", "none") + self.ood_threshold = 0.5 + # Uncertainty handling strategy + self.uncertainty_strategy = "inject_class" + self.human_handoff_threshold = 0.8 + self.confidence_scaling = False + # Handle different label mapping formats + if "model_id2label" in self.label_mappings: + self.id_to_label = self.label_mappings["model_id2label"] + self.id_to_label[0] = "out_of_distribution" + if "agency_id2label" in self.label_mappings: + self.agency_id2label = self.label_mappings["agency_id2label"] + self.agency_id2label["out_of_distribution"] = 0 + + # Get number of classes + if "num_labels" in self.label_mappings: + self.num_classes = self.label_mappings["num_labels"] + elif "num_classes" in self.label_mappings: + self.num_classes = self.label_mappings["num_classes"] + else: + self.num_classes = len(self.id_to_label) + except Exception as e: + self.logger.log_warn(f"Failed to load label mappings: {e}") + else: + self.logger.log_warn(f"Label file not found: {label_file}") + # Create default mappings + + def compute_uncertainty(self, logits, covariance_matrix=None): + """Compute uncertainty score based on configured method""" + + if self.ood_method == "sngp" and covariance_matrix is not None: + # Handle different covariance matrix shapes + if covariance_matrix.ndim == 3: + # Shape: [batch_size, num_classes, num_classes] + variance = np.array([np.trace(cov) for cov in covariance_matrix]) + elif covariance_matrix.ndim == 2: + if covariance_matrix.shape[0] == covariance_matrix.shape[1]: + # Single covariance matrix: [num_classes, num_classes] + variance = np.full(logits.shape[0], np.trace(covariance_matrix)) + else: + # Batch of diagonal variances: [batch_size, num_classes] + variance = np.mean(covariance_matrix, axis=-1) + else: + # 1D array: [batch_size] or [num_classes] + if len(covariance_matrix) == logits.shape[0]: + variance = covariance_matrix + else: + variance = np.full(logits.shape[0], np.mean(covariance_matrix)) + + return variance + + # Fallback to max probability confidence for any method + probabilities = softmax(logits, axis=1) + max_probs = np.max(probabilities, axis=1) + return 1.0 - max_probs + + def apply_uncertainty_strategy_new_format( + self, sample_result, uncertainty_score, probabilities, batch_idx + ): + """Apply uncertainty handling strategy to modify predictions - new format version""" + + # If no strategy is set, just return original predictions + if self.uncertainty_strategy is None or self.uncertainty_strategy == "none": + return sample_result + + confidence_score = 1.0 - uncertainty_score + is_high_uncertainty = uncertainty_score > self.ood_threshold + + if is_high_uncertainty: + if hasattr(self.logger, "log_info"): + self.logger.log_info( + f"Sample {batch_idx}: OOD detected ({uncertainty_score:.3f})" + ) + + if self.uncertainty_strategy == "inject_class": + if is_high_uncertainty: + # Inject OOD/uncertain class + ood_item = { + "agency_id": 0, + "agency_name": "out_of_distribution", + "confidence": float(uncertainty_score), + } + # Add at the beginning and shift others + return [ood_item] + sample_result + + elif self.uncertainty_strategy == "confidence_scaling": + # Scale probabilities by confidence + if self.confidence_scaling and confidence_score < 0.8: + scaled_result = [] + for item in sample_result: + scaled_item = item.copy() + scaled_item["confidence"] = float( + item["confidence"] * confidence_score + ) + scaled_result.append(scaled_item) + return scaled_result + + elif self.uncertainty_strategy == "threshold_filter": + # If too uncertain, return generic "uncertain" response + + if is_high_uncertainty: + return [ + { + "agency_id": 0, + "agency_name": "out_of_distribution", + "confidence": float(confidence_score), + } + ] + + # Return original predictions if no strategy applied + return sample_result + + def execute(self, requests): + """Execute postprocessing with uncertainty-aware prediction modification""" + responses = [] + + for request in requests: + logits_tensor = pb_utils.get_input_tensor_by_name(request, "logits") + if logits_tensor is None: + self.logger.log_error("No logits tensor found in request") + continue + + logits = logits_tensor.as_numpy() + + # Get covariance matrix for SNGP + covariance_matrix = None + if self.ood_method == "sngp": + try: + cov_tensor = pb_utils.get_input_tensor_by_name( + request, "covariance_matrix" + ) + if cov_tensor: + covariance_matrix = cov_tensor.as_numpy() + self.logger.log_info( + f"Got covariance matrix with shape: {covariance_matrix.shape}" + ) + except Exception as e: + self.logger.log_warn(f"Failed to get covariance matrix: {e}") + + # Get top_k parameter + top_k = 5 + try: + top_k_tensor = pb_utils.get_input_tensor_by_name(request, "top_k") + if top_k_tensor: + top_k = int(top_k_tensor.as_numpy()[0]) + except Exception as e: + self.logger.log_warn(f"Failed to get top_k parameter: {e}") + + top_k = min(top_k, self.num_classes) + + # Compute probabilities and uncertainty + probabilities = softmax(logits, axis=1) + + # Compute uncertainty if OOD method is enabled + uncertainty_scores = None + if self.ood_method != "none": + try: + uncertainty_scores = self.compute_uncertainty( + logits, covariance_matrix + ) + except Exception as e: + self.logger.log_warn(f"Failed to compute uncertainty: {e}") + uncertainty_scores = None + + batch_size = probabilities.shape[0] + top_k_results = [] + + for i in range(batch_size): + # Get standard top-k predictions + top_k_indices = np.argsort(probabilities[i])[-top_k:][::-1] + top_k_probs = probabilities[i][top_k_indices] + + # Build result in the new format + sample_result = [] + for idx, prob in zip(top_k_indices, top_k_probs): + label_name = self.id_to_label.get(str(idx), f"class_{idx}") + + # Create the new format with agency_id, agency_name, and confidence + prediction_item = { + "agency_id": self.agency_id2label.get(label_name), + "agency_name": label_name, + "confidence": float(prob), + } + sample_result.append(prediction_item) + + # Apply uncertainty strategy to potentially modify result + if self.ood_method != "none" and uncertainty_scores is not None: + sample_result = self.apply_uncertainty_strategy_new_format( + sample_result, uncertainty_scores[i], probabilities[i], i + ) + + top_k_results.append(sample_result) + + # Create output tensor - new format + # Convert the list of results to JSON string + results_json = json.dumps(top_k_results) + + # Create output tensor as single JSON string containing the array + output_tensor = pb_utils.Tensor( + "TOP_K_PREDICTIONS", np.array([results_json], dtype=object) + ) + + response = pb_utils.InferenceResponse(output_tensors=[output_tensor]) + responses.append(response) + + return responses + + def finalize(self): + """Clean up resources""" + pass diff --git a/src/model-training/model-repository/post-processing/config.pbtxt b/src/model-training/model-repository/post-processing/config.pbtxt new file mode 100644 index 00000000..384ae72d --- /dev/null +++ b/src/model-training/model-repository/post-processing/config.pbtxt @@ -0,0 +1,34 @@ +name: "post_processing" +backend: "python" +max_batch_size: 32 +input [ + { + name: "logits" + data_type: TYPE_FP32 + dims: [-1] + }, + { + name: "top_k" + data_type: TYPE_INT32 + dims: [ 1 ] + optional: true + } +] +output [ + { + name: "TOP_K_PREDICTIONS" + data_type: TYPE_STRING + dims: [ -1 ] + } +] + +instance_group [ + { + count: 1 + kind: KIND_GPU + } +] + +dynamic_batching { + max_queue_delay_microseconds: 100 +} \ No newline at end of file diff --git a/src/model-training/model-repository/pre-processing/1/model.py b/src/model-training/model-repository/pre-processing/1/model.py new file mode 100644 index 00000000..74dcb37e --- /dev/null +++ b/src/model-training/model-repository/pre-processing/1/model.py @@ -0,0 +1,193 @@ +import json +import numpy as np +import triton_python_backend_utils as pb_utils +from transformers import AutoTokenizer +import os + + +class TritonPythonModel: + """ + Enhanced text preprocessing model for text classification with OOD detection support + Handles tokenization for BERT, RoBERTa, XLM, and DistilBERT models + Supports both regular and OOD-enhanced models + """ + + def initialize(self, args): + """Initialize tokenizer and model configuration based on database info""" + + # Store args for later use and handle different argument formats + self.args = args + self.logger = pb_utils.Logger + try: + # Handle different argument formats that Triton might pass + if isinstance(args, dict): + model_path = args.get("model_repository", "") + else: + # Fallback if args is not a dict + model_path = str(args) + + except Exception as e: + self.logger.log_warn(f"Warning: Failed to parse args in initialize: {e}") + + label_file = os.path.join(model_path, "1", "label_mappings.json") + + # Load model configuration parameters with safe defaults + + # Load label mappings for additional context (optional) + try: + with open(label_file, "r") as f: + self.label_mappings = json.load(f) + self.num_classes = self.label_mappings.get("num_classes", 2) + self.model_name = self.label_mappings.get("model_name", None) + self.base_model_type = self.label_mappings.get("base_model_name", None) + self.max_length = self.label_mappings.get("sequence_length", 512) + self.logger.log_info( + f"Loaded label mappings with {self.num_classes} classes" + ) + except Exception as e: + self.logger.log_info(f"No label mappings found ({e})") + + # Initialize tokenizer based on model type + self.tokenizer = self._initialize_tokenizer() + + self.requires_token_type_ids = self._model_supports_token_type_ids() + + if hasattr(self.logger, "log_info"): + self.logger.log_info( + f"Token type IDs required: {self.requires_token_type_ids}" + ) + else: + print(f"Token type IDs required: {self.requires_token_type_ids}") + + def _model_supports_token_type_ids(self): + """ + Check if the base model supports token_type_ids based on model type + """ + # Clean the model type name + model_type = self.base_model_type.lower() + if "multilingual-" in model_type: + model_type = model_type.replace("multilingual-", "") + + # Models that explicitly reject token_type_ids + if model_type in ["distilbert"]: + return False + + # Models that use token_type_ids + if model_type in ["bert"]: + return True + + if model_type in ["xlm-roberta", "roberta"]: + return True + + # Fallback: try to detect from tokenizer + try: + if hasattr(self.tokenizer, "model_max_length"): + # Test tokenization to see if token_type_ids are returned + test_encoding = self.tokenizer( + "test", return_token_type_ids=True, return_tensors="np" + ) + return "token_type_ids" in test_encoding + except Exception as e: + self.logger.log_warn(f"Token type ID detection failed: {e}") + + return False + + def _initialize_tokenizer(self): + """Initialize appropriate tokenizer based on model configuration""" + try: + # Try to load from the model directory first + + tokenizer = AutoTokenizer.from_pretrained(self.model_name) + self.logger.log_info(f"Loaded tokenizer for {self.model_name}") + + return tokenizer + except Exception as e: + self.logger.log_error( + f"Failed to load tokenizer for {self.model_name}: {e}" + ) + + def execute(self, requests): + """Execute preprocessing on input texts""" + responses = [] + + for request in requests: + # Get input text + input_text = pb_utils.get_input_tensor_by_name(request, "TEXT") + texts = input_text.as_numpy() + + # Tokenize all texts in the batch + batch_input_ids = [] + batch_attention_mask = [] + batch_token_type_ids = [] + + for text in texts: + # Handle different text formats + if isinstance(text, bytes): + text = text.decode("utf-8") + elif isinstance(text, np.ndarray): + text = str(text.item()) + else: + text = str(text) + + # Tokenize with appropriate parameters + tokenize_params = { + "text": text, + "add_special_tokens": True, + "max_length": self.max_length, + "padding": "max_length", + "truncation": True, + "return_attention_mask": True, + "return_tensors": "np", + } + + # Only add token_type_ids for models that support it + if self.requires_token_type_ids: + tokenize_params["return_token_type_ids"] = True + + try: + encoding = self.tokenizer(**tokenize_params) + except Exception as e: + self.logger.log_error( + f"Tokenization failed for text: {text[:50]}... Error: {e}" + ) + # Create fallback encoding + encoding = { + "input_ids": np.zeros((1, self.max_length), dtype=np.int64), + "attention_mask": np.zeros( + (1, self.max_length), dtype=np.int64 + ), + } + if self.requires_token_type_ids: + encoding["token_type_ids"] = np.zeros( + (1, self.max_length), dtype=np.int64 + ) + + batch_input_ids.append(encoding["input_ids"][0]) + batch_attention_mask.append(encoding["attention_mask"][0]) + + if self.requires_token_type_ids and "token_type_ids" in encoding: + batch_token_type_ids.append(encoding["token_type_ids"][0]) + else: + # Create dummy token_type_ids for consistency (always output them) + batch_token_type_ids.append(np.zeros_like(encoding["input_ids"][0])) + + # Convert to numpy arrays with correct dtypes + input_ids = np.array(batch_input_ids, dtype=np.int64) + attention_mask = np.array(batch_attention_mask, dtype=np.int64) + token_type_ids = np.array(batch_token_type_ids, dtype=np.int64) + + # Create output tensors + output_tensors = [ + pb_utils.Tensor("input_ids", input_ids), + pb_utils.Tensor("attention_mask", attention_mask), + pb_utils.Tensor("token_type_ids", token_type_ids), + ] + response = pb_utils.InferenceResponse(output_tensors=output_tensors) + responses.append(response) + + return responses + + def finalize(self): + """Clean up resources""" + if hasattr(self, "tokenizer"): + del self.tokenizer \ No newline at end of file diff --git a/src/model-training/model-repository/pre-processing/config.pbtxt b/src/model-training/model-repository/pre-processing/config.pbtxt new file mode 100644 index 00000000..2cb1cb7b --- /dev/null +++ b/src/model-training/model-repository/pre-processing/config.pbtxt @@ -0,0 +1,32 @@ +name: "pre_processing" +backend: "python" +max_batch_size: 32 +input [ + { + name: "TEXT" + data_type: TYPE_STRING + dims: [-1] + } +] + + +output [ + { + name: "input_ids" + data_type: TYPE_INT64 + dims: [ -1 ] + }, + { + name: "attention_mask" + data_type: TYPE_INT64 + dims: [ -1 ] + }, + { + name: "token_type_ids" + data_type: TYPE_INT64 + dims: [ -1 ] + } +] +dynamic_batching { + max_queue_delay_microseconds: 100 +} \ No newline at end of file diff --git a/src/model-training/model-repository/text-classifier/1/README.txt b/src/model-training/model-repository/text-classifier/1/README.txt new file mode 100644 index 00000000..2f8cdca9 --- /dev/null +++ b/src/model-training/model-repository/text-classifier/1/README.txt @@ -0,0 +1,2 @@ +## Placeholder file for ensemble model +This is a place holder file for the ensemble model under version "1/" folder, so that the folder gets created in object storage \ No newline at end of file diff --git a/src/model-training/model-repository/text-classifier/config.pbtxt b/src/model-training/model-repository/text-classifier/config.pbtxt new file mode 100644 index 00000000..63f6c3e4 --- /dev/null +++ b/src/model-training/model-repository/text-classifier/config.pbtxt @@ -0,0 +1,39 @@ +name: "text_classifier" +platform: "onnxruntime_onnx" +max_batch_size: 32 +input [ + { + name: "input_ids" + data_type: TYPE_INT64 + dims: [-1] + }, + { + name: "attention_mask" + data_type: TYPE_INT64 + dims: [-1] + }, + { + name: "token_type_ids" + data_type: TYPE_INT64 + dims: [-1] + } +] +output [ + { + name: "logits" + data_type: TYPE_FP32 + dims: [-1] + } +] +dynamic_batching { + max_queue_delay_microseconds: 100 +} + +instance_group [ + { + count: 1 + kind: KIND_GPU + + } +] + diff --git a/src/model-training/model_trainer.py b/src/model-training/model_trainer.py new file mode 100644 index 00000000..bed7bc40 --- /dev/null +++ b/src/model-training/model_trainer.py @@ -0,0 +1,720 @@ +from datapipeline import DataPipeline +from trainingpipeline import TrainingPipeline, create_training_pipeline +import os +import sys +import shutil +import json +from datetime import datetime, timezone +from s3_ferry import S3Ferry +from create_triton_configs import generate_all_triton_configs +from constants import ( + MODEL_RESULTS_PATH, + S3_FERRY_MODEL_STORAGE_PATH, + ACCURACY_WEIGHT, + UNCERTAINTY_CONFIGS, + F1_WEIGHT, + SEQUENCE_LENGTH, + MODEL_TRAINING_SOURCE_PATH, + DEPLOYMENT_ENDPOINT, + CREATE_TRAINING_PROGRESS_SESSION_ENDPOINT, + UPDATE_TRAINING_PROGRESS_SESSION_ENDPOINT, + UPDATE_MODEL_TRAINING_STATUS_ENDPOINT, + INITIATING_TRAINING_PROGRESS_STATUS, + TRAINING_IN_PROGRESS_PROGRESS_STATUS, + DEPLOYING_MODEL_PROGRESS_STATUS, + MODEL_TRAINED_AND_DEPLOYED_PROGRESS_STATUS, + TRAINING_FAILED_STATUS, + DEPLOYMENT_FAILED_STATUS, + INITIATING_TRAINING_PROGRESS_PERCENTAGE, + TRAINING_IN_PROGRESS_PROGRESS_PERCENTAGE, + DEPLOYING_MODEL_PROGRESS_PERCENTAGE, + MODEL_TRAINED_AND_DEPLOYED_PROGRESS_PERCENTAGE, + INITIATING_TRAINING_PROGRESS_MESSAGE, + TRAINING_IN_PROGRESS_PROGRESS_MESSAGE, + DEPLOYING_MODEL_PROGRESS_MESSAGE, + MODEL_TRAINED_AND_DEPLOYED_PROGRESS_MESSAGE, + TRAINING_FAILED_STATUS_MESSAGE, + TRAINING_FAILED_PROGRESS_PERCENTAGE + +) + +from loguru import logger +import requests + +import argparse + +from loki_logger import LokiLogger +logger = LokiLogger(service_name="model-trainer") + + +class ModelTrainer: + def __init__( + self, + model_id, + model_name, + dataset_id, + model_types, + major_version, + minor_version, + latest, + current_deployment_env, + progress_session_id, + target_deployment_platform, + ) -> None: + try: + logger.info("INITIALIZING MODEL TRAINER") + self.model_id = model_id + self.model_name = model_name + self.dataset_id = dataset_id + self.model_types = model_types + self.major_version = major_version + self.minor_version = minor_version + self.latest = latest + self.current_deployment_platform = current_deployment_env + self.target_deployment_platform = target_deployment_platform + + self.progress_session_id = "" + + except Exception as e: + logger.error(f"EXCEPTION IN MODEL_TRAINER INIT : {e}") + + @staticmethod + def create_training_folders(folder_paths): + logger.info("CREATING FOLDER PATHS") + try: + for folder_path in folder_paths: + if not os.path.exists(folder_path): + os.makedirs(folder_path) + logger.info(f"SUCCESSFULLY CREATED MODEL FOLDER PATHS : {folder_paths}") + except Exception as e: + logger.error(f"FAILED TO CREATE MODEL FOLDER PATHS : {folder_paths}") + raise RuntimeError(e) + + def get_current_timestamp(self): + current_timestamp = int(datetime.now(timezone.utc).timestamp()) + return current_timestamp + + def create_training_progress_session(self): + """ + Create a training progress session in the database. + This function should be implemented to create a training progress session in the database. + """ + logger.info("Creating training progress session") + + payload = { + "modelId": int(self.model_id), + "modelName": self.model_name, + "majorVersion": self.major_version, + "minorVersion": self.minor_version, + "latest": self.latest, + } + + logger.info(f"Prepared training progress session payload {payload}") + + try: + # Make request to create training progress session endpoint + response = requests.post( + url=CREATE_TRAINING_PROGRESS_SESSION_ENDPOINT, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=300 # 5 minute timeout for creating progress session + ) + + logger.info(f"Create training progress session response - {response.status_code} - {response.text}") + + # Check if request was successful + + logger.info("Training progress session created successfully") + + session_data = response.json() + session_id = session_data["response"]["sessionId"] + + self.progress_session_id = session_id + + return response.json() + + except requests.HTTPError as e: + error_msg = f"HTTP error during creating training progress session: {e.response.status_code} - {e.response.text}" + logger.error(error_msg, model_id=self.model_id, status_code=e.response.status_code) + raise + + except requests.RequestException as e: + error_msg = f"Network error during creating training progress session: {str(e)}" + logger.error(error_msg, model_id=self.model_id) + raise + + except Exception as e: + error_msg = f"Unexpected error during creating training progress session: {str(e)}" + logger.error(error_msg, model_id=self.model_id) + raise + + def update_training_progression_session(self,training_status:str, training_message:str, progress_percentage:int, process_complete:bool): + """ + Update the training progress session in the database. + This function should be implemented to update the training progress session in the database. + """ + logger.info("Updating training progress session") + + if not self.progress_session_id: + logger.error("Progress session ID is not set. Cannot update training progress session.") + raise ValueError("Progress session ID is required to update the training progress session.") + + else: + + payload = { + "sessionId": self.progress_session_id, + "trainingStatus": training_status, + "trainingMessage": training_message, + "progressPercentage": progress_percentage, + "processComplete": process_complete + } + + logger.info(f"Prepared training progress session update payload {payload}") + + try: + # Make request to update training progress session endpoint + response = requests.post( + url=UPDATE_TRAINING_PROGRESS_SESSION_ENDPOINT, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=300 # 5 minute timeout for updating progress session + ) + + logger.info(f"Update training progress session response - {response.status_code} - {response.text}") + + # Check if request was successful + response.raise_for_status() + + logger.info("Training progress session updated successfully") + + return response.json() + + except requests.HTTPError as e: + error_msg = f"HTTP error during updating training progress session: {e.response.status_code} - {e.response.text}" + logger.error(error_msg, model_id=self.model_id, status_code=e.response.status_code) + raise + + except requests.RequestException as e: + error_msg = f"Network error during updating training progress session: {str(e)}" + logger.error(error_msg, model_id=self.model_id) + raise + + except Exception as e: + error_msg = f"Unexpected error during updating training progress session: {str(e)}" + logger.error(error_msg, model_id=self.model_id) + raise + + def update_training_results(self, training_results, model_s3_location): + """ + Update training results in the database. + This function should be implemented to update the training results in the database. + """ + logger.info("Updating training results in the database") + + payload = { + "modelId": self.model_id, + "trainingResults": training_results, + "modelS3Location": model_s3_location + } + + logger.info(f"Prepared deployment payload {payload}") + + try: + # Make request to deployment endpoint + response = requests.post( + url=UPDATE_MODEL_TRAINING_STATUS_ENDPOINT, + json=payload, + headers={"Content-Type": "application/json"}) + + logger.info(f"Update model endpoint response - {response.status_code} - {response.text}") + + # Check if request was successful + response.raise_for_status() + + logger.info("Model training data pushed to database successfully") + + return response.json() + + except requests.HTTPError as e: + error_msg = f"HTTP error during model deployment: {e.response.status_code} - {e.response.text}" + logger.error(error_msg, model_id=self.model_id, + current_env=self.current_deployment_platform, target_env=self.target_deployment_platform, + status_code=e.response.status_code) + raise + + except requests.RequestException as e: + error_msg = f"Network error during model deployment: {str(e)}" + logger.error(error_msg, model_id=self.model_id, + current_env=self.current_deployment_platform, target_env=self.target_deployment_platform) + raise + + except Exception as e: + error_msg = f"Unexpected error during model deployment: {str(e)}" + logger.error(error_msg, model_id=self.model_id, + current_env=self.current_deployment_platform, target_env=self.target_deployment_platform) + raise + + + def calculate_combined_score(self, accuracies, f1_scores): + """Calculate combined score using weighted average""" + if not accuracies or not f1_scores: + return 0.0 + + avg_accuracy = sum(accuracies) / len(accuracies) + avg_f1 = sum(f1_scores) / len(f1_scores) + + combined_score = (ACCURACY_WEIGHT * avg_accuracy) + (F1_WEIGHT * avg_f1) + return combined_score + + def deploy_model(self, deployment_environment) : + """Deploy the model to the specified environment""" + logger.info(f"DEPLOYING MODEL TO {deployment_environment}") + # Placeholder for deployment logic + # This could involve calling a deployment service, updating configs, etc. + # For now, just log the action + + logger.info(f"MODEL {self.model_name} (ID: {self.model_id}) deployed to {deployment_environment}") + + def train(self): + """UNIFIED TRAINING METHOD - TRAINS ALL VARIANTS""" + try: + logger.info("ENTERING UNIFIED TRAINING FUNCTION") + logger.info(f"DEPLOYMENT PLATFORM - {self.current_deployment_platform}") + + trainer.update_training_progression_session( + training_status=INITIATING_TRAINING_PROGRESS_STATUS, + training_message=INITIATING_TRAINING_PROGRESS_MESSAGE, + progress_percentage=INITIATING_TRAINING_PROGRESS_PERCENTAGE, + process_complete=False) + + + # Initialize services + s3_ferry = S3Ferry() + + # Load data + data_pipeline = DataPipeline(self.dataset_id) + dfs = data_pipeline.create_dataframes() + models_inference_metadata, _ = data_pipeline.models_and_filters() + + logger.info(f"MODELS_INFERENCE_METADATA : {models_inference_metadata}") + + # Setup paths + + # Generate all model variants to train + model_variants = [] + + trainer.update_training_progression_session( + training_status=TRAINING_IN_PROGRESS_PROGRESS_STATUS, + training_message=TRAINING_IN_PROGRESS_PROGRESS_MESSAGE, + progress_percentage=TRAINING_IN_PROGRESS_PROGRESS_PERCENTAGE, + process_complete=False) + + # Add standard models + for base_model in self.model_types: + + model_variants.append( + { + "name": base_model + "-sngp", + "base_model": base_model, + "full_model_name": base_model, + "ood_method": "sngp", + "type": "ood", + "uncertainty_strategy": UNCERTAINTY_CONFIGS.get( + "uncertainty_strategy", None + ), + "human_handoff_threshold": UNCERTAINTY_CONFIGS.get( + "human_handoff_threshold", 0.8 + ), + "confidence_scaling": UNCERTAINTY_CONFIGS.get( + "confidence_scaling", False + ), + } + ) + logger.info(f"TRAINING {len(model_variants)} MODEL VARIANTS:") + for variant in model_variants: + logger.info(f" - {variant['name']} ({variant['type']})") + + # Train all variants + all_results = [] + + for i, variant in enumerate(model_variants): + logger.info( + f"TRAINING VARIANT {i + 1}/{len(model_variants)}: {variant['name']}" + ) + + try: + # Create training pipeline + if variant["ood_method"]: + training_pipeline = create_training_pipeline( + dfs=dfs, + model_name=variant["base_model"], + full_name=variant["full_model_name"], + ood_method=variant["ood_method"], + ) + else: + training_pipeline = TrainingPipeline(dfs, variant["base_model"],full_name=variant["full_model_name"],) + + # Train the variant + model_dir, metrics = training_pipeline.train() + + # Calculate combined score + _, accuracies, f1_scores = metrics + combined_score = self.calculate_combined_score( + accuracies, f1_scores + ) + + # Store results + result = { + "variant": variant, + "model_path": model_dir, + "metrics": metrics, + "avg_accuracy": ( + sum(accuracies) / len(accuracies) if accuracies else 0 + ), + "avg_f1": sum(f1_scores) / len(f1_scores) if f1_scores else 0, + "combined_score": combined_score, # <-- Add this line + } + + all_results.append(result) + + logger.info( + f"COMPLETED {variant['name']} - Combined Score: {combined_score:.4f}" + ) + logger.info( + f" Avg Accuracy: {result['avg_accuracy']:.4f}, Avg F1: {result['avg_f1']:.4f}" + ) + + except Exception as e: + logger.error(f"FAILED TO TRAIN {variant['name']}: {e}") + continue + + # Select best model across all variants + if not all_results: + raise RuntimeError("No models were successfully trained") + + best_result = max(all_results, key=lambda x: x["combined_score"]) + best_variant = best_result["variant"] + + logger.info(f"BEST MODEL SELECTED: {best_variant['name']}") + logger.info(f"BEST COMBINED SCORE: {best_result['combined_score']:.4f}") + logger.info(f"BEST MODEL TYPE: {best_variant['type']}") + + # Save training summary + training_summary = { + "best_model": best_variant, + "best_score": best_result["combined_score"], + "all_results": [ + { + "variant": r["variant"], + "combined_score": r["combined_score"], + "avg_accuracy": r["avg_accuracy"], + "avg_f1": r["avg_f1"], + } + for r in all_results + ], + "total_variants_trained": len(all_results), + "training_timestamp": self.get_current_timestamp(), + } + + with open(f"{MODEL_RESULTS_PATH}/training_summary.json", "w") as f: + json.dump(training_summary, f, indent=2) + from trainingpipeline import export_inference_model_to_onnx + + # Convert best model to ONNX + # check if it is sngp + logger.info("CONVERTING SNGP MODEL TO ONNX") + onnx_path = export_inference_model_to_onnx(best_result["model_path"]) + logger.info(f"ONNX MODEL SAVED AT: {onnx_path}") + + # create model-id folder and copy model-repository directory contents there + new_model_repo_path = f"{MODEL_RESULTS_PATH}/{self.model_id}" + if not os.path.exists(new_model_repo_path): + os.makedirs(new_model_repo_path) + # this is the pre-defined model-repository path + model_repository_path = f"{MODEL_TRAINING_SOURCE_PATH}/model-repository" + + # copy all contents and directories of model-repository to new_model_repo_path + shutil.copytree( + src=model_repository_path, + dst=new_model_repo_path, + dirs_exist_ok=True, + ) + # add labels-mapping.json to new_model_repo_path pre-processing and post-processing directories + label_mappings_path = f"{new_model_repo_path}/pre-processing/1" + if not os.path.exists(label_mappings_path): + os.makedirs(label_mappings_path) + shutil.copy( + src=f"{best_result['model_path']}/config.json", + dst=f"{label_mappings_path}/label_mappings.json", + ) + shutil.copy( + src=f"{best_result['model_path']}/config.json", + dst=f"{new_model_repo_path}/post-processing/1/label_mappings.json", + ) + top_level_dirs = [ + d + for d in os.listdir(new_model_repo_path) + if os.path.isdir(os.path.join(new_model_repo_path, d)) + ] + # add modelId-{model-id} to all folders inside the new_model_repo_path + for dir_name in top_level_dirs: + old_path = os.path.join(new_model_repo_path, dir_name) + new_dir_name = f"{self.model_id}-{dir_name}" + new_path = os.path.join(new_model_repo_path, new_dir_name) + + logger.info(f"Renaming {dir_name} to {new_dir_name}") + os.rename(old_path, new_path) + + # move onnx model to the new model-id folder inside model-id/text-classifier/1/model.onnx + onnx_model_path = f"{new_model_repo_path}/{self.model_id}-text-classifier/1" + if not os.path.exists(onnx_model_path): + os.makedirs(onnx_model_path) + shutil.move( + src=f"{best_result['model_path']}/model.onnx", + dst=f"{onnx_model_path}/model.onnx", + ) + + triton_configs = generate_all_triton_configs( + model_id=str(self.model_id), + model_type=best_variant["base_model"], # "distilbert", "bert", etc. + num_labels=len(best_result["metrics"][0]), + sequence_length=SEQUENCE_LENGTH, + max_batch_size=16, + ood_method=best_variant.get("ood_method"), # "sngp", "energy", etc. + ood_threshold=0.5, + uncertainty_threshold=best_variant.get("uncertainty_strategy", "sngp"), + human_handoff_threshold=best_variant.get( + "human_handoff_threshold", 0.8 + ), + uncertainty_strategy="inject_class", # or other strategies + confidence_scaling=best_variant.get("confidence_scaling", False), + base_model_type=best_variant.get("base_model_type", "bert"), + ) + # Save Triton configs to model repository + for config_path, config_content in triton_configs.items(): + config_full_path = os.path.join(new_model_repo_path, config_path) + os.makedirs(os.path.dirname(config_full_path), exist_ok=True) + with open(config_full_path, "w") as f: + f.write(config_content) + # Create model archive + model_zip_path = new_model_repo_path + shutil.make_archive( + base_name=model_zip_path, root_dir=model_zip_path, format="zip" + ) + + # Upload to S3 + s3_save_location = f"{S3_FERRY_MODEL_STORAGE_PATH}/{str(self.model_id)}.zip" + + # Removing /app from path since S3 Ferry will already add /app to the path as defined in the config.env + local_source_location = f"{MODEL_RESULTS_PATH.replace('/app/', '')}/{str(self.model_id)}.zip" + + + logger.info("INITIATING MODEL UPLOAD TO S3") + _ = s3_ferry.transfer_file( + s3_save_location, "S3", local_source_location, "FS" + ) + + # Cleanup local files + MODEL_RESULT_FOLDER = f"{MODEL_RESULTS_PATH}/{self.model_id}" + MODEL_RESULT_ZIP_FILE = f"{MODEL_RESULTS_PATH}/{self.model_id}.zip" + + if os.path.exists(MODEL_RESULT_FOLDER): + try: + shutil.rmtree(MODEL_RESULT_FOLDER) + logger.info(f"Cleaned up folder '{MODEL_RESULT_FOLDER}'") + except Exception as e: + logger.warning( + f"Could not delete folder '{MODEL_RESULT_FOLDER}': {e}" + ) + + if os.path.exists(MODEL_RESULT_ZIP_FILE): + try: + os.remove(MODEL_RESULT_ZIP_FILE) + logger.info(f"Cleaned up zip file '{MODEL_RESULT_ZIP_FILE}'") + except Exception as e: + logger.warning( + f"Could not delete zip file '{MODEL_RESULT_ZIP_FILE}': {e}" + ) + + # Deploy the best model + if self.current_deployment_platform == "undeployed": + logger.info("MODEL DEPLOYMENT PLATFORM IS UNDEPLOYED") + + logger.info("UNIFIED TRAINING COMPLETED") + else: + logger.info( + f"INITIATING DEPLOYMENT OF {best_variant['name']} TO {self.current_deployment_platform}" + ) + + + logger.info("=" * 60) + logger.info("UNIFIED TRAINING COMPLETED SUCCESSFULLY") + logger.info(f"BEST MODEL: {best_variant['name']}") + logger.info(f"FINAL SCORE: {best_result['combined_score']:.4f}") + logger.info(f"VARIANTS TRAINED: {len(all_results)}") + logger.info("=" * 60) + + logger.info("Updating training results to database") + self.update_training_results( + training_results=all_results, + model_s3_location=s3_save_location) + + trainer.update_training_progression_session( + training_status=DEPLOYING_MODEL_PROGRESS_STATUS, + training_message=DEPLOYING_MODEL_PROGRESS_MESSAGE, + progress_percentage=100, + process_complete=True) + + + except Exception as e: + import traceback + + logger.error(f"EXCEPTION IN UNIFIED MODEL TRAINER: {e}") + logger.error(traceback.format_exc()) + + trainer.update_training_progression_session( + training_status=TRAINING_FAILED_STATUS, + training_message=TRAINING_FAILED_STATUS_MESSAGE, + progress_percentage=TRAINING_FAILED_PROGRESS_PERCENTAGE, + process_complete=False) + + raise + + def deploy(self): + + """ + Deploy a model from current environment to target environment using Ruuter endpoint. + + Args: + model_id: The ID of the model to deploy + current_env: Current deployment environment (e.g., 'testing', 'production') + target_env: Target deployment environment to deploy to + first_deployment: Whether this is the first deployment (default: False) + + """ + + + #TODO - Add sessionId here to pass session ID to the deployment endpoint + logger.info("Starting model deployment") + + # Prepare request payload + payload = { + "modelId": self.model_id, + "currentEnv": self.current_deployment_platform, + "targetEnv": self.target_deployment_platform, + "firstDeployment": True + } + + logger.info(f"Prepared deployment payload {payload}") + + try: + # Make request to deployment endpoint + response = requests.post( + DEPLOYMENT_ENDPOINT, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=300 # 5 minute timeout for deployment operations + ) + + logger.info(f"Deployment endpoint response - {response.status_code} - {response.text}") + + # Check if request was successful + response.raise_for_status() + + logger.info("Model deployment completed successfully") + + trainer.update_training_progression_session( + training_status=MODEL_TRAINED_AND_DEPLOYED_PROGRESS_STATUS, + training_message=MODEL_TRAINED_AND_DEPLOYED_PROGRESS_MESSAGE, + progress_percentage=MODEL_TRAINED_AND_DEPLOYED_PROGRESS_PERCENTAGE, + process_complete=True) + + + return response.json() + + except requests.HTTPError as e: + error_msg = f"HTTP error during model deployment: {e.response.status_code} - {e.response.text}" + logger.error(error_msg, model_id=self.model_id, + current_env=self.current_deployment_platform, target_env=self.target_deployment_platform, + status_code=e.response.status_code) + raise + + except requests.RequestException as e: + error_msg = f"Network error during model deployment: {str(e)}" + logger.error(error_msg, model_id=self.model_id, + current_env=self.current_deployment_platform, target_env=self.target_deployment_platform) + raise + + except Exception as e: + error_msg = f"Unexpected error during model deployment: {str(e)}" + logger.error(error_msg, model_id=self.model_id, + current_env=self.current_deployment_platform, target_env=self.target_deployment_platform) + raise + + + + +# ----------------------TODO: Uncomment the CLI section when needed---------------------- +def parse_args(): + parser = argparse.ArgumentParser(description="Model Trainer CLI") + parser.add_argument( + "--model_types", + type=str, + required=True, + help="Model types (JSON string or list)", + ) + parser.add_argument("--model_id", type=int, required=True, help="Model ID") + parser.add_argument("--job_id", type=int, required=True, help="Job ID") + parser.add_argument("--dataset_id", type=int, required=True, help="Dataset ID") + parser.add_argument("--model_name", type=str, required=True, help="Model Name") + parser.add_argument( + "--major_version", type=int, required=True, help="Major Version" + ) + parser.add_argument( + "--minor_version", type=int, required=True, help="Minor Version" + ) + parser.add_argument( + "--latest", type=str, required=True, help="Is Latest (true/false)" + ) + parser.add_argument( + "--deployment_environment", + type=str, + required=True, + help="Deployment Environment", + ) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + model_id = args.model_id + model_name = args.model_name + dataset_id = args.dataset_id + model_types = ( + json.loads(args.model_types) + if isinstance(args.model_types, str) + else args.model_types + ) + major_version = args.major_version + minor_version = args.minor_version + latest = args.latest.lower() == "true" + current_deployment_env = "undeployed" + progress_session_id = args.job_id + target_deployment_platform = args.deployment_environment + + trainer = ModelTrainer( + model_id=model_id, + model_name=model_name, + dataset_id=dataset_id, + model_types=model_types, + major_version=major_version, + minor_version=minor_version, + latest=latest, + current_deployment_env=current_deployment_env, + progress_session_id=progress_session_id, + target_deployment_platform=target_deployment_platform, + ) + + trainer.create_training_progress_session() + trainer.train() + trainer.deploy() \ No newline at end of file diff --git a/src/model-training/model_trainer_api.py b/src/model-training/model_trainer_api.py new file mode 100644 index 00000000..0ffca38a --- /dev/null +++ b/src/model-training/model_trainer_api.py @@ -0,0 +1,178 @@ +from fastapi.middleware.cors import CORSMiddleware +from fastapi import FastAPI + +from model_trainer import ModelTrainer +import json +from typing import Optional +from loguru import logger +from pydantic import BaseModel +import sys + +logger.remove() +logger.add(sys.stdout, format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}") +print("INIT STARTED model_trainer_api.py") + + +logger.info("INIT STARTED model_trainer_api.py") + +app = FastAPI(title="Model Training API - Unified Training") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +print("PROCESS STARTED model_trainer_api.py") +logger.info("PROCESS STARTED model_trainer_api.py") + + +class SessionPayload(BaseModel): + """Unified payload for all training (standard + OOD variants)""" + + cookie: str + old_model_id: str + new_model_id: str + update_type: str + prev_deployment_env: Optional[str] = None + progress_session_id: int + deployment_env: str + model_details: str + + +# Global training status +Training = False + + +@app.post("/model_trainer/") +async def unified_model_train(payload: SessionPayload): + """ + Unified training endpoint that trains all model variants: + - Standard models: estbert, xlm-roberta, multilingual-distilbert + - OOD variants: energy, SNGP, softmax for each base model + + Selects and deploys the best performing model across all variants. + """ + global Training + + try: + print("Starting unified model training") + print("payload: ", payload.dict()) + + # Extract payload data + cookie = payload.cookie + new_model_id = payload.new_model_id + old_model_id = payload.old_model_id + prev_deployment_env = payload.prev_deployment_env + update_type = payload.update_type + progress_session_id = payload.progress_session_id + model_details = json.loads(payload.model_details) + current_deployment_platform = payload.deployment_env + + logger.info(f"UNIFIED TRAINING STARTED FOR MODEL {new_model_id}") + logger.info("TRAINING ALL VARIANTS: Standard + OOD methods") + + Training = True + + # Initialize and start unified training + logger.info("INITIALIZING UNIFIED MODEL TRAINER") + + trainer = ModelTrainer( + cookie=cookie, + new_model_id=new_model_id, + old_model_id=old_model_id, + prev_deployment_env=prev_deployment_env, + update_type=update_type, + progress_session_id=progress_session_id, + current_deployment_platform=current_deployment_platform, + model_details=model_details, + ) + + # Train all variants + logger.info("STARTING UNIFIED TRAINING") + trainer.train() + logger.info("UNIFIED TRAINING COMPLETED") + + Training = False + logger.info("UNIFIED TRAINING SCRIPT COMPLETED") + + return { + "status": "success", + "message": "Unified training completed successfully", + "model_id": new_model_id, + "session_id": progress_session_id, + "training_type": "unified_standard_and_ood", + } + + except Exception as e: + Training = False + logger.error(f"Error in unified model training: {e}") + print(f"Error in unified model training: {e}") + + return { + "status": "error", + "message": f"Unified training failed: {str(e)}", + "error_type": type(e).__name__, + "training_type": "unified_standard_and_ood", + } + + +@app.get("/model_checker/") +async def model_checker(): + """Check current training status""" + print("Checking training status") + print("Training: ", Training) + return {"Training": Training} + + +@app.get("/supported_models/") +async def get_supported_models(): + """Return all supported model variants""" + base_models = ["estbert", "xlm-roberta", "multilingual-distilbert"] + ood_methods = ["energy", "sngp", "softmax"] + + models = [] + + # Add standard models + for model in base_models: + models.append( + { + "name": model, + "type": "standard", + "description": f"Standard {model} training", + } + ) + + # Add OOD variants + for model in base_models: + for ood_method in ood_methods: + models.append( + { + "name": f"{model}-{ood_method}", + "type": "ood", + "base_model": model, + "ood_method": ood_method, + "description": f"{model} with {ood_method.upper()} OOD detection", + } + ) + + return { + "base_models": base_models, + "ood_methods": ood_methods, + "all_variants": models, + "total_variants": len(models), + } + + +@app.get("/training_status/{session_id}") +async def get_training_status(session_id: int): + """Get detailed training status for a specific session""" + return { + "session_id": session_id, + "is_training": Training, + "status": "in_progress" if Training else "idle", + "message": "Unified training in progress" if Training else "No active training", + "training_type": "unified_standard_and_ood", + } diff --git a/src/model-training/requirements-cpu.txt b/src/model-training/requirements-cpu.txt new file mode 100644 index 00000000..6d4d27c3 --- /dev/null +++ b/src/model-training/requirements-cpu.txt @@ -0,0 +1,96 @@ +# CPU-only requirements for model training pipeline +# Install with: pip install -r requirements-cpu.txt + +# PyTorch CPU-only version +torch==2.4.0+cpu +torchvision==0.19.0+cpu +torchaudio==2.4.0+cpu +--extra-index-url https://download.pytorch.org/whl/cpu + +# Core ML and data processing +pandas==2.2.2 +scikit-learn==1.5.1 +numpy==1.24.4 +scipy==1.11.3 + +# Transformers and NLP +tokenizers==0.19.1 +huggingface-hub==0.24.2 +safetensors==0.4.3 +accelerate==0.33.0 +sentencepiece==0.2.0 +onnx>=1.12.0 +onnxruntime>=1.12.0 +transformers[onnx]>=4.20.0 + +# For Estonian models and multilingual support +sentence-transformers==2.2.2 +langdetect==1.0.9 + +# Pydantic and validation +pydantic==2.8.2 +pydantic-core==2.20.1 +annotated-types==0.7.0 + +# FastAPI and web framework +fastapi==0.111.1 +fastapi-cli==0.0.4 +uvicorn[standard]==0.30.5 +starlette==0.37.2 +python-multipart==0.0.9 + +# HTTP and networking +requests==2.32.3 +httpx==0.27.0 +httpcore==1.0.5 +httptools==0.6.1 +h11==0.14.0 +urllib3==2.2.2 +certifi==2024.7.4 +charset-normalizer==3.3.2 +idna==3.7 + +# Utilities and logging +loguru==0.7.2 +click==8.1.7 +tqdm==4.66.4 +rich==13.7.1 +colorama==0.4.6 + +# Configuration and environment +python-dotenv==1.0.1 +PyYAML==6.0.1 + +# File handling and serialization +pillow==10.2.0 +regex==2024.7.24 +filelock==3.15.4 +fsspec==2024.6.1 + +# Web server and async +anyio==4.4.0 +sniffio==1.3.1 +websockets==12.0 +watchfiles==0.22.0 +dnspython==2.6.1 +email-validator==2.2.0 + +# Template and markup +Jinja2==3.1.4 +MarkupSafe==2.1.5 +markdown-it-py==3.0.0 +mdurl==0.1.2 +Pygments==2.18.0 + +# Math and scientific computing +sympy==1.12 +mpmath==1.3.0 +networkx==3.2.1 +packaging==24.1 + +# System and CLI +typer==0.12.3 +shellingham==1.5.4 +typing-extensions==4.12.2 +six==1.16.0 +exceptiongroup==1.2.2 diff --git a/src/model-training/requirements-gpu.txt b/src/model-training/requirements-gpu.txt new file mode 100644 index 00000000..6cba67ed --- /dev/null +++ b/src/model-training/requirements-gpu.txt @@ -0,0 +1,101 @@ +# GPU-enabled requirements for model training pipeline +# Note: Requires NVIDIA GPU with CUDA 12.1+ support + +# PyTorch GPU version with CUDA 12.1 support +torch==2.4.0 +torchvision==0.19.0 +torchaudio==2.4.0 +--extra-index-url https://download.pytorch.org/whl/cu121 + +# Core ML and data processing +pandas==2.2.2 +scikit-learn==1.5.1 +numpy==1.24.4 +scipy==1.11.3 + +# Transformers and NLP (with GPU acceleration) +onnx>=1.12.0 +onnxruntime>=1.12.0 +transformers[onnx]>=4.20.0 +tokenizers==0.19.1 +huggingface-hub==0.24.2 +safetensors==0.4.3 +accelerate==0.33.0 +sentencepiece==0.2.0 + +# For Estonian models and multilingual support +sentence-transformers==2.2.2 +langdetect==1.0.9 + +# GPU-specific acceleration libraries +# CUDA toolkit components (if not system-installed) +nvidia-ml-py3==7.352.0 + +# Pydantic and validation +pydantic==2.8.2 +pydantic-core==2.20.1 +annotated-types==0.7.0 + +# FastAPI and web framework +fastapi==0.111.1 +fastapi-cli==0.0.4 +uvicorn[standard]==0.30.5 +starlette==0.37.2 +python-multipart==0.0.9 + +# HTTP and networking +requests==2.32.3 +httpx==0.27.0 +httpcore==1.0.5 +httptools==0.6.1 +h11==0.14.0 +urllib3==2.2.2 +certifi==2024.7.4 +charset-normalizer==3.3.2 +idna==3.7 + +# Utilities and logging +loguru==0.7.2 +click==8.1.7 +tqdm==4.66.4 +rich==13.7.1 +colorama==0.4.6 + +# Configuration and environment +python-dotenv==1.0.1 +PyYAML==6.0.1 + +# File handling and serialization +pillow==10.2.0 +regex==2024.7.24 +filelock==3.15.4 +fsspec==2024.6.1 + +# Web server and async +anyio==4.4.0 +sniffio==1.3.1 +websockets==12.0 +watchfiles==0.22.0 +dnspython==2.6.1 +email-validator==2.2.0 + +# Template and markup +Jinja2==3.1.4 +MarkupSafe==2.1.5 +markdown-it-py==3.0.0 +mdurl==0.1.2 +Pygments==2.18.0 + +# Math and scientific computing +sympy==1.12 +mpmath==1.3.0 +networkx==3.2.1 +packaging==24.1 + +# System and CLI +typer==0.12.3 +shellingham==1.5.4 +typing-extensions==4.12.2 +six==1.16.0 +exceptiongroup==1.2.2 + diff --git a/src/model-training/requirements.txt b/src/model-training/requirements.txt new file mode 100644 index 00000000..660cb46c --- /dev/null +++ b/src/model-training/requirements.txt @@ -0,0 +1,65 @@ +pandas==2.2.2 +scikit_learn==1.5.1 +pydantic==2.8.2 +annotated-types==0.7.0 +accelerate==0.33.0 +anyio==4.4.0 +certifi==2024.7.4 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +dnspython==2.6.1 +email_validator==2.2.0 +exceptiongroup==1.2.2 +fastapi==0.111.1 +fastapi-cli==0.0.4 +filelock==3.15.4 +fsspec==2024.6.1 +h11==0.14.0 +httpcore==1.0.5 +httptools==0.6.1 +httpx==0.27.0 +huggingface-hub==0.24.2 +idna==3.7 +Jinja2==3.1.4 +langdetect==1.0.9 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +mpmath==1.3.0 +networkx==3.2.1 +numpy==1.24.4 +packaging==24.1 +pillow==10.2.0 +pydantic==2.8.2 +pydantic_core==2.20.1 +Pygments==2.18.0 +python-dotenv==1.0.1 +python-multipart==0.0.9 +PyYAML==6.0.1 +regex==2024.7.24 +requests==2.32.3 +rich==13.7.1 +safetensors==0.4.3 +sentencepiece==0.2.0 +sentence-transformers==2.2.2 # For multilingual sentence models +shellingham==1.5.4 +six==1.16.0 +sniffio==1.3.1 +starlette==0.37.2 +sympy==1.12 +tokenizers==0.19.1 +torch==2.4.0 +tqdm==4.66.4 +transformers==4.44.0 +typer==0.12.3 +typing_extensions==4.12.2 +urllib3==2.2.2 +uvicorn==0.30.5 +watchfiles==0.22.0 +websockets==12.0 +loguru==0.7.2 +requests==2.32.3 +scipy==1.11.3 +matplotlib==3.7.2 +seaborn==0.12.2 \ No newline at end of file diff --git a/src/model-training/s3_ferry.py b/src/model-training/s3_ferry.py new file mode 100644 index 00000000..eb5c00b3 --- /dev/null +++ b/src/model-training/s3_ferry.py @@ -0,0 +1,54 @@ +import requests +from loguru import logger +from constants import S3_FERRY_ENDPOINT + +import sys + +from loki_logger import LokiLogger +logger = LokiLogger(service_name="model-trainer-s3-ferry") + +class S3Ferry: + def __init__(self): + # Updated to use correct Docker service name + self.url = S3_FERRY_ENDPOINT + + def transfer_file( + self, + destination_file_path, + destination_storage_type, + source_file_path, + source_storage_type, + ): + payload = self.get_s3_ferry_payload( + destination_file_path, + destination_storage_type, + source_file_path, + source_storage_type, + ) + + + logger.info(f"Transferring file with payload: {payload}") + response = requests.post(self.url, json=payload) + + logger.info(f"S3 FERRY STATUS RESPONSE: {response.text}") + if response.status_code != 200: + logger.error(f"Failed to transfer file: {response.text} to S3") + response.raise_for_status() + else: + logger.info(f"File transferred successfully: {response.json()}") + return response + + def get_s3_ferry_payload( + self, + destination_file_path: str, + destination_storage_type: str, + source_file_path: str, + source_storage_type: str, + ): + S3_FERRY_PAYLOAD = { + "destinationFilePath": destination_file_path, + "destinationStorageType": destination_storage_type, + "sourceFilePath": source_file_path, + "sourceStorageType": source_storage_type, + } + return S3_FERRY_PAYLOAD diff --git a/src/model-training/trainingpipeline.py b/src/model-training/trainingpipeline.py new file mode 100644 index 00000000..5e918630 --- /dev/null +++ b/src/model-training/trainingpipeline.py @@ -0,0 +1,1283 @@ +from transformers import ( + XLMRobertaTokenizer, + XLMRobertaForSequenceClassification, + Trainer, + TrainingArguments, + DistilBertTokenizer, + DistilBertForSequenceClassification, + BertForSequenceClassification, + BertTokenizer, +) +import json +from safetensors.torch import save_file +from torch.utils.data import Dataset +from sklearn.preprocessing import LabelEncoder +from sklearn.model_selection import train_test_split +from sklearn.metrics import accuracy_score, classification_report +import torch +import torch.nn as nn +import torch.nn.functional as F +import shutil +from pathlib import Path +import pandas as pd +import numpy as np +import sys +from typing import Counter, Union +from constants import ( + MODEL_CONFIGS, + SUPPORTED_BASE_MODELS, + SUPPORTED_OOD_METHODS, + DEFAULT_OOD_CONFIGS, + DEFAULT_TRAINING_ARGS, + MIN_SAMPLES_PER_CLASS, + TARGET_SAMPLES_FOR_SMALL_DATASETS, + TEST_SIZE_RATIO, + SEQUENCE_LENGTH, +) +from loguru import logger +import os +from transformers import logging as transformers_logging +import warnings + +warnings.filterwarnings( + "ignore", + message="Some weights of the model checkpoint were not used when initializing", +) +transformers_logging.set_verbosity_error() + + +from loki_logger import LokiLogger +logger = LokiLogger(service_name="model-trainer") + + +class CustomDataset(Dataset): + def __init__(self, encodings, labels, ood_labels=None): + self.encodings = encodings + self.labels = torch.tensor(labels, dtype=torch.long) + self.ood_labels = ( + torch.tensor(ood_labels, dtype=torch.long) + if ood_labels is not None + else None + ) + + def __getitem__(self, idx): + item = {key: val[idx] for key, val in self.encodings.items()} + item["labels"] = self.labels[idx] + if self.ood_labels is not None: + item["ood_labels"] = self.ood_labels[idx] + return item + + def __len__(self): + return len(self.labels) + + +class SpectralNormalization(nn.Module): + """Spectral normalization layer for improved uncertainty estimation""" + + def __init__(self, layer, n_power_iterations=1, eps=1e-12): + super().__init__() + self.layer = layer + self.n_power_iterations = n_power_iterations + self.eps = eps + + # Initialize spectral norm + with torch.no_grad(): + weight = getattr(layer, "weight") + h, w = weight.size() + u = nn.Parameter(torch.randn(h), requires_grad=False) + v = nn.Parameter(torch.randn(w), requires_grad=False) + self.register_parameter("u", u) + self.register_parameter("v", v) + + def forward(self, x): + if self.training: + self._update_uv() + return self.layer(x) + + def _update_uv(self): + weight = getattr(self.layer, "weight") + with torch.no_grad(): + for _ in range(self.n_power_iterations): + self.v.data = F.normalize( + torch.mv(weight.t(), self.u), dim=0, eps=self.eps + ) + self.u.data = F.normalize(torch.mv(weight, self.v), dim=0, eps=self.eps) + + sigma = torch.dot(self.u, torch.mv(weight, self.v)) + weight.data /= sigma + + +class OODLoss(nn.Module): + """Combined loss for OOD detection""" + + def __init__(self, ood_weight=0.1, energy_margin=10.0, temperature=1.0): + super().__init__() + self.ood_weight = ood_weight + self.energy_margin = energy_margin + self.temperature = temperature + self.ce_loss = nn.CrossEntropyLoss() + + def forward(self, logits, labels, ood_labels=None): + # Standard classification loss + ce_loss = self.ce_loss(logits, labels) + + if ood_labels is None: + return ce_loss + + # Energy-based OOD loss + energy = -torch.logsumexp(logits / self.temperature, dim=-1) + + # Separate ID and OOD samples + is_ood = (ood_labels == 1).float() + is_id = 1.0 - is_ood + + # Energy loss: low energy for ID, high energy for OOD + energy_loss = ( + torch.relu(self.energy_margin - energy) * is_ood # High energy for OOD + + energy * is_id # Low energy for ID + ) + + total_loss = ce_loss + self.ood_weight * energy_loss.mean() + return total_loss + + +class EnhancedModel(nn.Module): + """Enhanced model with optional OOD detection capabilities""" + + def __init__( + self, + base_model, + num_labels, + use_spectral_norm=False, + hidden_dim=768, + dropout_rate=0.1, + ): + super().__init__() + self.base_model = base_model + self.num_labels = num_labels + self.use_spectral_norm = use_spectral_norm + self.hidden_dim = hidden_dim + self.dropout_rate = dropout_rate + + # Get the hidden size from the base model + if hasattr(base_model.config, "hidden_size"): + self.hidden_size = base_model.config.hidden_size + else: + self.hidden_size = hidden_dim + + # Additional layers for uncertainty estimation + if use_spectral_norm: + self.uncertainty_head = nn.Sequential( + nn.Dropout(dropout_rate), + SpectralNormalization(nn.Linear(self.hidden_size, hidden_dim)), + nn.ReLU(), + nn.Dropout(dropout_rate), + SpectralNormalization(nn.Linear(hidden_dim, num_labels)), + ) + else: + self.uncertainty_head = None + + def forward(self, input_ids, attention_mask=None, labels=None, **kwargs): + # Get outputs from base model + outputs = self.base_model( + input_ids=input_ids, attention_mask=attention_mask, **kwargs + ) + + if self.uncertainty_head is not None: + # Extract hidden representation for uncertainty head + hidden_state = None + # Try different ways to get the hidden representation + if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None: + hidden_state = outputs.pooler_output + elif hasattr(outputs, "last_hidden_state"): + # Use [CLS] token representation (first token) + hidden_state = outputs.last_hidden_state[:, 0, :] + elif ( + hasattr(outputs, "hidden_states") and outputs.hidden_states is not None + ): + # Use the last hidden state if available + hidden_state = outputs.hidden_states[-1][:, 0, :] + else: + # Fallback: try to find any tensor that looks like hidden states + for attr_name in ["logits", "prediction_logits"]: + if hasattr(outputs, attr_name): + attr_value = getattr(outputs, attr_name) + if ( + isinstance(attr_value, torch.Tensor) + and len(attr_value.shape) >= 2 + ): + # Use the raw logits and add a linear layer to get hidden representation + if attr_value.shape[-1] == self.num_labels: + # This is likely the classification logits, skip uncertainty head + break + else: + raise ValueError( + f"Could not extract hidden state from model outputs: {type(outputs)}" + ) + + if hidden_state is not None: + # Apply uncertainty head + logits = self.uncertainty_head(hidden_state) + # Create a new output object with updated logits + outputs.logits = logits + + return outputs + + def save_pretrained( + self, + save_directory: Union[str, os.PathLike], + push_to_hub: bool = False, + safe_serialization: bool = True, + **kwargs, + ): + """ + Save the model to a directory, following transformers conventions. + + Args: + save_directory: Directory where to save the model + push_to_hub: Whether to push to huggingface hub (not implemented) + safe_serialization: Whether to use safetensors format + **kwargs: Additional arguments passed to base_model.save_pretrained + """ + save_directory = Path(save_directory) + save_directory.mkdir(parents=True, exist_ok=True) + if hasattr(self.base_model, "distilbert"): + backbone = self.base_model.distilbert + elif hasattr(self.base_model, "bert"): + backbone = self.base_model.bert + elif hasattr(self.base_model, "roberta"): + backbone = self.base_model.roberta + else: + # Fallback: save the whole base model but this might cause issues + backbone = self.base_model + logger.warning("Could not extract backbone - saving full base model") + # Save the base model first + base_model_dir = save_directory / "base_model" + backbone.save_pretrained(base_model_dir, **kwargs) + + # Save the enhanced model state dict + if safe_serialization: + try: + # Save only the uncertainty head if it exists + if self.uncertainty_head is not None: + uncertainty_state = self.uncertainty_head.state_dict() + save_file( + uncertainty_state, + save_directory / "uncertainty_head.safetensors", + ) + except ImportError: + # Fallback to regular torch save + safe_serialization = False + + if not safe_serialization: + if self.uncertainty_head is not None: + torch.save( + self.uncertainty_head.state_dict(), + save_directory / "uncertainty_head.bin", + ) + + # Save the model configuration + config = { + "model_type": "enhanced_model", + "num_labels": self.num_labels, + "use_spectral_norm": self.use_spectral_norm, + "hidden_dim": self.hidden_dim, + "dropout_rate": self.dropout_rate, + "hidden_size": self.hidden_size, + "base_model_type": self.base_model.__class__.__name__, + "transformers_version": getattr(self.base_model, "_version", None), + } + + # Add base model config if available + if hasattr(self.base_model, "config"): + config["base_model_config"] = self.base_model.config.to_dict() + + with open(save_directory / "enhanced_config.json", "w") as f: + json.dump(config, f, indent=2) + + print(f"Enhanced model saved to {save_directory}") + + @classmethod + def from_pretrained( + cls, + pretrained_model_path: Union[str, os.PathLike], + base_model_class=None, + **kwargs, + ): + """ + Load an enhanced model from a directory. + + Args: + pretrained_model_path: Path to the saved model directory + base_model_class: The class to use for loading the base model + **kwargs: Additional arguments + """ + pretrained_model_path = Path(pretrained_model_path) + + # Load configuration + with open(pretrained_model_path / "enhanced_config.json", "r") as f: + config = json.load(f) + + # Load base model + base_model_dir = pretrained_model_path / "base_model" + if base_model_class is not None: + base_model = base_model_class.from_pretrained(base_model_dir) + else: + # Try to infer the base model class from transformers + try: + from transformers import AutoModel + + base_model = AutoModel.from_pretrained(base_model_dir) + except Exception as e: + raise ValueError( + f"Could not load base model. Please provide base_model_class. Error: {e}" + ) + + # Create enhanced model instance + model = cls( + base_model=base_model, + num_labels=config["num_labels"], + use_spectral_norm=config["use_spectral_norm"], + hidden_dim=config["hidden_dim"], + dropout_rate=config["dropout_rate"], + ) + + # Load uncertainty head if it exists + if config["use_spectral_norm"] and model.uncertainty_head is not None: + uncertainty_head_path = None + if (pretrained_model_path / "uncertainty_head.safetensors").exists(): + try: + from safetensors.torch import load_file + + uncertainty_state = load_file( + pretrained_model_path / "uncertainty_head.safetensors" + ) + model.uncertainty_head.load_state_dict(uncertainty_state) + except ImportError: + uncertainty_head_path = ( + pretrained_model_path / "uncertainty_head.bin" + ) + elif (pretrained_model_path / "uncertainty_head.bin").exists(): + uncertainty_head_path = pretrained_model_path / "uncertainty_head.bin" + + if uncertainty_head_path: + uncertainty_state = torch.load( + uncertainty_head_path, map_location="cpu" + ) + model.uncertainty_head.load_state_dict(uncertainty_state) + + return model + + +class EnhancedModelInference(nn.Module): + """ + Inference-only version of EnhancedModel - ONNX compatible + """ + + def __init__(self, base_model, num_labels, hidden_dim=768, dropout_rate=0.1): + super().__init__() + self.base_model = base_model + self.num_labels = num_labels + self.hidden_dim = hidden_dim + self.dropout_rate = dropout_rate + + # Get the hidden size from the base model + if hasattr(base_model.config, "hidden_size"): + self.hidden_size = base_model.config.hidden_size + else: + self.hidden_size = hidden_dim + + # Simple uncertainty head - no spectral norm wrapper needed + # The weights will be copied from the trained spectral normalized layers + self.uncertainty_head = nn.Sequential( + nn.Dropout(dropout_rate), + nn.Linear(self.hidden_size, hidden_dim), + nn.ReLU(), + nn.Dropout(dropout_rate), + nn.Linear(hidden_dim, num_labels), + ) + + def _model_supports_token_type_ids(self): + """ + Check if the base model supports token_type_ids + """ + if hasattr(self.base_model, "config") and hasattr( + self.base_model.config, "model_type" + ): + model_type = self.base_model.config.model_type + + # Models that explicitly reject token_type_ids + if model_type in ["distilbert"]: + return False + + # Models that use token_type_ids + if model_type in ["bert"]: + return True + + # Models that accept but ignore token_type_ids (like xlm-roberta, roberta) + if model_type in ["xlm-roberta", "roberta"]: + return True # They accept it even though they ignore it + + # Fallback: check if model has type_vocab_size > 1 + if hasattr(self.base_model, "config") and hasattr( + self.base_model.config, "type_vocab_size" + ): + return self.base_model.config.type_vocab_size > 1 + + # Default: try to pass it (most models accept it) + return True + + def forward( + self, input_ids, attention_mask, token_type_ids=None, labels=None, **kwargs + ): + # Get outputs from base model + + if self._model_supports_token_type_ids(): + outputs = self.base_model( + input_ids=input_ids, + attention_mask=attention_mask, + token_type_ids=token_type_ids, + ) + else: + outputs = self.base_model( + input_ids=input_ids, attention_mask=attention_mask + ) + + # Extract hidden representation + if hasattr(outputs, "pooler_output") and outputs.pooler_output is not None: + hidden_state = outputs.pooler_output + elif hasattr(outputs, "last_hidden_state"): + hidden_state = outputs.last_hidden_state[:, 0, :] + else: + # Fallback for ONNX + batch_size = input_ids.shape[0] + hidden_state = torch.zeros( + batch_size, self.hidden_size, device=input_ids.device + ) + + # Apply uncertainty head (weights are already spectrally normalized!) + logits = self.uncertainty_head(hidden_state) + + from transformers.modeling_outputs import SequenceClassifierOutput + + return SequenceClassifierOutput( + logits=logits, + hidden_states=getattr(outputs, "hidden_states", None), + attentions=getattr(outputs, "attentions", None), + ) + + def save_pretrained(self, save_directory, tokenizer=None, **kwargs): + """Save inference model""" + save_directory = Path(save_directory) + save_directory.mkdir(parents=True, exist_ok=True) + + # Save the base model + base_model_dir = save_directory / "base_model" + self.base_model.save_pretrained(base_model_dir, **kwargs) + + # Save tokenizer if provided + if tokenizer is not None: + tokenizer.save_pretrained(save_directory) + tokenizer.save_pretrained(base_model_dir) + + # Save uncertainty head + torch.save( + self.uncertainty_head.state_dict(), + save_directory / "uncertainty_head.bin", + ) + + # Save inference config + config = { + "model_type": "enhanced_model_inference", + "num_labels": self.num_labels, + "hidden_dim": self.hidden_dim, + "dropout_rate": self.dropout_rate, + "hidden_size": self.hidden_size, + "base_model_type": self.base_model.__class__.__name__, + } + + if hasattr(self.base_model, "config"): + config["base_model_config"] = self.base_model.config.to_dict() + + with open(save_directory / "inference_config.json", "w") as f: + json.dump(config, f, indent=2) + + logger.info(f"Inference model saved to {save_directory}") + + @classmethod + def from_pretrained(cls, pretrained_model_path, **kwargs): + """Load inference model""" + pretrained_model_path = Path(pretrained_model_path) + + # Load configuration + with open(pretrained_model_path / "inference_config.json", "r") as f: + config = json.load(f) + + # Load base model + base_model_dir = pretrained_model_path / "base_model" + from transformers import AutoModel + + base_model = AutoModel.from_pretrained(base_model_dir) + + # Create inference model instance + model = cls( + base_model=base_model, + num_labels=config["num_labels"], + hidden_dim=config["hidden_dim"], + dropout_rate=config["dropout_rate"], + ) + + # Load uncertainty head + uncertainty_head_path = pretrained_model_path / "uncertainty_head.bin" + if uncertainty_head_path.exists(): + uncertainty_state = torch.load(uncertainty_head_path, map_location="cpu") + model.uncertainty_head.load_state_dict(uncertainty_state) + + return model + + +def export_inference_model_to_onnx(model_dir: str): + """ + Export inference model to ONNX - much simpler now! + """ + try: + from transformers import AutoTokenizer + import torch + + model_dir = Path(model_dir) + + # Load inference model + inference_model = EnhancedModelInference.from_pretrained(model_dir) + inference_model.eval() + + # Load tokenizer + tokenizer = AutoTokenizer.from_pretrained(model_dir) + + # Create dummy input + dummy_input = tokenizer( + "This is a sample text for ONNX export.", + return_tensors="pt", + return_token_type_ids=True, + return_attention_mask=True, + padding=True, + truncation=True, + max_length=SEQUENCE_LENGTH, + ) + output_path = model_dir / "model.onnx" + + # Export to ONNX (should work smoothly now!) + if inference_model._model_supports_token_type_ids(): + torch.onnx.export( + inference_model, + ( + dummy_input["input_ids"], + dummy_input["attention_mask"], + dummy_input["token_type_ids"], + ), + str(output_path), + input_names=["input_ids", "attention_mask", "token_type_ids"], + output_names=["logits"], + dynamic_axes={ + "input_ids": {0: "batch_size", 1: "sequence_length"}, + "attention_mask": {0: "batch_size", 1: "sequence_length"}, + "token_type_ids": {0: "batch_size", 1: "sequence_length"}, + "logits": {0: "batch_size"}, + }, + opset_version=14, + do_constant_folding=True, + export_params=True, + ) + else: + torch.onnx.export( + inference_model, + ( + dummy_input["input_ids"], + dummy_input["attention_mask"], + ), + str(output_path), + input_names=["input_ids", "attention_mask"], + output_names=["logits"], + dynamic_axes={ + "input_ids": {0: "batch_size", 1: "sequence_length"}, + "attention_mask": {0: "batch_size", 1: "sequence_length"}, + "logits": {0: "batch_size"}, + }, + opset_version=13, + do_constant_folding=True, + export_params=True, + ) + + with torch.no_grad(): + test_output = inference_model( + dummy_input["input_ids"], + dummy_input["attention_mask"], + dummy_input["token_type_ids"], + ) + print(test_output) + print("Forward pass successful") + + if output_path.exists(): + logger.info(f"ONNX export successful: {output_path}") + return str(output_path) + else: + return None + + except Exception as e: + logger.error(f"ONNX export failed: {e}") + return None + + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +logger.info(f"TRAINING HARDWARE {device}") + + +class TrainingPipeline: + def __init__( + self, dfs, model_name, full_name=None, ood_method=None, ood_config=None + ): + self.model_name = model_name + self.dfs = dfs + self.ood_method = ood_method + self.full_name = full_name + self.ood_config = ood_config or {} + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + # Validate model name + if model_name not in SUPPORTED_BASE_MODELS: + raise ValueError( + f"Unsupported model: {model_name}. Supported: {SUPPORTED_BASE_MODELS}" + ) + + # Initialize base model + self.base_model = self._initialize_base_model(model_name) + + def _initialize_base_model(self, model_name): + """Initialize the base model based on model name""" + config = MODEL_CONFIGS[model_name] + + if config["type"] == "bert": + model = BertForSequenceClassification.from_pretrained(config["model_name"]) + self._freeze_and_unfreeze_bert_layers(model) + + elif config["type"] == "roberta": + model = XLMRobertaForSequenceClassification.from_pretrained( + config["model_name"] + ) + self._freeze_and_unfreeze_roberta_layers(model) + + elif config["type"] == "distilbert": + model = DistilBertForSequenceClassification.from_pretrained( + config["model_name"] + ) + self._freeze_and_unfreeze_distilbert_layers(model) + + else: + raise ValueError(f"Unknown model type: {config['type']}") + + return model + + def _freeze_and_unfreeze_bert_layers(self, model): + """Helper method for BERT-based models""" + if hasattr(model, "bert"): + bert_model = model.bert + elif hasattr(model, "base_model"): + bert_model = model.base_model + else: + # Fallback - assume the model itself is the BERT model + bert_model = model + + # Freeze all parameters first + for param in bert_model.parameters(): + param.requires_grad = False + + # Unfreeze last 2 layers + if hasattr(bert_model, "encoder") and hasattr(bert_model.encoder, "layer"): + for param in bert_model.encoder.layer[-2:].parameters(): + param.requires_grad = True + + # Unfreeze classifier if it exists + if hasattr(model, "classifier"): + for param in model.classifier.parameters(): + param.requires_grad = True + + def _freeze_and_unfreeze_roberta_layers(self, model): + """Helper method for RoBERTa-based models""" + if hasattr(model, "roberta"): + roberta_model = model.roberta + elif hasattr(model, "base_model"): + roberta_model = model.base_model + elif hasattr(model, "encoder"): + roberta_model = model + else: + logger.warning(f"RoBERTa model structure: {type(model)}") + for name, module in model.named_children(): + logger.info(f" - {name}: {type(module)}") + if hasattr(module, "encoder"): + roberta_model = module + break + else: + roberta_model = model + + # Freeze all parameters first + for param in roberta_model.parameters(): + param.requires_grad = False + + encoder = None + if hasattr(roberta_model, "encoder"): + encoder = roberta_model.encoder + elif hasattr(roberta_model, "transformer"): + encoder = roberta_model.transformer + + if encoder and hasattr(encoder, "layer"): + for param in encoder.layer[-2:].parameters(): + param.requires_grad = True + else: + logger.warning("Could not find encoder layers to unfreeze") + + # Unfreeze classifier if it exists + if hasattr(model, "classifier"): + for param in model.classifier.parameters(): + param.requires_grad = True + + def _freeze_and_unfreeze_distilbert_layers(self, model): + """Helper method for DistilBERT-based models""" + if hasattr(model, "distilbert"): + distilbert_model = model.distilbert + elif hasattr(model, "base_model"): + distilbert_model = model.base_model + else: + distilbert_model = model + + # Freeze all parameters first + for param in distilbert_model.parameters(): + param.requires_grad = False + + # Unfreeze last 2 layers + if hasattr(distilbert_model, "transformer") and hasattr( + distilbert_model.transformer, "layer" + ): + for param in distilbert_model.transformer.layer[-2:].parameters(): + param.requires_grad = True + + # Unfreeze classifier if it exists + if hasattr(model, "classifier"): + for param in model.classifier.parameters(): + param.requires_grad = True + + def preprocess_conversation(self, text): + """Preprocess conversation data""" + text = str(text).strip() + + # Handle common conversation patterns + text = text.replace("\n", " [SEP] ") + text = text.replace("\t", " ") + + # Limit length for transformer models + max_length = 400 + if len(text.split()) > max_length: + words = text.split()[:max_length] + text = " ".join(words) + + return text + + def tokenize_data(self, data, tokenizer): + """Enhanced tokenization for conversation data""" + processed_data = [self.preprocess_conversation(text) for text in data] + + tokenized = tokenizer.batch_encode_plus( + processed_data, + truncation=True, + padding=True, + max_length=SEQUENCE_LENGTH, + return_token_type_ids=False, + return_attention_mask=True, + return_tensors="pt", + ) + return tokenized + + def data_split(self, df): + """Improved data split for flat classification""" + unique_classes = df["target"].unique() + + if len(df) < 100: + # For small datasets, ensure each class has representation + train_samples = [] + test_samples = [] + + for class_name in unique_classes: + class_samples = df[df["target"] == class_name] + + if len(class_samples) == 1: + train_samples.append(class_samples) + else: + train_class, test_class = train_test_split( + class_samples, test_size=TEST_SIZE_RATIO, random_state=42 + ) + train_samples.append(train_class) + test_samples.append(test_class) + + train_df = pd.concat(train_samples) if train_samples else pd.DataFrame() + test_df = pd.concat(test_samples) if test_samples else pd.DataFrame() + + # If test set is empty, duplicate some training samples + if len(test_df) == 0: + test_df = train_df.sample(min(len(train_df), 5), random_state=42) + else: + # For larger datasets, use stratified split + try: + train_df, test_df = train_test_split( + df, + test_size=TEST_SIZE_RATIO, + random_state=42, + stratify=df["target"], + ) + except ValueError: + train_df, test_df = train_test_split( + df, test_size=TEST_SIZE_RATIO, random_state=42 + ) + + return train_df, test_df + + def get_tokenizer_and_model_for_training(self, model_name, num_labels): + """Get appropriate tokenizer and model for training""" + config = MODEL_CONFIGS[model_name] + + if config["type"] == "bert": + model = BertForSequenceClassification.from_pretrained( + config["model_name"], + num_labels=num_labels, + ignore_mismatched_sizes=True, + ) + tokenizer = BertTokenizer.from_pretrained(config["tokenizer_name"]) + self._freeze_and_unfreeze_bert_layers(model) + + elif config["type"] == "roberta": + model = XLMRobertaForSequenceClassification.from_pretrained( + config["model_name"], + num_labels=num_labels, + ignore_mismatched_sizes=True, + ) + tokenizer = XLMRobertaTokenizer.from_pretrained(config["tokenizer_name"]) + self._freeze_and_unfreeze_roberta_layers(model) + + elif config["type"] == "distilbert": + model = DistilBertForSequenceClassification.from_pretrained( + config["model_name"], + num_labels=num_labels, + ignore_mismatched_sizes=True, + ) + tokenizer = DistilBertTokenizer.from_pretrained(config["tokenizer_name"]) + self._freeze_and_unfreeze_distilbert_layers(model) + + else: + raise ValueError(f"Unknown model type: {config['type']}") + + return model, tokenizer + + def replicate_data(self, df, target_size): + """Replicate data to reach target size""" + if len(df) >= target_size: + return df + + multiplier = target_size // len(df) + 1 + replicated_dfs = [df] * multiplier + replicated_df = pd.concat(replicated_dfs, ignore_index=True) + replicated_df = replicated_df.sample(n=target_size, random_state=42) + return replicated_df + + def extract_model_components(self, model): + """Extract model components based on model type with robust error handling""" + config = MODEL_CONFIGS[self.model_name] + + try: + if config["type"] == "bert": + # Try different possible BERT model structures + bert_layers = None + if hasattr(model, "bert") and hasattr(model.bert, "encoder"): + bert_layers = model.bert.encoder.layer[-2:].state_dict() + elif hasattr(model, "base_model") and hasattr( + model.base_model, "encoder" + ): + bert_layers = model.base_model.encoder.layer[-2:].state_dict() + else: + logger.warning( + "Could not extract BERT layers, using pattern matching" + ) + bert_layers = { + k: v + for k, v in model.state_dict().items() + if "encoder.layer.10" in k or "encoder.layer.11" in k + } + + classifier_layers = ( + model.classifier.state_dict() + if hasattr(model, "classifier") + else {} + ) + return bert_layers, classifier_layers + + elif config["type"] == "roberta": + # Handle XLM-RoBERTa structure + roberta_layers = None + + # First try to find the main transformer layers + if hasattr(model, "roberta") and hasattr(model.roberta, "encoder"): + roberta_layers = model.roberta.encoder.layer[-2:].state_dict() + elif hasattr(model, "base_model") and hasattr( + model.base_model, "encoder" + ): + roberta_layers = model.base_model.encoder.layer[-2:].state_dict() + else: + # For XLM-RoBERTa, extract by parameter name pattern + logger.warning("Using pattern matching for XLM-RoBERTa layers") + roberta_layers = {} + for name, param in model.named_parameters(): + # Extract last 2 encoder layers (typically layer 10 and 11 for base models) + if "encoder.layer.10." in name or "encoder.layer.11." in name: + # Remove the model prefix to get relative layer name + layer_name = name.split("encoder.layer.")[-1] + roberta_layers[f"encoder.layer.{layer_name}"] = ( + param.data.clone() + ) + + classifier_layers = ( + model.classifier.state_dict() + if hasattr(model, "classifier") + else {} + ) + return roberta_layers, classifier_layers + + elif config["type"] == "distilbert": + # Try different possible DistilBERT model structures + distilbert_layers = None + if hasattr(model, "distilbert") and hasattr( + model.distilbert, "transformer" + ): + distilbert_layers = model.distilbert.transformer.layer[ + -2: + ].state_dict() + elif hasattr(model, "base_model") and hasattr( + model.base_model, "transformer" + ): + distilbert_layers = model.base_model.transformer.layer[ + -2: + ].state_dict() + else: + logger.warning( + "Could not extract DistilBERT layers, using pattern matching" + ) + distilbert_layers = { + k: v + for k, v in model.state_dict().items() + if "transformer.layer.4." in k or "transformer.layer.5." in k + } # DistilBERT has 6 layers + + classifier_layers = ( + model.classifier.state_dict() + if hasattr(model, "classifier") + else {} + ) + return distilbert_layers, classifier_layers + + else: + raise ValueError(f"Unknown model type: {config['type']}") + + except Exception as e: + logger.error(f"Error extracting model components: {e}") + # Fallback: return relevant parts of model state dict + logger.warning("Using fallback: extracting layers by name pattern") + + model_layers = {} + classifier_layers = {} + + for name, param in model.named_parameters(): + if "classifier" in name: + classifier_layers[name] = param.data.clone() + elif "encoder.layer." in name and any( + f"encoder.layer.{i}." in name for i in [10, 11, 4, 5] + ): # Last layers for different models + model_layers[name] = param.data.clone() + + return model_layers, classifier_layers + + def train(self): + """Standard training method""" + classes = [] + accuracies = [] + f1_scores = [] + label_encoders = [] + + method_name = f"{self.model_name}" + ( + f"-{self.ood_method}" if self.ood_method else "" + ) + logger.info(f"INITIATING TRAINING FOR {method_name}") + + for i in range(len(self.dfs)): + logger.info(f"TRAINING FOR DATAFRAME {i + 1} of {len(self.dfs)}") + current_df = self.dfs[i] + + if len(current_df) < MIN_SAMPLES_PER_CLASS: + current_df = self.replicate_data( + current_df, TARGET_SAMPLES_FOR_SMALL_DATASETS + ).reset_index(drop=True) + + train_df, test_df = self.data_split(current_df) + label_encoder = LabelEncoder() + train_labels = label_encoder.fit_transform(train_df["target"]) + test_labels = label_encoder.transform(test_df["target"]) + self.model_label2id = {} + + for idx, label in enumerate(label_encoder.classes_): + self.model_label2id[label] = idx + self.model_id2label = {v: k for k, v in self.model_label2id.items()} + self.agency_label2id = {} + agency_ids_column = "agency_id" + for i, row in train_df.iterrows(): + agency_id = row[agency_ids_column] + if agency_id not in self.agency_label2id: + self.agency_label2id[agency_id] = row["target"] + self.agency_id2label = {v: k for k, v in self.agency_label2id.items()} + print( + self.agency_label2id, + self.agency_id2label, + self.model_label2id, + self.model_id2label, + ) + # Get model and tokenizer + model, tokenizer = self.get_tokenizer_and_model_for_training( + self.model_name, len(label_encoder.classes_) + ) + + # Apply OOD enhancements if specified + if self.ood_method == "sngp": + logger.info("Applying spectral normalization enhancement") + model = EnhancedModel( + base_model=model, + num_labels=len(label_encoder.classes_), + use_spectral_norm=True, + ) + + train_encodings = self.tokenize_data(train_df["input"].tolist(), tokenizer) + test_encodings = self.tokenize_data(test_df["input"].tolist(), tokenizer) + + # Prepare OOD labels if needed + train_ood_labels = None + test_ood_labels = None + if self.ood_method in ["energy", "sngp"]: + # All samples are ID (in practice, you'd have real OOD data) + train_ood_labels = np.zeros(len(train_labels)) + test_ood_labels = np.zeros(len(test_labels)) + + train_dataset = CustomDataset( + train_encodings, train_labels, train_ood_labels + ) + + test_dataset = CustomDataset(test_encodings, test_labels, test_ood_labels) + + # Setup training arguments + training_args = TrainingArguments( + output_dir="tmp", + **DEFAULT_TRAINING_ARGS, + disable_tqdm=False, + ) + + # Use custom trainer for OOD methods + if self.ood_method in ["energy", "sngp"]: + trainer = self._get_ood_trainer( + model, training_args, train_dataset, test_dataset + ) + else: + trainer = Trainer( + model=model, + args=training_args, + train_dataset=train_dataset, + eval_dataset=test_dataset, + compute_metrics=lambda eval_pred: { + "accuracy": accuracy_score( + eval_pred.label_ids, eval_pred.predictions.argmax(axis=1) + ) + }, + ) + + trainer.train() + # save the model + from constants import MODEL_RESULTS_PATH + + # save the model + model_dir = f"{MODEL_RESULTS_PATH}/{method_name}" + os.makedirs(model_dir, exist_ok=True) + inference_model = EnhancedModelInference( + base_model=model.base_model, + num_labels=len(label_encoder.classes_), + hidden_dim=model.hidden_dim, + dropout_rate=model.dropout_rate, + ) + if ( + hasattr(model, "uncertainty_head") + and model.uncertainty_head is not None + ): + # Extract weights from spectral normalized layers + training_state = model.uncertainty_head.state_dict() + + # Map spectral norm weights to regular linear layers + inference_state = {} + + # Map from SpectralNormalization layers to regular Linear layers + layer_mappings = { + "1.layer.weight": "1.weight", # First spectral norm -> first linear + "1.layer.bias": "1.bias", + "4.layer.weight": "4.weight", # Second spectral norm -> second linear + "4.layer.bias": "4.bias", + } + + for training_key, inference_key in layer_mappings.items(): + if training_key in training_state: + inference_state[inference_key] = training_state[training_key] + + # Load the mapped weights + inference_model.uncertainty_head.load_state_dict( + inference_state, strict=False + ) + logger.info( + "Successfully transferred spectrally normalized weights to inference model" + ) + + inference_model.save_pretrained(model_dir, tokenizer=tokenizer) + tokenizer.save_pretrained(model_dir) + logger.info(f"Model saved to {model_dir}") + # save labelmappings, ood config to config.json + config = { + "num_labels": len(label_encoder.classes_), + "model_name": self.full_name, + "hidden_dim": model.hidden_dim, + "dropout_rate": model.dropout_rate, + "sequence_length": SEQUENCE_LENGTH, + "ood_method": self.ood_method, + "ood_config": self.ood_config, + "base_model_name": self.model_name, + "base_model_type": model.base_model.__class__.__name__, + "model_label2id": self.model_label2id, + "model_id2label": self.model_id2label, + "agency_label2id": self.agency_label2id, + "agency_id2label": self.agency_id2label, + } + import json + + with open(os.path.join(model_dir, "config.json"), "w") as f: + json.dump(config, f, indent=2) + + # Evaluate model + predictions, labels, _ = trainer.predict(test_dataset) + predictions = predictions.argmax(axis=-1) + report = classification_report( + labels, + predictions, + target_names=label_encoder.classes_, + output_dict=True, + zero_division=0, + ) + + # Log results + logger.info(f"Classification Results for {method_name}:") + for cls in label_encoder.classes_: + if cls in report: + precision = report[cls]["precision"] + f1 = report[cls]["f1-score"] + logger.info( + f" Class '{cls}': Precision={precision:.3f}, F1={f1:.3f}" + ) + + classes.append(cls) + accuracies.append(precision) + f1_scores.append(f1) + + label_encoders.append(label_encoder) + + # Clean up + if os.path.exists("tmp"): + shutil.rmtree("tmp") + + metrics = (classes, accuracies, f1_scores) + return model_dir, metrics + + def _get_ood_trainer(self, model, training_args, train_dataset, test_dataset): + """Get custom trainer for OOD methods""" + + class OODTrainer(Trainer): + def __init__(self, ood_method, ood_config, **kwargs): + super().__init__(**kwargs) + self.ood_method = ood_method + self.ood_config = ood_config + + if ood_method == "energy": + config = DEFAULT_OOD_CONFIGS["energy"] + config.update(ood_config) + self.ood_loss = OODLoss( + ood_weight=config.get("energy_weight", 0.1), + energy_margin=config.get("energy_margin", 10.0), + temperature=config.get("energy_temp", 1.0), + ) + + def compute_loss(self, model, inputs, return_outputs=False): + labels = inputs.get("labels") + ood_labels = inputs.get("ood_labels") + + outputs = model( + **{ + k: v + for k, v in inputs.items() + if k not in ["labels", "ood_labels"] + } + ) + logits = outputs.get("logits") + + if ood_labels is not None and self.ood_method == "energy": + loss = self.ood_loss(logits, labels, ood_labels) + else: + loss_fct = nn.CrossEntropyLoss() + loss = loss_fct(logits.view(-1, logits.size(-1)), labels.view(-1)) + + return (loss, outputs) if return_outputs else loss + + return OODTrainer( + ood_method=self.ood_method, + ood_config=self.ood_config, + model=model, + args=training_args, + train_dataset=train_dataset, + eval_dataset=test_dataset, + compute_metrics=lambda eval_pred: { + "accuracy": accuracy_score( + eval_pred.label_ids, eval_pred.predictions.argmax(axis=1) + ) + }, + ) + + +def create_training_pipeline( + dfs, model_name, full_name=None, ood_method=None, **ood_config +): + """ + Factory function to create training pipelines. + + Args: + dfs: List of dataframes + model_name: Name of the base model + ood_method: OOD method ("energy", "sngp", "softmax", or None for standard) + **ood_config: Additional OOD configuration parameters + + Returns: + TrainingPipeline: Configured training pipeline + """ + if ood_method and ood_method not in SUPPORTED_OOD_METHODS: + raise ValueError( + f"Unsupported OOD method: {ood_method}. Supported: {SUPPORTED_OOD_METHODS}" + ) + + return TrainingPipeline( + dfs=dfs, + model_name=model_name, + full_name=full_name, + ood_method=ood_method, + ood_config=ood_config, + ) diff --git a/src/s3_dataset_processor/__init__.py b/src/s3_dataset_processor/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/s3_dataset_processor/config/__init__.py b/src/s3_dataset_processor/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/s3_dataset_processor/config/settings.py b/src/s3_dataset_processor/config/settings.py new file mode 100644 index 00000000..9ccc2b67 --- /dev/null +++ b/src/s3_dataset_processor/config/settings.py @@ -0,0 +1,27 @@ +"""Application configuration settings.""" + +import os + + +class Settings: + """Application settings and configuration.""" + + # API Configuration + API_TITLE = "S3 Dataset Processor API" + API_DESCRIPTION = "API for decoding and processing S3 presigned URLs" + API_VERSION = "1.0.0" + + # Directory Configuration + DATA_DIR = "/app/data" + + # Download Configuration + DOWNLOAD_TIMEOUT = 300 # 5 minutes + CHUNK_SIZE = 8192 + + def __init__(self): + """Initialize settings and create necessary directories.""" + os.makedirs(self.DATA_DIR, exist_ok=True) + + +# Global settings instance +settings = Settings() diff --git a/src/s3_dataset_processor/constants.py b/src/s3_dataset_processor/constants.py new file mode 100644 index 00000000..cfa2b978 --- /dev/null +++ b/src/s3_dataset_processor/constants.py @@ -0,0 +1,15 @@ +from pathlib import Path + +# --- Configuration --- +DATASET_UPDATE_URL = "http://ruuter-public:8086/global-classifier/datasets/update" +STATUS_UPDATE_URL = ( + "http://ruuter-public:8086/global-classifier/agencies/data/generation" +) +PROGRESS_UPDATE_URL = ( + "http://ruuter-public:8086/global-classifier/datasets/progress/update" +) +SCRIPT_DIR = Path("/app/src/s3_dataset_processor") +AGGREGATED_CSV_FILE = "aggregated_dataset.csv" +SYNCED_WITH_CKB = "Synced_with_CKB" +SYNC_WITH_CKB_FAILED = "Sync_with_CKB_Failed" +OUTPUT_DATA_DIR = "/app/output_datasets" diff --git a/src/s3_dataset_processor/dataset_generation_callback_processor.py b/src/s3_dataset_processor/dataset_generation_callback_processor.py new file mode 100644 index 00000000..f8624e4c --- /dev/null +++ b/src/s3_dataset_processor/dataset_generation_callback_processor.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 +""" +Standalone script for processing dataset generation callbacks. +Uploads only the aggregated CSV, merging with previous if needed. +""" + +import sys +import json +import argparse +import logging +import re +import requests +import traceback +import os +import pandas as pd +from constants import ( + DATASET_UPDATE_URL, + STATUS_UPDATE_URL, + AGGREGATED_CSV_FILE, + OUTPUT_DATA_DIR, + SYNCED_WITH_CKB, + SYNC_WITH_CKB_FAILED, + SCRIPT_DIR, + PROGRESS_UPDATE_URL, +) + +# --- Logging Setup --- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[logging.StreamHandler(sys.stdout)], +) +logger = logging.getLogger(__name__) + +# --- Python Path Setup --- +sys.path.insert(0, str(SCRIPT_DIR)) + +try: + from services.url_decoder_service import URLDecoderService + from services.s3_ferry_service import S3Ferry + + url_decoder_service = URLDecoderService() + s3_ferry_service = S3Ferry() +except ImportError as e: + logger.error(f"Failed to import services: {e}") + traceback.print_exc() + sys.exit(1) + + +def update_item_ids(df: pd.DataFrame, dataset_id: int) -> pd.DataFrame: + """Update item_id column to format {dataset_id}_{row_number} (1-based).""" + if "item_id" in df.columns: + df = df.copy() + df["item_id"] = [f"{dataset_id}_{i + 1}" for i in range(len(df))] + return df + + +def update_dataset_version_id(df: pd.DataFrame, dataset_id: int) -> pd.DataFrame: + """Update dataset_version_id column to the current dataset_id.""" + if "dataset_version_id" in df.columns: + df = df.copy() + df["dataset_version_id"] = dataset_id + return df + + +def combine_csvs_and_update_ids( + prev_csv_path: str, curr_csv_path: str, dataset_id: int, output_path: str +) -> str: + """Combine previous and current CSVs, update item_id and dataset_version_id, and save to output_path.""" + try: + prev_df = pd.read_csv(prev_csv_path) + curr_df = pd.read_csv(curr_csv_path) + combined_df = pd.concat([prev_df, curr_df], ignore_index=True) + combined_df = update_item_ids(combined_df, dataset_id) + combined_df = update_dataset_version_id(combined_df, dataset_id) + combined_df.to_csv(output_path, index=False) + logger.info(f"Combined CSV saved to {output_path}") + return output_path + except Exception as e: + logger.error(f"Error combining CSVs: {e}") + raise + + +def upload_csv_to_s3(local_csv_path: str, dataset_id: int) -> None: + """Upload the CSV file to S3 using S3Ferry.""" + destination_file_path = f"/datasets/{dataset_id}/{AGGREGATED_CSV_FILE}" + source_file_path = local_csv_path.replace("/app/", "") + logger.info(f"Uploading {local_csv_path} to S3 as {destination_file_path}") + response = s3_ferry_service.transfer_file( + destination_file_path=destination_file_path, + destination_storage_type="S3", + source_file_path=source_file_path, + source_storage_type="FS", + ) + logger.info(f"S3 upload status: {response.status_code}") + logger.info(f"S3 upload response: {response.text}") + if response.status_code not in [200, 201]: + raise RuntimeError(f"Failed to upload CSV to S3: {response.text}") + + +def notify_progress_uploading_to_s3(session_id: int) -> None: + """Notify progress update: New Dataset Uploading to S3.""" + payload = { + "sessionId": session_id, + "generationStatus": "Success", + "generationMessage": "Dataset has been uploaded to S3 and Dataset Generation is completed.", + "progressPercentage": 100, + "processComplete": True, + } + try: + logger.info( + f"Calling progress update endpoint: {PROGRESS_UPDATE_URL} with payload={payload}" + ) + response = requests.post( + PROGRESS_UPDATE_URL, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=30, + ) + logger.info(f"Progress update response - HTTP Status: {response.status_code}") + logger.info(f"Progress update response body: {response.text}") + if response.status_code not in [200, 201]: + logger.warning(f"Progress update endpoint returned error: {response.text}") + except Exception as e: + logger.error(f"Error calling progress update endpoint: {e}") + traceback.print_exc() + + +def notify_dataset_update(output_csv_path: str) -> None: + """Notify local dataset update endpoint before uploading to S3.""" + update_payload = {"filePath": output_csv_path} + try: + logger.info( + f"Calling dataset update endpoint: {DATASET_UPDATE_URL} with filePath={output_csv_path}" + ) + update_response = requests.post( + DATASET_UPDATE_URL, + json=update_payload, + headers={"Content-Type": "application/json"}, + timeout=30, + ) + logger.info( + f"Dataset update response - HTTP Status: {update_response.status_code}" + ) + logger.info(f"Dataset update response body: {update_response.text}") + if update_response.status_code not in [200, 201]: + logger.warning( + f"Dataset update endpoint returned error: {update_response.text}" + ) + except Exception as e: + logger.error(f"Error calling dataset update endpoint: {e}") + traceback.print_exc() + + +def send_status_update(dataset_id: int, encoded_results: str) -> None: + """Send status update to Ruuter.""" + decoded_results = url_decoder_service.decode_signed_urls(encoded_results) + agencies = [] + overall_success = True + for result in decoded_results: + dataset_metadata = result.get("dataset_metadata", {}) + agency_id = dataset_metadata.get("agency_id", "unknown") + success = result.get("success", False) + sync_status = SYNCED_WITH_CKB if success else SYNC_WITH_CKB_FAILED + agencies.append({"agencyId": agency_id, "syncStatus": sync_status}) + if not success: + overall_success = False + + generation_status = "Generation_Success" if overall_success else "Generation_Failed" + callback_payload = { + "agencies": agencies, + "datasetId": dataset_id, + "generationStatus": generation_status, + } + + logger.info(f"Sending callback payload to: {STATUS_UPDATE_URL}") + try: + response = requests.post( + STATUS_UPDATE_URL, + json=callback_payload, + headers={"Content-Type": "application/json"}, + timeout=30, + ) + logger.info(f"Status update response - HTTP Status: {response.status_code}") + logger.info(f"Status update response body: {response.text}") + except Exception as e: + logger.error(f"Error sending callback to status update endpoint: {e}") + traceback.print_exc() + + +def process_callback_background( + file_path: str, encoded_results: str, session_id: int +) -> None: + """Process the dataset generation callback: upload CSV to S3 and send status update.""" + try: + logger.info(f"Starting processing for: {file_path}") + + # Extract dataset ID from file path (e.g., output_datasets/12.csv -> 12) + dataset_id_match = re.search(r"/([^/]+)\.csv$", file_path) + dataset_id = int(dataset_id_match.group(1)) if dataset_id_match else None + if not dataset_id: + logger.error("Could not extract dataset_id from file path.") + return + + logger.info(f"Extracted dataset ID: {dataset_id}") + + current_csv_path = file_path + output_csv_path = f"{OUTPUT_DATA_DIR}/{dataset_id}_aggregated.csv" + + if dataset_id <= 2: + logger.info("No previous dataset. Using current CSV only.") + df = pd.read_csv(current_csv_path) + df = update_item_ids(df, dataset_id) + df = update_dataset_version_id(df, dataset_id) + df.to_csv(output_csv_path, index=False) + else: + prev_dataset_id = dataset_id - 1 + prev_csv_local = f"{OUTPUT_DATA_DIR}/{prev_dataset_id}_prev.csv" + prev_csv_s3_path = f"datasets/{prev_dataset_id}/{AGGREGATED_CSV_FILE}" + logger.info( + f"Attempting to download previous CSV from S3: {prev_csv_s3_path}" + ) + try: + response = s3_ferry_service.transfer_file( + destination_file_path=prev_csv_local.replace("/app/", ""), + destination_storage_type="FS", + source_file_path=prev_csv_s3_path, + source_storage_type="S3", + ) + logger.info(f"S3 download status: {response.status_code}") + if response.status_code in [200, 201] and os.path.exists( + prev_csv_local + ): + logger.info(f"Previous CSV downloaded to {prev_csv_local}") + combine_csvs_and_update_ids( + prev_csv_local, current_csv_path, dataset_id, output_csv_path + ) + else: + logger.warning("Previous CSV not found in S3, using current only.") + df = pd.read_csv(current_csv_path) + df = update_item_ids(df, dataset_id) + df = update_dataset_version_id(df, dataset_id) + df.to_csv(output_csv_path, index=False) + except Exception as e: + logger.warning( + f"Failed to download previous CSV: {e}. Using current only." + ) + df = pd.read_csv(current_csv_path) + df = update_item_ids(df, dataset_id) + df = update_dataset_version_id(df, dataset_id) + df.to_csv(output_csv_path, index=False) + + notify_dataset_update(output_csv_path) + upload_csv_to_s3(output_csv_path, dataset_id) + send_status_update(dataset_id, encoded_results) + + logger.info("Processing completed successfully") + notify_progress_uploading_to_s3(session_id) + + except Exception as e: + logger.error(f"Error in processing: {str(e)}") + traceback.print_exc() + raise + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Process dataset generation callback") + parser.add_argument( + "--file-path", required=True, help="File path of the generated dataset CSV" + ) + parser.add_argument( + "--encoded-results", required=True, help="Encoded results string" + ) + parser.add_argument("--output-json", help="Output JSON file path for response") + parser.add_argument( + "--session-id", required=True, help="Session ID for the callback" + ) + return parser.parse_args() + + +def main(): + """Main function to handle callback processing.""" + args = parse_args() + try: + logger.info("Starting callback processing...") + logger.info(f"File path: {args.file_path}") + logger.info(f"Encoded results length: {len(args.encoded_results)} characters") + + process_callback_background( + args.file_path, args.encoded_results, args.session_id + ) + + response = { + "message": "Callback processing completed successfully", + "status": "completed", + "file_path": args.file_path, + } + + if args.output_json: + with open(args.output_json, "w") as f: + json.dump(response, f, indent=2) + logger.info(f"Response written to: {args.output_json}") + + print(json.dumps(response)) + logger.info("Callback processing completed successfully") + + except Exception as e: + logger.error(f"Error processing callback: {str(e)}") + traceback.print_exc() + error_response = { + "message": f"Callback processing failed: {str(e)}", + "status": "error", + "file_path": args.file_path, + } + if args.output_json: + with open(args.output_json, "w") as f: + json.dump(error_response, f, indent=2) + print(json.dumps(error_response)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/s3_dataset_processor/download_source_dataset.py b/src/s3_dataset_processor/download_source_dataset.py new file mode 100644 index 00000000..71357c0b --- /dev/null +++ b/src/s3_dataset_processor/download_source_dataset.py @@ -0,0 +1,129 @@ +""" +Direct Python script for downloading datasets from S3 signed URLs. +Replaces the FastAPI /download-datasets endpoint for CronManager execution. +""" + +import sys +import json +import argparse +import logging +import traceback +from constants import SCRIPT_DIR + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[logging.StreamHandler(sys.stdout)], +) +logger = logging.getLogger(__name__) + +# Add the s3_dataset_processor to Python path to import modules FIRST +# This path corresponds to the volume mount in docker-compose.yml +script_dir = SCRIPT_DIR +sys.path.insert(0, str(script_dir)) + +# Now import the services AFTER adding to path +try: + from services.url_decoder_service import URLDecoderService + from services.download_service import DownloadService + from services.extraction_service import ExtractionService + from handlers.response_handler import ResponseHandler + + logger.info("✅ Successfully imported all required modules") +except ImportError as e: + logger.error(f"❌ Failed to import required modules: {e}") + logger.error(f"Python path: {sys.path}") + logger.error(f"Script directory exists: {script_dir.exists()}") + if script_dir.exists(): + logger.error(f"Contents of script directory: {list(script_dir.iterdir())}") + sys.exit(1) + + +def main(): + """Main function to handle dataset download process.""" + parser = argparse.ArgumentParser( + description="Download datasets from S3 signed URLs" + ) + parser.add_argument( + "--encoded-data", required=True, help="Base64 encoded signed URLs data" + ) + parser.add_argument( + "--extract-files", + action="store_true", + default=True, + help="Extract downloaded files", + ) + parser.add_argument("--output-json", help="Output file path for results JSON") + + args = parser.parse_args() + + try: + if not args.encoded_data or not isinstance(args.encoded_data, str): + logger.error("'encoded_data' must be a non-empty string") + sys.exit(1) + + logger.info("Initializing services...") + # Initialize services + url_decoder_service = URLDecoderService() + download_service = DownloadService() + extraction_service = ExtractionService() + response_handler = ResponseHandler() + + logger.info("Decoding signed URLs...") + # Decode the data using service + decoded_data = url_decoder_service.decode_signed_urls(args.encoded_data) + logger.info(f"Starting download for {len(decoded_data)} files: {decoded_data}") + + logger.info("Processing downloads...") + # Process downloads using service + downloaded_files, successful_downloads, failed_downloads = ( + download_service.process_downloads(decoded_data) + ) + + logger.info("Processing extractions...") + # Process extractions using service + extracted_folders = extraction_service.process_extractions( + downloaded_files, args.extract_files + ) + + logger.info("Formatting response...") + # Format response using handler + response = response_handler.format_download_response( + decoded_data, + downloaded_files, + successful_downloads, + failed_downloads, + extracted_folders, + ) + + # Output results + if args.output_json: + with open(args.output_json, "w") as f: + json.dump(response.dict(), f, indent=2) + logger.info(f"Results written to {args.output_json}") + else: + print(json.dumps(response.dict(), indent=2)) + + # Log summary + logger.info( + f"Download completed: {successful_downloads} successful, {failed_downloads} failed" + ) + logger.info(f"Extracted folders: {len(extracted_folders)}") + + # Exit with appropriate code + sys.exit(0 if response.success else 1) + + except ValueError as e: + logger.error(f"Decoding error: {str(e)}") + traceback.print_exc() + sys.exit(1) + except Exception as e: + logger.error(f"Internal error: {str(e)}") + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/s3_dataset_processor/handlers/__init__.py b/src/s3_dataset_processor/handlers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/s3_dataset_processor/handlers/response_handler.py b/src/s3_dataset_processor/handlers/response_handler.py new file mode 100644 index 00000000..19264ea7 --- /dev/null +++ b/src/s3_dataset_processor/handlers/response_handler.py @@ -0,0 +1,41 @@ +"""Handler for API response formatting.""" + +from typing import List, Dict, Any + +from models.schemas import DownloadResponse, DownloadedFile + + +class ResponseHandler: + """Handler class for formatting API responses.""" + + @staticmethod + def format_download_response( + decoded_data: List[Dict[str, Any]], + downloaded_files: List[DownloadedFile], + successful_downloads: int, + failed_downloads: int, + extracted_folders: List[Dict[str, str]], + ) -> DownloadResponse: + """ + Format download response. + + Args: + decoded_data: Original decoded data + downloaded_files: List of downloaded files + successful_downloads: Number of successful downloads + failed_downloads: Number of failed downloads + extracted_folders: List of extracted folder information + + Returns: + Formatted download response + """ + return DownloadResponse( + success=successful_downloads > 0, + message=f"Downloaded {successful_downloads} files, {failed_downloads} failed", + total_downloads=len(decoded_data), + successful_downloads=successful_downloads, + failed_downloads=failed_downloads, + downloaded_files=downloaded_files, + extracted_folders=extracted_folders, + total_extracted_folders=len(extracted_folders), + ) diff --git a/src/s3_dataset_processor/models/__init__.py b/src/s3_dataset_processor/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/s3_dataset_processor/models/schemas.py b/src/s3_dataset_processor/models/schemas.py new file mode 100644 index 00000000..a3e2f1df --- /dev/null +++ b/src/s3_dataset_processor/models/schemas.py @@ -0,0 +1,74 @@ +"""Pydantic models for API requests and responses.""" + +from pydantic import BaseModel +from typing import List, Dict, Optional, Any + + +class DownloadRequest(BaseModel): + """Request model for downloading files from signed URLs.""" + + encoded_data: str + extract_files: Optional[bool] = True + + +class DownloadedFile(BaseModel): + """Model for downloaded file information.""" + + agency_id: str + agency_name: str + original_filename: str + local_path: str + file_size: int + extracted_path: Optional[str] = None + extraction_success: bool = False + + +class DownloadResponse(BaseModel): + """Response model for download operation.""" + + success: bool + message: str + total_downloads: int + successful_downloads: int + failed_downloads: int + downloaded_files: List[DownloadedFile] + extracted_folders: List[Dict[str, str]] + total_extracted_folders: int + + +class ChunkDownloadRequest(BaseModel): + """Request model for downloading a single chunk.""" + + dataset_id: str + page_num: int + + +class MultiChunkDownloadRequest(BaseModel): + """Request model for downloading multiple chunks.""" + + dataset_id: str + chunk_ids: List[int] + + +class ChunkDownloadResponse(BaseModel): + """Response model for single chunk download.""" + + success: bool + dataset_id: str + page_num: Optional[int] = None + chunk_data: Optional[Dict[str, Any]] = None + error: Optional[str] = None + message: str + + +class MultiChunkDownloadResponse(BaseModel): + """Response model for multi-chunk download and aggregation.""" + + success: bool + dataset_id: str + chunk_info: Optional[Dict[str, Any]] = None + aggregated_data: List[Dict[str, Any]] + download_summary: Dict[str, Any] + download_details: Optional[List[Dict[str, Any]]] = None + error: Optional[str] = None + message: str diff --git a/src/s3_dataset_processor/services/__init__.py b/src/s3_dataset_processor/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/s3_dataset_processor/services/download_service.py b/src/s3_dataset_processor/services/download_service.py new file mode 100644 index 00000000..658a5f0e --- /dev/null +++ b/src/s3_dataset_processor/services/download_service.py @@ -0,0 +1,118 @@ +"""Service for file download operations.""" + +import os +import requests +from typing import List, Dict, Any +from urllib.parse import urlparse +from config.settings import settings +from models.schemas import DownloadedFile +import sys +import logging + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[logging.StreamHandler(sys.stdout)], +) +logger = logging.getLogger(__name__) + + +class DownloadService: + """Service class for handling file download operations.""" + + def __init__(self): + """Initialize the download service.""" + self.data_dir = settings.DATA_DIR + self.timeout = settings.DOWNLOAD_TIMEOUT + self.chunk_size = settings.CHUNK_SIZE + + def download_file(self, url: str, local_path: str) -> bool: + """ + Download a file from URL to local path. + + Args: + url: The presigned URL to download from + local_path: Local file path to save the file + + Returns: + True if download successful, False otherwise + """ + try: + response = requests.get(url, stream=True, timeout=self.timeout) + response.raise_for_status() + logger.info(f"resonse_payload:{response}") + + # Ensure directory exists + os.makedirs(os.path.dirname(local_path), exist_ok=True) + + with open(local_path, "wb") as f: + for chunk in response.iter_content(chunk_size=self.chunk_size): + if chunk: + f.write(chunk) + + return True + except Exception as e: + logger.error(f"Failed to download {url}: {e}") + return False + + def process_downloads(self, decoded_data: List[Dict[str, Any]]) -> tuple: + """ + Process multiple downloads from decoded data. + + Args: + decoded_data: List of decoded URL data + + Returns: + Tuple of (downloaded_files, successful_downloads, failed_downloads) + """ + downloaded_files = [] + successful_downloads = 0 + failed_downloads = 0 + try: + for entry in decoded_data: + agency_id = entry.get("agencyId", "unknown") + agency_name = entry.get("agencyName", "Unknown Agency") + signed_url = entry.get("dataUrl", "") + + if not signed_url: + failed_downloads += 1 + continue + + # Parse URL to get filename + parsed_url = urlparse(signed_url) + original_filename = ( + parsed_url.path.split("/")[-1] + if parsed_url.path + else f"{agency_id}.zip" + ) + + # Download file to data directory + local_file_path = os.path.join(self.data_dir, original_filename) + logger.info( + f"Downloading {original_filename} for agency {agency_id} with name {agency_name}" + ) + + if self.download_file(signed_url, local_file_path): + file_size = os.path.getsize(local_file_path) + + downloaded_file = DownloadedFile( + agency_id=agency_id, + agency_name=agency_name, + original_filename=original_filename, + local_path=local_file_path, + file_size=file_size, + ) + + downloaded_files.append(downloaded_file) + successful_downloads += 1 + logger.info(f"Successfully downloaded {original_filename}") + + else: + failed_downloads += 1 + logger.error(f"Failed to download {original_filename}") + except Exception as e: + logger.error(f"Error processing downloads: {e}") + return downloaded_files, successful_downloads, failed_downloads + return downloaded_files, successful_downloads, failed_downloads diff --git a/src/s3_dataset_processor/services/extraction_service.py b/src/s3_dataset_processor/services/extraction_service.py new file mode 100644 index 00000000..8b937234 --- /dev/null +++ b/src/s3_dataset_processor/services/extraction_service.py @@ -0,0 +1,143 @@ +"""Service for file extraction operations.""" + +import os +import zipfile +import shutil +from typing import List, Dict + +from config.settings import settings +from models.schemas import DownloadedFile +import sys +import logging + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[logging.StreamHandler(sys.stdout)], +) +logger = logging.getLogger(__name__) + + +class ExtractionService: + """Service class for handling file extraction operations.""" + + def __init__(self): + """Initialize the extraction service.""" + self.data_dir = settings.DATA_DIR + + def extract_zip_file(self, zip_path: str, extract_to: str) -> bool: + """ + Extract a ZIP file to the specified directory. + + Args: + zip_path: Path to the ZIP file + extract_to: Directory to extract files to + + Returns: + True if extraction successful, False otherwise + """ + try: + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(extract_to) + return True + except Exception as e: + print(f"Failed to extract {zip_path}: {e}") + return False + + def process_extractions( + self, downloaded_files: List[DownloadedFile], extract_files: bool = True + ) -> List[Dict[str, str]]: + """ + Process extractions for downloaded files. + + Args: + downloaded_files: List of downloaded files to extract + extract_files: Whether to extract files + + Returns: + List of extracted folder information + """ + extracted_folders = [] + + if not extract_files: + return extracted_folders + + for downloaded_file in downloaded_files: + if not downloaded_file.original_filename.lower().endswith(".zip"): + continue + + # Extract agency name from filename + # agency_name = downloaded_file.original_filename.replace(".zip", "") + agency_name = downloaded_file.agency_name + agency_dir = os.path.join(self.data_dir, agency_name) + os.makedirs(agency_dir, exist_ok=True) + + # Extract to temporary directory first + temp_extract_dir = os.path.join(self.data_dir, f"temp_{agency_name}") + logger.info( + f"Extracting {downloaded_file.original_filename} to temporary directory: {temp_extract_dir}" + ) + + if self.extract_zip_file(downloaded_file.local_path, temp_extract_dir): + self._move_extracted_contents(temp_extract_dir, agency_dir) + + # Update downloaded file info + downloaded_file.extracted_path = agency_dir + downloaded_file.extraction_success = True + logger.info( + f"Successfully extracted {downloaded_file.original_filename}" + ) + + # Add to extracted folders list + extracted_folders.append( + { + "agency_id": downloaded_file.agency_id, + "agency_name": downloaded_file.agency_name, + "folder_path": agency_dir, + } + ) + + # Remove the ZIP file after successful extraction + os.remove(downloaded_file.local_path) + logger.info(f"Removed ZIP file {downloaded_file.local_path}") + else: + downloaded_file.extraction_success = False + logger.error(f"Failed to extract {downloaded_file.original_filename}") + + return extracted_folders + + def _move_extracted_contents(self, temp_extract_dir: str, agency_dir: str) -> None: + """ + Move extracted contents from temporary directory to agency directory. + + Args: + temp_extract_dir: Temporary extraction directory + agency_dir: Target agency directory + """ + extracted_items = os.listdir(temp_extract_dir) + + if len(extracted_items) == 1 and os.path.isdir( + os.path.join(temp_extract_dir, extracted_items[0]) + ): + # Single folder was extracted - move its contents to the agency directory + nested_folder = os.path.join(temp_extract_dir, extracted_items[0]) + logger.info( + f"Found nested folder structure, moving contents from {nested_folder} to {agency_dir}" + ) + + for item in os.listdir(nested_folder): + src = os.path.join(nested_folder, item) + dst = os.path.join(agency_dir, item) + shutil.move(src, dst) + else: + # Multiple items or files extracted - move everything to agency directory + logger.info(f"Moving extracted contents directly to {agency_dir}") + for item in extracted_items: + src = os.path.join(temp_extract_dir, item) + dst = os.path.join(agency_dir, item) + shutil.move(src, dst) + + # Clean up temporary directory + shutil.rmtree(temp_extract_dir, ignore_errors=True) diff --git a/src/s3_dataset_processor/services/s3_ferry_service.py b/src/s3_dataset_processor/services/s3_ferry_service.py new file mode 100644 index 00000000..0c0cfafe --- /dev/null +++ b/src/s3_dataset_processor/services/s3_ferry_service.py @@ -0,0 +1,166 @@ +"""Service for S3Ferry file transfer operations.""" + +import requests +import logging +import traceback +from typing import Dict + +# Configure logging +logger = logging.getLogger(__name__) + + +class S3Ferry: + """Service class for handling S3Ferry file transfer operations.""" + + def __init__(self, base_url: str = "http://gc-s3-ferry:3000"): + """ + Initialize the S3Ferry service. + + Args: + base_url: Base URL for the S3Ferry service + """ + self.base_url = base_url + self.url = f"{base_url}/v1/files/copy" + logger.info(f"S3Ferry service initialized with URL: {self.url}") + + def transfer_file( + self, + destination_file_path: str, + destination_storage_type: str, + source_file_path: str, + source_storage_type: str, + ) -> requests.Response: + """ + Transfer a file using S3Ferry service. + + Args: + destination_file_path: Path where the file should be stored in destination + destination_storage_type: Type of destination storage (e.g., 's3', 'local') + source_file_path: Path of the source file + source_storage_type: Type of source storage (e.g., 'local', 's3') + + Returns: + Response object from the S3Ferry service + """ + try: + payload = self.get_s3_ferry_payload( + destination_file_path, + destination_storage_type, + source_file_path, + source_storage_type, + ) + + logger.info( + f"[S3_FERRY] Transferring file: {source_file_path} -> {destination_file_path}" + ) + logger.debug(f"[S3_FERRY] Payload: {payload}") + + response = requests.post( + self.url, + json=payload, + headers={"Content-Type": "application/json"}, + timeout=60, + ) + + logger.info(f"[S3_FERRY] Transfer response status: {response.status_code}") + + # Accept both 200 (OK) and 201 (Created) as success + if response.status_code not in [200, 201]: + logger.error(f"[S3_FERRY] Transfer failed: {response.text}") + else: + logger.info( + f"[S3_FERRY] ✅ Transfer successful (HTTP {response.status_code})" + ) + + return response + + except Exception as e: + logger.error(f"[S3_FERRY] Error during file transfer: {str(e)}") + traceback.print_exc() + raise + + def get_s3_ferry_payload( + self, + destination_file_path: str, + destination_storage_type: str, + source_file_path: str, + source_storage_type: str, + ) -> Dict[str, str]: + """ + Generate S3Ferry payload for file transfer. + + Args: + destination_file_path: Path where the file should be stored in destination + destination_storage_type: Type of destination storage + source_file_path: Path of the source file + source_storage_type: Type of source storage + + Returns: + Dictionary containing the S3Ferry payload + """ + payload = { + "destinationFilePath": destination_file_path, + "destinationStorageType": destination_storage_type, + "sourceFilePath": source_file_path, + "sourceStorageType": source_storage_type, + } + + return payload + + def upload_to_s3( + self, local_file_path: str, s3_destination_path: str + ) -> requests.Response: + """ + Convenience method to upload a local file to S3. + + Args: + local_file_path: Path to the local file + s3_destination_path: S3 destination path (e.g., 'bucket/folder/file.json') + + Returns: + Response object from the S3Ferry service + """ + return self.transfer_file( + destination_file_path=s3_destination_path, + destination_storage_type="S3", + source_file_path=local_file_path, + source_storage_type="FS", + ) + + def download_from_s3( + self, s3_source_path: str, local_destination_path: str + ) -> requests.Response: + """ + Convenience method to download a file from S3 to local storage. + + Args: + s3_source_path: S3 source path (e.g., 'bucket/folder/file.json') + local_destination_path: Local destination path + + Returns: + Response object from the S3Ferry service + """ + return self.transfer_file( + destination_file_path=local_destination_path, + destination_storage_type="local", + source_file_path=s3_source_path, + source_storage_type="s3", + ) + + # def copy_s3_to_s3(self, source_s3_path: str, destination_s3_path: str) -> requests.Response: + # """ + # Convenience method to copy files between S3 locations. + + # Args: + # source_s3_path: Source S3 path + # destination_s3_path: Destination S3 path + + # Returns: + # Response object from the S3Ferry service + # """ + # return self.transfer_file( + # destination_file_path=destination_s3_path, + # destination_storage_type="s3", + # source_file_path=source_s3_path, + # source_storage_type="s3" + # ) diff --git a/src/s3_dataset_processor/services/url_decoder_service.py b/src/s3_dataset_processor/services/url_decoder_service.py new file mode 100644 index 00000000..901008b0 --- /dev/null +++ b/src/s3_dataset_processor/services/url_decoder_service.py @@ -0,0 +1,38 @@ +"""Service for URL decoding operations.""" + +import urllib.parse +import json +from typing import List, Dict, Any + + +class URLDecoderService: + """Service class for handling URL decoding operations.""" + + @staticmethod + def decode_signed_urls(encoded_data: str) -> List[Dict[str, Any]]: + """ + Decode URL-encoded signed URLs data. + + Args: + encoded_data: URL-encoded JSON string containing signed URLs + + Returns: + List of decoded URL data dictionaries + + Raises: + ValueError: If decoding or JSON parsing fails + """ + try: + # URL decode the data + decoded_data = urllib.parse.unquote(encoded_data) + + # Parse JSON + parsed_data = json.loads(decoded_data) + + return parsed_data + + except json.JSONDecodeError as e: + raise ValueError(f"Failed to parse JSON: {e}") + + except Exception as e: + raise ValueError(f"Failed to decode data: {e}") diff --git a/src/tests/inference/prod_deployment_tests.py b/src/tests/inference/prod_deployment_tests.py new file mode 100644 index 00000000..fb2088d5 --- /dev/null +++ b/src/tests/inference/prod_deployment_tests.py @@ -0,0 +1,18 @@ +def test_load_model(): + """Test the load_model function.""" + pass + + +def test_hot_swap_model(): + """Test the hot_swap_model function.""" + pass + + +def test_delete_model(): + """Test the delete_model function.""" + pass + + +def test_model_inference(): + """Test model inference functionality.""" + pass diff --git a/src/tests/inference/test_deployment_tests.py b/src/tests/inference/test_deployment_tests.py new file mode 100644 index 00000000..e69de29b diff --git a/uv.lock b/uv.lock index 1396f922..08189daa 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,141 @@ version = 1 revision = 2 requires-python = ">=3.12" +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "certifi" +version = "2025.7.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.115.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" }, +] + [[package]] name = "filelock" version = "3.18.0" @@ -25,18 +160,75 @@ name = "global-classifier" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "litserve" }, + { name = "loguru" }, { name = "numpy" }, + { name = "pytest" }, + { name = "requests" }, { name = "ruff" }, { name = "torch" }, ] [package.metadata] requires-dist = [ + { name = "litserve", specifier = ">=0.2.13" }, + { name = "loguru", specifier = ">=0.7.2" }, { name = "numpy", specifier = ">=2.2.6" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "requests", specifier = ">=2.32.0" }, { name = "ruff", specifier = ">=0.11.11" }, { name = "torch", specifier = ">=2.7.0" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" }, + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -49,6 +241,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "litserve" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "pyzmq" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/e0/8267d94c8b227c94412788f0d31b01fdf5f3ed481611bdbcb2f0b4b64f77/litserve-0.2.13.tar.gz", hash = "sha256:ac2b8ff9392f720081690ad61ac8915fae4c22fb3151a101870a86392c934d46", size = 74949, upload-time = "2025-07-01T19:44:46.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/f8/5968963e4310625fbf0cb673af05bd1c7b38027a1e269528c85aa8df4a17/litserve-0.2.13-py3-none-any.whl", hash = "sha256:0c4acfe4e26d5cbfbdac7c1cd9fb9c6115960ff9713e35eb16f3994c6bb9c6bf", size = 88469, upload-time = "2025-07-01T19:44:45.662Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + [[package]] name = "markupsafe" version = "3.0.2" @@ -276,6 +495,195 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/4e/0d0c945463719429b7bd21dece907ad0bde437a2ff12b9b12fee94722ab0/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1", size = 89265, upload-time = "2024-10-01T17:00:38.172Z" }, ] +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/06/50a4e9648b3e8b992bef8eb632e457307553a89d294103213cfd47b3da69/pyzmq-27.0.0.tar.gz", hash = "sha256:b1f08eeb9ce1510e6939b6e5dcd46a17765e2333daae78ecf4606808442e52cf", size = 280478, upload-time = "2025-06-13T14:09:07.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/a7/9ad68f55b8834ede477842214feba6a4c786d936c022a67625497aacf61d/pyzmq-27.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cbabc59dcfaac66655c040dfcb8118f133fb5dde185e5fc152628354c1598e52", size = 1305438, upload-time = "2025-06-13T14:07:31.676Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/26aa0f98665a22bc90ebe12dced1de5f3eaca05363b717f6fb229b3421b3/pyzmq-27.0.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cb0ac5179cba4b2f94f1aa208fbb77b62c4c9bf24dd446278b8b602cf85fcda3", size = 895095, upload-time = "2025-06-13T14:07:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/cf/85/c57e7ab216ecd8aa4cc7e3b83b06cc4e9cf45c87b0afc095f10cd5ce87c1/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a48f0228eab6cbf69fde3aa3c03cbe04e50e623ef92ae395fce47ef8a76152", size = 651826, upload-time = "2025-06-13T14:07:34.831Z" }, + { url = "https://files.pythonhosted.org/packages/69/9a/9ea7e230feda9400fb0ae0d61d7d6ddda635e718d941c44eeab22a179d34/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111db5f395e09f7e775f759d598f43cb815fc58e0147623c4816486e1a39dc22", size = 839750, upload-time = "2025-06-13T14:07:36.553Z" }, + { url = "https://files.pythonhosted.org/packages/08/66/4cebfbe71f3dfbd417011daca267539f62ed0fbc68105357b68bbb1a25b7/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c8878011653dcdc27cc2c57e04ff96f0471e797f5c19ac3d7813a245bcb24371", size = 1641357, upload-time = "2025-06-13T14:07:38.21Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f6/b0f62578c08d2471c791287149cb8c2aaea414ae98c6e995c7dbe008adfb/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0ed2c1f335ba55b5fdc964622254917d6b782311c50e138863eda409fbb3b6d", size = 2020281, upload-time = "2025-06-13T14:07:39.599Z" }, + { url = "https://files.pythonhosted.org/packages/37/b9/4f670b15c7498495da9159edc374ec09c88a86d9cd5a47d892f69df23450/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e918d70862d4cfd4b1c187310015646a14e1f5917922ab45b29f28f345eeb6be", size = 1877110, upload-time = "2025-06-13T14:07:41.027Z" }, + { url = "https://files.pythonhosted.org/packages/66/31/9dee25c226295b740609f0d46db2fe972b23b6f5cf786360980524a3ba92/pyzmq-27.0.0-cp312-abi3-win32.whl", hash = "sha256:88b4e43cab04c3c0f0d55df3b1eef62df2b629a1a369b5289a58f6fa8b07c4f4", size = 559297, upload-time = "2025-06-13T14:07:42.533Z" }, + { url = "https://files.pythonhosted.org/packages/9b/12/52da5509800f7ff2d287b2f2b4e636e7ea0f001181cba6964ff6c1537778/pyzmq-27.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:dce4199bf5f648a902ce37e7b3afa286f305cd2ef7a8b6ec907470ccb6c8b371", size = 619203, upload-time = "2025-06-13T14:07:43.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/6d/7f2e53b19d1edb1eb4f09ec7c3a1f945ca0aac272099eab757d15699202b/pyzmq-27.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:56e46bbb85d52c1072b3f809cc1ce77251d560bc036d3a312b96db1afe76db2e", size = 551927, upload-time = "2025-06-13T14:07:45.51Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/876b27c4ff777db4ceba1c69ea90d3c825bb4f8d5e7cd987ce5802e33c55/pyzmq-27.0.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c36ad534c0c29b4afa088dc53543c525b23c0797e01b69fef59b1a9c0e38b688", size = 1340826, upload-time = "2025-06-13T14:07:46.881Z" }, + { url = "https://files.pythonhosted.org/packages/43/69/58ef8f4f59d3bcd505260c73bee87b008850f45edca40ddaba54273c35f4/pyzmq-27.0.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:67855c14173aec36395d7777aaba3cc527b393821f30143fd20b98e1ff31fd38", size = 897283, upload-time = "2025-06-13T14:07:49.562Z" }, + { url = "https://files.pythonhosted.org/packages/43/15/93a0d0396700a60475ad3c5d42c5f1c308d3570bc94626b86c71ef9953e0/pyzmq-27.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8617c7d43cd8ccdb62aebe984bfed77ca8f036e6c3e46dd3dddda64b10f0ab7a", size = 660567, upload-time = "2025-06-13T14:07:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b3/fe055513e498ca32f64509abae19b9c9eb4d7c829e02bd8997dd51b029eb/pyzmq-27.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:67bfbcbd0a04c575e8103a6061d03e393d9f80ffdb9beb3189261e9e9bc5d5e9", size = 847681, upload-time = "2025-06-13T14:07:52.77Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4f/ff15300b00b5b602191f3df06bbc8dd4164e805fdd65bb77ffbb9c5facdc/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5cd11d46d7b7e5958121b3eaf4cd8638eff3a720ec527692132f05a57f14341d", size = 1650148, upload-time = "2025-06-13T14:07:54.178Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/84bdfff2a224a6f26a24249a342e5906993c50b0761e311e81b39aef52a7/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b801c2e40c5aa6072c2f4876de8dccd100af6d9918d4d0d7aa54a1d982fd4f44", size = 2023768, upload-time = "2025-06-13T14:07:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/dc2db178c26a42228c5ac94a9cc595030458aa64c8d796a7727947afbf55/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20d5cb29e8c5f76a127c75b6e7a77e846bc4b655c373baa098c26a61b7ecd0ef", size = 1885199, upload-time = "2025-06-13T14:07:57.166Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/dae7b06a1f8cdee5d8e7a63d99c5d129c401acc40410bef2cbf42025e26f/pyzmq-27.0.0-cp313-cp313t-win32.whl", hash = "sha256:a20528da85c7ac7a19b7384e8c3f8fa707841fd85afc4ed56eda59d93e3d98ad", size = 575439, upload-time = "2025-06-13T14:07:58.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/bc/1709dc55f0970cf4cb8259e435e6773f9946f41a045c2cb90e870b7072da/pyzmq-27.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d8229f2efece6a660ee211d74d91dbc2a76b95544d46c74c615e491900dc107f", size = 639933, upload-time = "2025-06-13T14:08:00.777Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + [[package]] name = "ruff" version = "0.11.11" @@ -310,6 +718,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/29/93c53c098d301132196c3238c312825324740851d77a8500a2462c0fd888/setuptools-80.8.0-py3-none-any.whl", hash = "sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0", size = 1201470, upload-time = "2025-05-20T14:02:51.348Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, +] + [[package]] name = "sympy" version = "1.14.0" @@ -386,3 +815,175 @@ sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e3549295 wheels = [ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, ] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339, upload-time = "2025-06-15T19:05:24.516Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409, upload-time = "2025-06-15T19:05:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939, upload-time = "2025-06-15T19:05:26.494Z" }, + { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270, upload-time = "2025-06-15T19:05:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370, upload-time = "2025-06-15T19:05:28.548Z" }, + { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654, upload-time = "2025-06-15T19:05:29.997Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667, upload-time = "2025-06-15T19:05:31.172Z" }, + { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213, upload-time = "2025-06-15T19:05:32.299Z" }, + { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718, upload-time = "2025-06-15T19:05:33.415Z" }, + { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098, upload-time = "2025-06-15T19:05:34.534Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209, upload-time = "2025-06-15T19:05:35.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786, upload-time = "2025-06-15T19:05:36.559Z" }, + { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343, upload-time = "2025-06-15T19:05:37.5Z" }, + { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004, upload-time = "2025-06-15T19:05:38.499Z" }, + { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671, upload-time = "2025-06-15T19:05:39.52Z" }, + { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772, upload-time = "2025-06-15T19:05:40.897Z" }, + { url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789, upload-time = "2025-06-15T19:05:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551, upload-time = "2025-06-15T19:05:43.781Z" }, + { url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420, upload-time = "2025-06-15T19:05:45.244Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950, upload-time = "2025-06-15T19:05:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706, upload-time = "2025-06-15T19:05:47.459Z" }, + { url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814, upload-time = "2025-06-15T19:05:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820, upload-time = "2025-06-15T19:05:50.088Z" }, + { url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194, upload-time = "2025-06-15T19:05:51.186Z" }, + { url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349, upload-time = "2025-06-15T19:05:52.201Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836, upload-time = "2025-06-15T19:05:53.265Z" }, + { url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343, upload-time = "2025-06-15T19:05:54.252Z" }, + { url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916, upload-time = "2025-06-15T19:05:55.264Z" }, + { url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582, upload-time = "2025-06-15T19:05:56.317Z" }, + { url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752, upload-time = "2025-06-15T19:05:57.359Z" }, + { url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436, upload-time = "2025-06-15T19:05:58.447Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016, upload-time = "2025-06-15T19:05:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727, upload-time = "2025-06-15T19:06:01.086Z" }, + { url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" }, + { url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" }, + { url = "https://files.pythonhosted.org/packages/2c/00/70f75c47f05dea6fd30df90f047765f6fc2d6eb8b5a3921379b0b04defa2/watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297", size = 402114, upload-time = "2025-06-15T19:06:06.186Z" }, + { url = "https://files.pythonhosted.org/packages/53/03/acd69c48db4a1ed1de26b349d94077cca2238ff98fd64393f3e97484cae6/watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018", size = 393879, upload-time = "2025-06-15T19:06:07.369Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c8/a9a2a6f9c8baa4eceae5887fecd421e1b7ce86802bcfc8b6a942e2add834/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0", size = 450026, upload-time = "2025-06-15T19:06:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/fe/51/d572260d98388e6e2b967425c985e07d47ee6f62e6455cefb46a6e06eda5/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12", size = 457917, upload-time = "2025-06-15T19:06:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/4258e52917bf9f12909b6ec314ff9636276f3542f9d3807d143f27309104/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb", size = 483602, upload-time = "2025-06-15T19:06:11.088Z" }, + { url = "https://files.pythonhosted.org/packages/84/99/bee17a5f341a4345fe7b7972a475809af9e528deba056f8963d61ea49f75/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77", size = 596758, upload-time = "2025-06-15T19:06:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/e4bec1d59b25b89d2b0716b41b461ed655a9a53c60dc78ad5771fda5b3e6/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92", size = 477601, upload-time = "2025-06-15T19:06:13.391Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fa/a514292956f4a9ce3c567ec0c13cce427c158e9f272062685a8a727d08fc/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e", size = 451936, upload-time = "2025-06-15T19:06:14.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/c3bf927ec3bbeb4566984eba8dd7a8eb69569400f5509904545576741f88/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b", size = 626243, upload-time = "2025-06-15T19:06:16.232Z" }, + { url = "https://files.pythonhosted.org/packages/e6/65/6e12c042f1a68c556802a84d54bb06d35577c81e29fba14019562479159c/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259", size = 623073, upload-time = "2025-06-15T19:06:17.457Z" }, + { url = "https://files.pythonhosted.org/packages/89/ab/7f79d9bf57329e7cbb0a6fd4c7bd7d0cee1e4a8ef0041459f5409da3506c/watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f", size = 400872, upload-time = "2025-06-15T19:06:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/df/d5/3f7bf9912798e9e6c516094db6b8932df53b223660c781ee37607030b6d3/watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e", size = 392877, upload-time = "2025-06-15T19:06:19.55Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c5/54ec7601a2798604e01c75294770dbee8150e81c6e471445d7601610b495/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa", size = 449645, upload-time = "2025-06-15T19:06:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/0a/04/c2f44afc3b2fce21ca0b7802cbd37ed90a29874f96069ed30a36dfe57c2b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8", size = 457424, upload-time = "2025-06-15T19:06:21.712Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b0/eec32cb6c14d248095261a04f290636da3df3119d4040ef91a4a50b29fa5/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f", size = 481584, upload-time = "2025-06-15T19:06:22.777Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/ca4bb71c68a937d7145aa25709e4f5d68eb7698a25ce266e84b55d591bbd/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e", size = 596675, upload-time = "2025-06-15T19:06:24.226Z" }, + { url = "https://files.pythonhosted.org/packages/a1/dd/b0e4b7fb5acf783816bc950180a6cd7c6c1d2cf7e9372c0ea634e722712b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb", size = 477363, upload-time = "2025-06-15T19:06:25.42Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240, upload-time = "2025-06-15T19:06:26.552Z" }, + { url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607, upload-time = "2025-06-15T19:06:27.606Z" }, + { url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315, upload-time = "2025-06-15T19:06:29.076Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +]