├── .gitignore ├── requirements.txt ├── htdoc ├── mirobody.json ├── __ │ ├── auth │ │ ├── init.json │ │ ├── iframe │ │ ├── handler │ │ └── experiments.js │ └── firebase │ │ └── init.json ├── mirobody.svg ├── index.html ├── assets │ ├── logo-MLEW-e8g.svg │ └── polyfills-legacy-DglrV6q4.js └── github-markdown-light.css ├── .env ├── docker ├── Dockerfile.backend.cloud ├── package.json ├── Dockerfile.backend ├── docker-compose.yaml └── init_fake_data.sh ├── main.py ├── config └── config.example.yaml ├── shell └── update_mirobody.sh ├── tools ├── health │ ├── user_service.py │ ├── health_indicator_service.py │ └── genetic_service.py └── finance │ └── fundamentals_service.py ├── deploy.sh ├── README.md └── user_case └── 1m_users.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | config/config.yaml 3 | .cursorindexingignore 4 | .env 5 | .specstory/ 6 | __pycache__/ 7 | .DS_Store -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | --extra-index-url https://repo.thetahealth.ai/repository/pypi-ai-releases/simple/ 2 | mirobody==20251212.205900 3 | 4 | yfinance 5 | -------------------------------------------------------------------------------- /htdoc/mirobody.json: -------------------------------------------------------------------------------- 1 | { 2 | "__IS_EHR_CONFIG_ON__": true, 3 | "__IS_API_CONFIG_ON__": true, 4 | "__IS_QR_LOGIN_ON__": false, 5 | "__IS_GOOGLE_LOGIN_ON__": true, 6 | "__IS_APPLE_LOGIN_ON__": true 7 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # [OPTIONAL] Configuartion sets. 2 | # "localdb", "test", "gray", or "prod". 3 | ENV=localdb 4 | 5 | # [OPTIONAL] Encryption of sensitive configuration values. 6 | CONFIG_ENCRYPTION_KEY=SecretKey 7 | 8 | # [OPTIONAL] Remote configuration server. 9 | CONFIG_SERVER= 10 | CONFIG_TOKEN= -------------------------------------------------------------------------------- /htdoc/__/auth/init.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "__FIREBASE_API_KEY__", 3 | "authDomain": "__FIREBASE_AUTH_DOMAIN__", 4 | "projectId": "__FIREBASE_PROJECT_ID__", 5 | "storageBucket": "__FIREBASE_STORAGE_BUCKET__", 6 | "messagingSenderId": "__FIREBASE_MESSAGING_SENDER_ID__", 7 | "appId": "__FIREBASE_APP_ID__", 8 | "measurementId": "__FIREBASE_MEASUREMENT_ID__" 9 | } -------------------------------------------------------------------------------- /htdoc/__/firebase/init.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "__FIREBASE_API_KEY__", 3 | "authDomain": "__FIREBASE_AUTH_DOMAIN__", 4 | "projectId": "__FIREBASE_PROJECT_ID__", 5 | "storageBucket": "__FIREBASE_STORAGE_BUCKET__", 6 | "messagingSenderId": "__FIREBASE_MESSAGING_SENDER_ID__", 7 | "appId": "__FIREBASE_APP_ID__", 8 | "measurementId": "__FIREBASE_MEASUREMENT_ID__" 9 | } -------------------------------------------------------------------------------- /docker/Dockerfile.backend.cloud: -------------------------------------------------------------------------------- 1 | # This Dockerfile requires THETA_REGISTRY to be passed as a build argument 2 | # Example: docker build --build-arg THETA_REGISTRY="registry-url/" ... 3 | ARG THETA_REGISTRY 4 | FROM ${THETA_REGISTRY}theta/mirobody-backend:latest 5 | 6 | WORKDIR /app 7 | 8 | COPY requirements.txt . 9 | RUN --mount=type=cache,target=/root/.cache/pip \ 10 | pip install -r requirements.txt && \ 11 | pip list 12 | 13 | -------------------------------------------------------------------------------- /htdoc/__/auth/iframe: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mirobody-chart-renderer", 3 | "version": "1.0.0", 4 | "description": "Chart rendering service for Mirobody using @antv/gpt-vis-ssr", 5 | "private": true, 6 | "scripts": { 7 | "test": "node pub/tools/render-chart.js" 8 | }, 9 | "dependencies": { 10 | "@antv/gpt-vis-ssr": "^0.3.1" 11 | }, 12 | "engines": { 13 | "node": ">=18.0.0", 14 | "npm": ">=9.0.0" 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /htdoc/mirobody.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /htdoc/__/auth/handler: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /htdoc/__/auth/experiments.js: -------------------------------------------------------------------------------- 1 | (function(){window.EXPERIMENTS={DISPLAY_CONTINUE_BUTTON_IF_NOT_REDIRECT:{id:1000003,rollout:1,defaultValue:!0,expiration:new Date("April 1, 2020"),stagingRollout:1},CHECK_CONTINUE_URL_IS_AUTHORIZED:{id:1000004,rollout:1,defaultValue:!0,expiration:new Date("September 1, 2020"),stagingRollout:1},POPUP_POST_MESSAGE_TO_IFRAME:{id:1000005,rollout:1,defaultValue:!0,expiration:new Date("October 1, 2020"),stagingRollout:1},CHECK_OAUTH_STATE_STORED_BEFORE_REDIRECT:{id:1000006,rollout:1,defaultValue:!0,expiration:new Date("April 1, 2021"), 2 | stagingRollout:1},CHECK_REDIRECT_URL_IS_AUTHORIZED:{id:1000007,rollout:1,defaultValue:!0,expiration:new Date("June 1, 2024"),stagingRollout:1}};}).call(this); 3 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from mirobody.server import Server 2 | from mirobody.utils import Config 3 | from mirobody.utils.db_initializer import initialize_database 4 | #----------------------------------------------------------------------------- 5 | 6 | async def main(): 7 | yaml_filenames = ['config/config.yaml'] 8 | 9 | config = await Config.init(yaml_filenames=yaml_filenames) 10 | 11 | # Initialize database schema before starting server 12 | pg_config = config.get_postgresql() 13 | db_init_success = await initialize_database(pg_config, enable_idempotency=False) 14 | if not db_init_success: 15 | print("Warning: Database initialization encountered errors. Check logs for details.") 16 | 17 | await Server.start(yaml_filenames) 18 | 19 | #----------------------------------------------------------------------------- 20 | 21 | if __name__ == "__main__": 22 | import asyncio 23 | asyncio.run(main()) 24 | 25 | #----------------------------------------------------------------------------- 26 | -------------------------------------------------------------------------------- /docker/Dockerfile.backend: -------------------------------------------------------------------------------- 1 | ARG BASE_REGISTRY="" 2 | FROM ${BASE_REGISTRY}ubuntu:24.04 3 | 4 | # Install system dependencies 5 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ 6 | --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ 7 | apt-get update && \ 8 | apt-get install -y --no-install-recommends \ 9 | g++ gfortran build-essential \ 10 | libfftw3-dev libhdf5-dev libblas-dev liblapack-dev \ 11 | libmagic-dev \ 12 | python3 python3-venv \ 13 | nodejs npm \ 14 | fonts-wqy-microhei fonts-wqy-zenhei fontconfig && \ 15 | fc-cache -fv && \ 16 | mkdir /root/venv && \ 17 | python3 -m venv /root/venv 18 | 19 | ENV PATH=/root/venv/bin:${PATH} 20 | 21 | # Install npm packages 22 | RUN mkdir -p /app/mirobody 23 | WORKDIR /app/mirobody 24 | 25 | COPY docker/package.json . 26 | RUN --mount=type=cache,target=/root/.npm \ 27 | npm config set registry https://registry.npmmirror.com && \ 28 | npm install --omit=dev && \ 29 | npm list 30 | 31 | # Install Python packages 32 | WORKDIR /app 33 | 34 | COPY requirements.txt . 35 | RUN --mount=type=cache,target=/root/.cache/pip \ 36 | pip install -r requirements.txt && \ 37 | pip list 38 | -------------------------------------------------------------------------------- /config/config.example.yaml: -------------------------------------------------------------------------------- 1 | OPENROUTER_API_KEY: '' 2 | 3 | SERPER_API_KEY: '' 4 | 5 | FAL_KEY: '' 6 | 7 | E2B_API_KEY: '' 8 | 9 | ALLOWED_TOOLS_SYNERGY: 10 | - genetic_service 11 | 12 | DISALLOWED_TOOLS_SYNERGY: 13 | - get_user_health_profile 14 | 15 | LOG_NAME: '' 16 | LOG_DIR: '' 17 | LOG_LEVEL: INFO 18 | HTTP_SERVER_NAME: mirobody 19 | HTTP_SERVER_VERSION: 1.0.1 20 | HTTP_HOST: 0.0.0.0 21 | HTTP_PORT: 18080 22 | HTTP_URI_PREFIX: '' 23 | HTTP_HTDOC: htdoc 24 | 25 | HTTP_HEADERS: 26 | Access-Control-Allow-Origin: '*' 27 | Access-Control-Allow-Credentials: 'true' 28 | Access-Control-Allow-Methods: '*' 29 | Access-Control-Allow-Headers: '*' 30 | Access-Control-Max-Age: '86400' 31 | 32 | MCP_FRONTEND_URL: http://localhost:18080 33 | MCP_PUBLIC_URL: http://localhost:18080 34 | DATA_PUBLIC_URL: http://localhost:18080 35 | QR_LOGIN_URL: '' 36 | DATABASE_DECRYPTION_KEY: 'mydatabaseencryptionkey' 37 | JWT_KEY: 'myjwtkey' 38 | 39 | REDIS_HOST: redis 40 | REDIS_PORT: 6379 41 | REDIS_DB: 0 42 | REDIS_SSL: false 43 | REDIS_SSL_CHECK_HOSTNAME: false 44 | REDIS_SSL_CERT_REQS: none 45 | REDIS_PASSWORD: 'your_secure_password_here' 46 | PG_HOST: db 47 | PG_PORT: 5432 48 | PG_USER: holistic_user 49 | PG_DBNAME: holistic_db 50 | PG_SCHEMA: theta_ai 51 | PG_MIN_CONNECTION: 5 52 | PG_MAX_CONNECTION: 20 53 | PG_PASSWORD: 'holistic_password' 54 | PG_ENCRYPTION_KEY: 'mydatabaseencryptionkey' 55 | 56 | S3_KEY: '' 57 | S3_TOKEN: '' 58 | S3_REGION: '' 59 | S3_BUCKET: '' 60 | S3_PREFIX: '' 61 | S3_CDN: '' 62 | MCP_TOOL_DIRS: 63 | - mirobody/pub/tools 64 | - tools/health 65 | - tools/finance 66 | 67 | MCP_RESOURCE_DIRS: 68 | - mirobody/pub/resources 69 | AGENT_DIRS: 70 | - mirobody/pub/agents 71 | JWT_PRIVATE_KEY: '' 72 | 73 | GOOGLE_CLIENT_ID: '' 74 | FIREBASE_PROJECT_ID: '' 75 | 76 | APPLE_TEAM_ID: '' 77 | APPLE_KEY_ID: '' 78 | APPLE_PRIVATE_KEY: '' 79 | APPLE_CLIENT_ID: '' 80 | APPLE_AUTH_CLIENT_ID: '' 81 | 82 | EMAIL_FROM: '' 83 | EMAIL_FROM_NAME: '' 84 | EMAIL_TEMPLATE: '' 85 | EMAIL_SMTP_PASS: '' 86 | EMAIL_PREDEFINE_CODES: 87 | demo1@mirobody.ai: '777777' 88 | demo2@mirobody.ai: '777777' 89 | demo3@mirobody.ai: '777777' 90 | ANTHROPIC_API_KEY: '' 91 | OPENAI_API_KEY_RTC: '' 92 | -------------------------------------------------------------------------------- /htdoc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Mirobody 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /shell/update_mirobody.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to check latest mirobody version and update requirements.txt 4 | # Usage: ./update_mirobody.sh 5 | 6 | set -e 7 | 8 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 9 | PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" 10 | 11 | # Colors 12 | GREEN='\033[0;32m' 13 | BLUE='\033[0;34m' 14 | YELLOW='\033[1;33m' 15 | NC='\033[0m' # No Color 16 | 17 | printf "${BLUE}========================================${NC}\n" 18 | printf "${BLUE} Update Mirobody Version${NC}\n" 19 | printf "${BLUE}========================================${NC}\n\n" 20 | 21 | # Get current version from requirements.txt 22 | CURRENT_VERSION="" 23 | if [ -f "$PROJECT_ROOT/requirements.txt" ]; then 24 | CURRENT_VERSION=$(grep "^mirobody==" "$PROJECT_ROOT/requirements.txt" | sed 's/mirobody==//') 25 | printf "Current version in requirements.txt: ${YELLOW}${CURRENT_VERSION:-Not found}${NC}\n" 26 | fi 27 | 28 | # Query latest version 29 | printf "Querying latest version...\n" 30 | LATEST_VERSION=$(pip index versions mirobody --extra-index-url https://repo.thetahealth.ai/repository/pypi-ai-releases/simple/ 2>/dev/null | grep "LATEST:" | awk '{print $2}') 31 | 32 | if [ -z "$LATEST_VERSION" ]; then 33 | printf "${YELLOW}Could not fetch latest version${NC}\n" 34 | exit 1 35 | fi 36 | 37 | printf "Latest available version: ${GREEN}${LATEST_VERSION}${NC}\n\n" 38 | 39 | # Update requirements.txt 40 | if [ -f "$PROJECT_ROOT/requirements.txt" ]; then 41 | # Check for mirobody with or without version 42 | if grep -q "^mirobody" "$PROJECT_ROOT/requirements.txt"; then 43 | # Replace existing line (whether it has == or not) 44 | sed -i.bak "s/^mirobody.*/mirobody==${LATEST_VERSION}/" "$PROJECT_ROOT/requirements.txt" 45 | else 46 | # Ensure newline before appending if file is not empty and doesn't end with newline 47 | if [ -s "$PROJECT_ROOT/requirements.txt" ] && [ "$(tail -c1 "$PROJECT_ROOT/requirements.txt" | wc -l)" -eq 0 ]; then 48 | echo "" >> "$PROJECT_ROOT/requirements.txt" 49 | fi 50 | echo "mirobody==${LATEST_VERSION}" >> "$PROJECT_ROOT/requirements.txt" 51 | fi 52 | rm -f "$PROJECT_ROOT/requirements.txt.bak" 53 | printf "${GREEN}✓ Updated requirements.txt to mirobody==${LATEST_VERSION}${NC}\n" 54 | else 55 | printf "${YELLOW}requirements.txt not found${NC}\n" 56 | exit 1 57 | fi 58 | 59 | printf "\n${BLUE}========================================${NC}\n" 60 | printf "${GREEN}✓ Version Updated${NC}\n" 61 | printf "${BLUE}========================================${NC}\n\n" 62 | printf " ${YELLOW}${CURRENT_VERSION:-N/A}${NC} → ${GREEN}${LATEST_VERSION}${NC}\n\n" 63 | printf "${YELLOW}Next: Run ./deploy.sh to apply changes${NC}\n\n" 64 | -------------------------------------------------------------------------------- /docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | x-common: &common 2 | pull_policy: never 3 | healthcheck: 4 | interval: 10s 5 | timeout: 5s 6 | retries: 3 7 | start_period: 15s 8 | logging: 9 | driver: "json-file" 10 | options: 11 | max-size: "5m" 12 | max-file: "3" 13 | 14 | services: 15 | backend: 16 | image: mirobody-backend:latest 17 | deploy: 18 | resources: 19 | limits: 20 | memory: 1G 21 | pull_policy: never 22 | restart: always 23 | stop_grace_period: 1s 24 | command: > 25 | sh -c " 26 | cd /app/ && 27 | python -m main 28 | " 29 | ports: 30 | - "18080:18080" 31 | volumes: 32 | - ..:/app 33 | - ../htdoc:/htdoc 34 | - mirobody_upload:/srv/theta_mcp/upload 35 | - mirobody_charts:/srv/theta_mcp/charts 36 | networks: 37 | - mirobody_network 38 | environment: 39 | - PYTHONUNBUFFERED=1 40 | - PYTHONPATH=/app 41 | - TZ=Etc/UTC 42 | - LoggerType=${LoggerType:-json} 43 | - ENV=${ENV:-localdb} 44 | - HTTP_HOST=0.0.0.0 45 | - HTTP_PORT=18080 46 | - HTTP_ROOT=/htdoc 47 | - CONFIG_ENCRYPTION_KEY=${CONFIG_ENCRYPTION_KEY} 48 | 49 | depends_on: 50 | - redis 51 | - db 52 | 53 | db-init: 54 | image: ${POSTGRES_IMAGE:-pgvector/pgvector:pg17} 55 | pull_policy: never 56 | restart: "no" 57 | depends_on: 58 | - db 59 | volumes: 60 | - ./mirobody_fake_people.sql:/sql/mirobody_fake_people.sql:ro 61 | - ./init_fake_data.sh:/init_fake_data.sh:ro 62 | entrypoint: ["/bin/bash", "/init_fake_data.sh"] 63 | networks: 64 | - mirobody_network 65 | environment: 66 | - TZ=Etc/UTC 67 | - PGHOST=db 68 | - PGPORT=5432 69 | # FIXME 70 | - PGUSER=holistic_user 71 | - PGPASSWORD=holistic_password 72 | - PGDATABASE=holistic_db 73 | 74 | db: 75 | image: ${POSTGRES_IMAGE:-pgvector/pgvector:pg17} 76 | deploy: 77 | resources: 78 | limits: 79 | memory: 2G 80 | restart: always 81 | stop_grace_period: 1s 82 | ports: 83 | - "5432:5432" 84 | volumes: 85 | - mirobody_postgres:/var/lib/postgresql/data 86 | - ..:/app 87 | networks: 88 | - mirobody_network 89 | environment: 90 | - TZ=Etc/UTC 91 | - ENV=${ENV:-localdb} 92 | - POSTGRES_USER=holistic_user 93 | - POSTGRES_PASSWORD=holistic_password 94 | - POSTGRES_DB=holistic_db 95 | 96 | redis: 97 | image: ${REDIS_IMAGE:-redis:7.0-alpine} 98 | restart: always 99 | stop_grace_period: 1s 100 | ports: 101 | - "6379:6379" 102 | command: > 103 | redis-server 104 | --bind 0.0.0.0 105 | --port 6379 106 | --protected-mode no 107 | --requirepass your_secure_password_here 108 | --maxmemory 512mb 109 | --maxmemory-policy allkeys-lru 110 | --appendonly yes 111 | --appendfilename "appendonly.aof" 112 | --appendfsync everysec 113 | --loglevel notice 114 | --timeout 60 115 | --tcp-keepalive 30 116 | --io-threads 4 117 | --io-threads-do-reads yes 118 | --tcp-backlog 511 119 | networks: 120 | - mirobody_network 121 | environment: 122 | - TZ=Etc/UTC 123 | 124 | networks: 125 | mirobody_network: 126 | driver: bridge 127 | ipam: 128 | driver: default 129 | 130 | volumes: 131 | mirobody_postgres: 132 | mirobody_upload: 133 | mirobody_charts: 134 | -------------------------------------------------------------------------------- /tools/health/user_service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | User Service 4 | Responsible for user basic information and health profile management 5 | """ 6 | 7 | import logging 8 | 9 | from datetime import datetime 10 | from typing import Any, Dict 11 | 12 | from mirobody.utils.db import execute_query 13 | 14 | #----------------------------------------------------------------------------- 15 | 16 | class UserService: 17 | """User Information Service""" 18 | 19 | def __init__(self): 20 | self.name = "User Service" 21 | self.version = "2.0.0" 22 | 23 | #------------------------------------------------------------------------- 24 | 25 | def _serialize_datetime(self, obj: Any) -> Any: 26 | """Convert datetime objects to ISO format strings for JSON serialization""" 27 | 28 | if isinstance(obj, datetime): 29 | return obj.isoformat() 30 | 31 | elif isinstance(obj, dict): 32 | return {k: self._serialize_datetime(v) for k, v in obj.items()} 33 | 34 | elif isinstance(obj, list): 35 | return [self._serialize_datetime(item) for item in obj] 36 | 37 | return obj 38 | 39 | #------------------------------------------------------------------------- 40 | 41 | async def get_user_health_profile(self, user_info: Dict[str, Any]) -> Dict[str, Any]: 42 | """ 43 | Get user health profile information 44 | 45 | Retrieves comprehensive health profile data from health_user_profile_by_system table. 46 | 47 | Args: 48 | no args needed. 49 | 50 | Returns: 51 | Dictionary containing user health profile data 52 | """ 53 | 54 | try: 55 | user_id = user_info.get("user_id") 56 | user_id = str(user_id) 57 | 58 | # Log for debugging 59 | logging.info(f"Getting health profile for user: {user_id}") 60 | 61 | # Get the latest profile from health_user_profile_by_system table 62 | sql = """ 63 | SELECT last_update_time, common_part FROM theta_ai.health_user_profile_by_system 64 | WHERE user_id = :user_id AND is_deleted = false 65 | ORDER BY version DESC 66 | LIMIT 1 67 | """ 68 | 69 | result = await execute_query(sql, params={"user_id": user_id}) 70 | 71 | if result and isinstance(result, list): 72 | profile_data = result[0] 73 | 74 | # Serialize datetime objects in the profile data 75 | serialized_data = self._serialize_datetime(profile_data) 76 | 77 | return { 78 | "success": True, 79 | "data": serialized_data, 80 | } 81 | 82 | # No data found, use redirect_to_upload to trigger upload flow 83 | return { 84 | "success": False, 85 | "redirect_to_upload": True, 86 | "error": "No health profile data found", 87 | "data": None, 88 | } 89 | 90 | except Exception as e: 91 | logging.error(f"Failed to retrieve user health profile: {str(e)}") 92 | 93 | return { 94 | "success": False, 95 | "error": f"Failed to retrieve user health profile: {str(e)}", 96 | "data": None, 97 | } 98 | 99 | #----------------------------------------------------------------------------- 100 | -------------------------------------------------------------------------------- /tools/health/health_indicator_service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Health Indicator Service 4 | Responsible for searching and retrieving health indicators from time-series data 5 | """ 6 | 7 | import logging 8 | from datetime import datetime 9 | from typing import Any, Dict, List, Optional 10 | 11 | from mirobody.utils.db import execute_query 12 | 13 | #----------------------------------------------------------------------------- 14 | 15 | class HealthIndicatorService: 16 | """Health Indicator Service""" 17 | 18 | def __init__(self): 19 | self.name = "Health Indicator Service" 20 | self.version = "1.0.0" 21 | 22 | #------------------------------------------------------------------------- 23 | 24 | def _serialize_datetime(self, obj: Any) -> Any: 25 | """Convert datetime objects to ISO format strings for JSON serialization""" 26 | 27 | if isinstance(obj, datetime): 28 | return obj.isoformat() 29 | 30 | elif isinstance(obj, dict): 31 | return {k: self._serialize_datetime(v) for k, v in obj.items()} 32 | 33 | elif isinstance(obj, list): 34 | return [self._serialize_datetime(item) for item in obj] 35 | 36 | return obj 37 | 38 | #------------------------------------------------------------------------- 39 | 40 | async def get_health_indicator(self, user_info: Dict[str, Any], keywords: str) -> Dict[str, Any]: 41 | """ 42 | Get health indicators based on keywords 43 | 44 | Searches the theta_ai.th_series_data table for indicators matching the keywords. 45 | 46 | Args: 47 | user_info: User information dictionary (must contain 'user_id') 48 | keywords: Search keywords for indicator name (uses ILIKE matching) 49 | 50 | Returns: 51 | Dictionary containing: 52 | - success: bool 53 | - data: List of matching records (or empty list) 54 | - error: Error message (if applicable) 55 | """ 56 | 57 | try: 58 | user_id = user_info.get("user_id") 59 | if not user_id: 60 | return { 61 | "success": False, 62 | "error": "user_id is required", 63 | "data": None 64 | } 65 | 66 | user_id = str(user_id) 67 | 68 | logging.info(f"Searching health indicators for user: {user_id}, keywords: {keywords}") 69 | 70 | # Select all columns to ensure we get "other fields" like original_data, etc. 71 | # Sorting by column 5 (timestamp) descending to get latest data first 72 | sql = """ 73 | SELECT * FROM theta_ai.th_series_data 74 | WHERE user_id = :user_id 75 | AND indicator ILIKE :keywords 76 | ORDER BY 5 DESC 77 | LIMIT 50 78 | """ 79 | 80 | search_pattern = f"%{keywords}%" 81 | 82 | result = await execute_query(sql, params={"user_id": user_id, "keywords": search_pattern}) 83 | 84 | if result: 85 | # Serialize datetime objects 86 | serialized_data = self._serialize_datetime(result) 87 | return { 88 | "success": True, 89 | "data": serialized_data, 90 | } 91 | 92 | # No matches found 93 | return { 94 | "success": True, 95 | "data": [], 96 | } 97 | 98 | except Exception as e: 99 | logging.error(f"Failed to search health indicators: {str(e)}") 100 | 101 | return { 102 | "success": False, 103 | "error": f"Failed to search health indicators: {str(e)}", 104 | "data": None, 105 | } 106 | 107 | #----------------------------------------------------------------------------- 108 | 109 | -------------------------------------------------------------------------------- /docker/init_fake_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # init_fake_data.sh 4 | # 用于在每次 db 容器启动后,自动导入预置的测试数据 5 | # 6 | # 功能: 7 | # 1. 等待 PostgreSQL 可连接 8 | # 2. 检测 theta_ai schema 下的 3 个表是否存在 9 | # 3. 如果表不存在,等待 60 秒后重试(最多 3 次) 10 | # 4. 表存在后执行 SQL 导入(ON CONFLICT DO NOTHING 保证幂等) 11 | # 12 | 13 | set -e 14 | 15 | # 配置 16 | MAX_RETRIES=3 17 | RETRY_INTERVAL=60 18 | SQL_FILE="/sql/mirobody_fake_people.sql" 19 | 20 | # 需要检测的表 21 | REQUIRED_TABLES=("health_app_user" "health_user_profile_by_system" "th_series_data") 22 | SCHEMA="theta_ai" 23 | 24 | # 日志函数 25 | log_info() { 26 | echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1" 27 | } 28 | 29 | log_warning() { 30 | echo "[WARNING] $(date '+%Y-%m-%d %H:%M:%S') - $1" 31 | } 32 | 33 | log_error() { 34 | echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $1" 35 | } 36 | 37 | # 等待 PostgreSQL 可连接 38 | wait_for_postgres() { 39 | log_info "等待 PostgreSQL 可连接..." 40 | local max_wait=30 41 | local count=0 42 | 43 | while [ $count -lt $max_wait ]; do 44 | if pg_isready -h "$PGHOST" -p "${PGPORT:-5432}" -U "$PGUSER" > /dev/null 2>&1; then 45 | log_info "PostgreSQL 已就绪" 46 | return 0 47 | fi 48 | count=$((count + 1)) 49 | sleep 1 50 | done 51 | 52 | log_error "等待 PostgreSQL 超时(${max_wait}秒)" 53 | return 1 54 | } 55 | 56 | # 检测单个表是否存在 57 | check_table_exists() { 58 | local table_name=$1 59 | local result 60 | 61 | result=$(psql -h "$PGHOST" -p "${PGPORT:-5432}" -U "$PGUSER" -d "$PGDATABASE" -tAc \ 62 | "SELECT EXISTS ( 63 | SELECT FROM information_schema.tables 64 | WHERE table_schema = '$SCHEMA' 65 | AND table_name = '$table_name' 66 | );") 67 | 68 | [ "$result" = "t" ] 69 | } 70 | 71 | # 检测所有必需的表是否存在 72 | check_all_tables_exist() { 73 | local missing_tables=() 74 | 75 | for table in "${REQUIRED_TABLES[@]}"; do 76 | if ! check_table_exists "$table"; then 77 | missing_tables+=("$table") 78 | fi 79 | done 80 | 81 | if [ ${#missing_tables[@]} -eq 0 ]; then 82 | log_info "所有必需的表都已存在: ${REQUIRED_TABLES[*]}" 83 | return 0 84 | else 85 | log_info "缺少以下表: ${missing_tables[*]}" 86 | return 1 87 | fi 88 | } 89 | 90 | # 执行 SQL 导入 91 | execute_sql_import() { 92 | log_info "开始执行 SQL 数据导入: $SQL_FILE" 93 | 94 | if [ ! -f "$SQL_FILE" ]; then 95 | log_error "SQL 文件不存在: $SQL_FILE" 96 | return 1 97 | fi 98 | 99 | # 执行 SQL 文件 100 | if psql -h "$PGHOST" -p "${PGPORT:-5432}" -U "$PGUSER" -d "$PGDATABASE" -f "$SQL_FILE" > /dev/null 2>&1; then 101 | log_info "SQL 数据导入成功完成" 102 | return 0 103 | else 104 | log_error "SQL 数据导入失败" 105 | return 1 106 | fi 107 | } 108 | 109 | # 主函数 110 | main() { 111 | log_info "========== 开始初始化假人数据 ==========" 112 | log_info "数据库: $PGHOST:${PGPORT:-5432}/$PGDATABASE" 113 | 114 | # 等待 PostgreSQL 就绪 115 | if ! wait_for_postgres; then 116 | log_error "无法连接到 PostgreSQL,退出" 117 | exit 1 118 | fi 119 | 120 | # 带重试的表检测 121 | local retry_count=0 122 | while [ $retry_count -lt $MAX_RETRIES ]; do 123 | retry_count=$((retry_count + 1)) 124 | log_info "第 $retry_count 次尝试检测表(共 $MAX_RETRIES 次)" 125 | 126 | if check_all_tables_exist; then 127 | # 表存在,执行导入 128 | if execute_sql_import; then 129 | log_info "========== 初始化完成 ==========" 130 | exit 0 131 | else 132 | log_error "SQL 导入失败,退出" 133 | exit 1 134 | fi 135 | fi 136 | 137 | # 表不存在,等待后重试 138 | if [ $retry_count -lt $MAX_RETRIES ]; then 139 | log_info "等待 ${RETRY_INTERVAL} 秒后重试..." 140 | sleep $RETRY_INTERVAL 141 | fi 142 | done 143 | 144 | # 所有重试都失败 145 | log_warning "经过 $MAX_RETRIES 次尝试后,theta_ai schema 下的表仍未就绪" 146 | log_warning "请确保应用程序已正确创建数据库表结构" 147 | log_info "========== 初始化未完成(表不存在) ==========" 148 | exit 0 # 以 0 退出,避免容器重启循环 149 | } 150 | 151 | # 执行主函数 152 | main 153 | 154 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Activate virtual environment if it exists 4 | if [ -f "venv/bin/activate" ]; then 5 | source venv/bin/activate 6 | elif [ -f ".venv/bin/activate" ]; then 7 | source .venv/bin/activate 8 | fi 9 | 10 | # Load environment variables from .env file if it exists 11 | if [ -f ".env" ]; then 12 | echo "Loading environment variables from .env file..." 13 | set -a # automatically export all variables 14 | source .env 15 | set +a 16 | fi 17 | 18 | #----------------------------------------------------------------------------- 19 | # Global config. 20 | 21 | # Build mode: up, image or build (default: up) 22 | BUILD_MODE="up" 23 | 24 | # Theta registry base URL 25 | THETA_REGISTRY_BASE="theta-public-registry.cn-hangzhou.cr.aliyuncs.com" 26 | 27 | # Cloud images (only for backend) 28 | CLOUD_BACKEND_IMAGE="${THETA_REGISTRY_BASE}/theta/mirobody-backend" 29 | 30 | # Local images 31 | BACKEND_IMAGE="mirobody-backend" 32 | DOCKER_COMPOSE_FILE="docker/docker-compose.yaml" 33 | 34 | #----------------------------------------------------------------------------- 35 | # Parse command line arguments. 36 | 37 | for arg in "$@"; do 38 | case $arg in 39 | --mode=*) 40 | BUILD_MODE="${arg#*=}" 41 | shift 42 | ;; 43 | --help) 44 | echo "Usage: ./deploy.sh [OPTIONS]" 45 | echo "" 46 | echo "Options:" 47 | echo " --mode=image Pull pre-built backend image from theta registry" 48 | echo " --mode=build Build backend from scratch using official base images" 49 | echo " --mode=up Skip build, directly compose up (default)" 50 | echo " --help Show this help message" 51 | echo "" 52 | echo "Examples:" 53 | echo " ./deploy.sh # Skip build and start (default)" 54 | echo " ./deploy.sh --mode=image # Use theta registry images" 55 | echo " ./deploy.sh --mode=build # Build from scratch using official Docker Hub images" 56 | echo " ./deploy.sh --mode=up # Skip build and start" 57 | echo "" 58 | exit 0 59 | ;; 60 | *) 61 | echo "Unknown argument: $arg" 62 | echo "Use --help for usage information" 63 | exit 1 64 | ;; 65 | esac 66 | done 67 | 68 | # Validate build mode 69 | if [[ "$BUILD_MODE" != "up" && "$BUILD_MODE" != "image" && "$BUILD_MODE" != "build" ]]; then 70 | echo "Error: BUILD_MODE must be 'up', 'image' or 'build', got: $BUILD_MODE" 71 | exit 1 72 | fi 73 | 74 | echo "==========================================" 75 | echo "Build Mode: $BUILD_MODE" 76 | echo "==========================================" 77 | 78 | #----------------------------------------------------------------------------- 79 | # Functions. 80 | 81 | # Build backend image based on mode 82 | build_backend() { 83 | local mode=$1 84 | if [[ "$mode" == "image" ]]; then 85 | echo "Pulling backend base image from cloud..." 86 | docker pull $CLOUD_BACKEND_IMAGE:latest 87 | echo "Building backend image with local requirements..." 88 | docker build -f docker/Dockerfile.backend.cloud -t $BACKEND_IMAGE:latest --build-arg THETA_REGISTRY="${THETA_REGISTRY}" . 89 | else 90 | echo "Building backend image from Ubuntu base..." 91 | docker build -f docker/Dockerfile.backend -t $BACKEND_IMAGE:latest --build-arg BASE_REGISTRY="${BASE_REGISTRY}" . 92 | fi 93 | } 94 | 95 | #----------------------------------------------------------------------------- 96 | # Build or prepare images based on mode. 97 | 98 | # Set images and registry prefixes based on mode 99 | if [[ "$BUILD_MODE" == "image" ]]; then 100 | export REDIS_IMAGE="${THETA_REGISTRY_BASE}/docker.io/redis:7.0" 101 | export POSTGRES_IMAGE="${THETA_REGISTRY_BASE}/docker.io/pgvector:pg17" 102 | export BASE_REGISTRY="${THETA_REGISTRY_BASE}/" 103 | export THETA_REGISTRY="${THETA_REGISTRY_BASE}/" 104 | else 105 | # Build and up mode use official images 106 | export REDIS_IMAGE="redis:7.0-alpine" 107 | export POSTGRES_IMAGE="pgvector/pgvector:pg17" 108 | export BASE_REGISTRY="" 109 | export THETA_REGISTRY="" 110 | fi 111 | 112 | if [[ "$BUILD_MODE" == "up" ]]; then 113 | echo "==========================================" 114 | echo "Up Mode: Skip build, use existing images" 115 | echo "==========================================" 116 | 117 | echo "Using existing images:" 118 | echo " - $BACKEND_IMAGE:latest" 119 | 120 | # Check if backend image exists 121 | if ! docker image inspect $BACKEND_IMAGE:latest > /dev/null 2>&1; then 122 | echo "Error: Backend image $BACKEND_IMAGE:latest not found." 123 | echo "Please run with --mode=image or --mode=local first." 124 | exit 1 125 | fi 126 | 127 | elif [[ "$BUILD_MODE" == "image" ]]; then 128 | echo "==========================================" 129 | echo "Image Mode: Pulling pre-built backend" 130 | echo "==========================================" 131 | 132 | build_backend "image" 133 | 134 | else 135 | echo "==========================================" 136 | echo "Build Mode: Building images from scratch" 137 | echo "==========================================" 138 | 139 | build_backend "build" 140 | fi 141 | 142 | #----------------------------------------------------------------------------- 143 | # Docker compose. 144 | 145 | docker compose -f $DOCKER_COMPOSE_FILE down 146 | docker compose -f $DOCKER_COMPOSE_FILE up -d --remove-orphans 147 | docker compose -f $DOCKER_COMPOSE_FILE logs -f 148 | 149 | 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mirobody 2 | 3 | ### Open Source AI-Native Data Engine for Your Personal Data 4 | 5 | Mirobody is not just another chatbot wrapper. It is a privacy-first data platform designed to bridge your data with the latest AI capabilities. It serves as a universal adapter for your tools, fully compliant with the Model Context Protocol (MCP). 6 | 7 | --- 8 | 9 | ### Why Mirobody? 10 | 11 | * **Write Tools Once, Run Everywhere** 12 | 13 | Forget about complex JSON schemas, manual bindings, or router configurations. In Mirobody, **your Python code is the only definition required.** 14 | * Tools built here instantly work in **ChatGPT** (via Apps-SDK) and the entire **MCP Ecosystem** (Claude, Cursor, IDEs). 15 | * Mirobody works simultaneously as an **MCP Client** (to use tools) and an **OAuth-enabled MCP Server** (to provide data), creating a complete data loop. 16 | 17 | * **Your Data Is an Asset, Not a Payload** 18 | 19 | Mirobody is built for **Personal Intelligence**, not just local storage. We believe the next frontier of AI is not knowing more about the world, but knowing more about *you*. 20 | * General AI creates generic answers. Mirobody uses your data to create a **Personal Knowledge Base**, enabling AI to give answers that are truly relevant to your life. 21 | * You can run the entire engine **locally** on your machine. We provide the architecture to unlock your data's value without ever compromising ownership. 22 | 23 | * **Native Agent Engine** 24 | * Powered by a **self-developed agent engine** that fully reproduces **Claude Code's** autonomous capabilities locally. 25 | * Designed to directly load standard **Claude Agent Skills** (Coming Soon), turning your private data into an actionable knowledge base. 26 | 27 | 28 | --- 29 | 30 | ## 🏥 Theta Wellness: Our Health Intelligence App 31 | 32 | **Theta Wellness** is our flagship application built on Mirobody, demonstrating the platform's capabilities in the **Personal Health** domain. We have built a professional-grade **Health Data Analysis** suite that showcases how Mirobody can handle the most complex, multi-modal, and sensitive data environments. 33 | 34 | * **Broad Integration**: Connects with **300+ device manufacturers**, Apple Health, and Google Health. 35 | * **EHR Ready**: Compatible with systems covering **90% of the US population's** Electronic Health Records. 36 | * **Multi-Modal Analysis**: Analyze health data via Voice, Image, Files, or Text. 37 | 38 | > **💡 Empowering the Community** 39 | > 40 | > We are open-sourcing the Mirobody engine because the same architecture that powers our medical-grade Health Agent can power **your business**. 41 | > 42 | > Whether you want to build a **Finance Analyzer**, **Legal Assistant**, or **DevOps Bot**, the infrastructure is ready. We focus on Health; you build the rest. Simply swap the files in the `tools/` directory to start your own vertical. 43 | 44 | 45 | --- 46 | 47 | ## ⚡ Quick Start 48 | 49 | ### 1. Configuration 50 | Initialize your environment in seconds: 51 | 52 | ```bash 53 | cd config 54 | cp config.example.yaml config.yaml 55 | ``` 56 | 57 | > **Note**: 58 | > 59 | > * **LLM Setup**: `OPENROUTER_API_KEY` is required. 60 | > * **Auth Setup**: To enable **Google/Apple OAuth** or **Email Verification**, fill in the respective fields in `config.yaml`. 61 | > * All API keys are encrypted automatically. 62 | 63 | ### 2\. Create Your Tools 64 | 65 | Mirobody adopts a **"Tools-First"** philosophy. No complex binding logic is required. Simply drop your Python scripts into the `tools/` directory: 66 | 67 | * ✨ **Zero Config**: The system auto-discovers your functions. 68 | * 🐍 **Pure Python**: Use the libraries you love (Pandas, NumPy, etc.). 69 | * 🔧 **Universal**: A single tool file works for both REST API and MCP. 70 | 71 | ### 3\. Deployment 72 | 73 | Launch the platform using our unified deployment script. 74 | 75 | **Option A: Image Mode** ⭐ **(Recommended)** 76 | *Downloads pre-built images.* 77 | *Faster deployment with synthetic test user data included in the database (coming soon).* 78 | 79 | ```bash 80 | ./deploy.sh --mode=image 81 | ``` 82 | 83 | **Option B: Build Mode** 84 | *Builds everything from scratch.* 85 | 86 | ```bash 87 | ./deploy.sh --mode=build 88 | ``` 89 | 90 | 91 | 92 | **Daily Startup** 93 | *For regular use after initial setup, simply run:* 94 | 95 | ```bash 96 | ./deploy.sh 97 | ``` 98 | 99 | ----- 100 | 101 | ## 🔐 Access & Authentication 102 | 103 | Once deployed, you can access the platform through the local web interface or our official hosted client. 104 | 105 | ### 1\. Access Interfaces 106 | 107 | | Interface | URL | Description | 108 | |-----------|-----|-------------| 109 | | **Local Web App** | `http://localhost:18080` | Fully self-hosted web interface running locally. | 110 | | **Official Client**| [https://mirobody.ai](https://mirobody.ai) | **Recommended.** Our official web client that connects securely to your local backend service. | 111 | | **MCP Server** | `http://localhost:18080/mcp` | For Claude Desktop / Cursor integration. | 112 | 113 | 114 | To use Mirobody with Cursor, add the following configuration to your MCP settings: 115 | 116 | ```json 117 | "mirobady_mcp": { 118 | "command": "npx", 119 | "args": [ 120 | "-y", 121 | "universal-mcp-proxy" 122 | ], 123 | "env": { 124 | "UMCP_ENDPOINT": "http://localhost:18080/mcp" 125 | } 126 | } 127 | ``` 128 | 129 | ### 2\. Login Methods 130 | 131 | You can choose to configure your own authentication providers or use the pre-set demo account. 132 | 133 | * **Social Login**: Google Account / Apple Account (Requires configuration in `config.yaml`) 134 | * **Email Login**: Email Verification Code (Requires configuration in `config.yaml`) 135 | * **Demo Account** (Instant Access): 136 | * **Users:** `demo1@mirobody.ai`, `demo2@mirobody.ai`, `demo3@mirobody.ai` (More demo users configurable in `config.yaml`) 137 | * **Password:** `777777` 138 | 139 | ----- 140 | 141 | ## 🔌 API Reference 142 | 143 | Mirobody provides standard endpoints for integration: 144 | 145 | | Endpoint | Description | Protocol | 146 | |----------|-------------|----------| 147 | | `/mcp` | MCP Protocol Interface | JSON-RPC 2.0 | 148 | | `/api/chat` | AI Chat Interface | OpenAI Compatible | 149 | | `/api/history` | Session Management | REST | 150 | 151 | ----- 152 | 153 |

154 | Built with ❤️ for the AI Open Source Community. 155 |

156 | -------------------------------------------------------------------------------- /htdoc/assets/logo-MLEW-e8g.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tools/finance/fundamentals_service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Fundamentals Service - yfinance Integration 4 | Fetch key fundamental metrics and financial ratios using yfinance 5 | Provides comprehensive fundamental analysis data including valuation ratios, profitability metrics, and financial health indicators 6 | """ 7 | 8 | import logging 9 | from datetime import datetime 10 | from typing import Any, Dict, Optional 11 | 12 | try: 13 | import yfinance as yf 14 | YFINANCE_AVAILABLE = True 15 | except ImportError: 16 | YFINANCE_AVAILABLE = False 17 | logging.warning("yfinance not installed. Fundamentals service will not be available.") 18 | 19 | 20 | class FundamentalsService: 21 | """Fundamentals Service - Fetch key fundamental metrics using yfinance""" 22 | 23 | def __init__(self): 24 | self.name = "Fundamentals Service" 25 | self.version = "1.0.0" 26 | 27 | if not YFINANCE_AVAILABLE: 28 | logging.error("yfinance library is not available. Please install with: pip install yfinance") 29 | else: 30 | logging.info(f"Fundamentals Service v{self.version} initialized with yfinance integration") 31 | 32 | async def get_key_metrics( 33 | self, 34 | symbol: str, 35 | user_info: Optional[Dict[str, Any]] = None, 36 | ) -> Dict[str, Any]: 37 | """ 38 | Fetch key fundamental metrics and financial ratios using yfinance. 39 | 40 | This tool retrieves comprehensive fundamental analysis data including valuation ratios, 41 | profitability metrics, financial health indicators, and other key metrics for investment analysis. 42 | 43 | Args: 44 | symbol (str): Stock ticker symbol (e.g., "AAPL", "TSLA", "MSFT"). 45 | This is a required parameter. Case-insensitive. 46 | 47 | user_info (Optional[Dict[str, Any]]): User information for logging and tracking purposes. 48 | Not used for authentication in this service. 49 | 50 | Returns: 51 | Dict[str, Any]: A dictionary containing: 52 | - success (bool): Whether the operation succeeded 53 | - data (Dict): Dictionary of fundamental metrics including: 54 | * Valuation Metrics: 55 | - market_cap: Market capitalization 56 | - trailing_pe: Trailing P/E ratio 57 | - forward_pe: Forward P/E ratio 58 | - price_to_book: Price-to-Book ratio 59 | - price_to_sales: Price-to-Sales ratio 60 | - peg_ratio: PEG ratio (P/E to Growth) 61 | - enterprise_value: Enterprise value 62 | - ev_to_revenue: EV/Revenue ratio 63 | - ev_to_ebitda: EV/EBITDA ratio 64 | * Profitability Metrics: 65 | - profit_margins: Net profit margin 66 | - operating_margins: Operating margin 67 | - gross_margins: Gross margin 68 | - return_on_equity: ROE (Return on Equity) 69 | - return_on_assets: ROA (Return on Assets) 70 | - ebitda: EBITDA 71 | * Per-Share Metrics: 72 | - trailing_eps: Trailing Earnings Per Share 73 | - forward_eps: Forward Earnings Per Share 74 | - book_value: Book value per share 75 | - revenue_per_share: Revenue per share 76 | * Dividend Metrics: 77 | - dividend_yield: Dividend yield (as decimal) 78 | - dividend_rate: Annual dividend rate 79 | - payout_ratio: Dividend payout ratio 80 | * Financial Health: 81 | - debt_to_equity: Debt-to-Equity ratio 82 | - current_ratio: Current ratio 83 | - quick_ratio: Quick ratio (Acid-test ratio) 84 | - total_cash: Total cash 85 | - total_debt: Total debt 86 | - free_cashflow: Free cash flow 87 | * Growth & Other: 88 | - revenue_growth: Revenue growth rate 89 | - earnings_growth: Earnings growth rate 90 | - beta: Beta (volatility measure) 91 | - shares_outstanding: Number of shares outstanding 92 | - float_shares: Float shares 93 | - held_percent_insiders: Insider ownership percentage 94 | - held_percent_institutions: Institutional ownership percentage 95 | - metadata (Dict): Metadata about the response: 96 | * symbol: Stock symbol queried 97 | * query_time: Timestamp of the query 98 | * data_source: Data provider (yfinance) 99 | * currency: Currency of financial data 100 | - error (Optional[str]): Error message if the operation failed 101 | 102 | Raises: 103 | ValueError: If required parameters are missing or invalid 104 | Exception: If yfinance API call fails or data processing encounters an error 105 | 106 | Examples: 107 | # Get fundamental metrics for Apple stock 108 | result = await service.get_key_metrics("AAPL") 109 | 110 | # Access specific metrics 111 | if result['success']: 112 | pe_ratio = result['data']['trailing_pe'] 113 | market_cap = result['data']['market_cap'] 114 | roe = result['data']['return_on_equity'] 115 | """ 116 | try: 117 | # Check if yfinance is available 118 | if not YFINANCE_AVAILABLE: 119 | return { 120 | "success": False, 121 | "error": "yfinance library is not installed. Please install with: pip install yfinance", 122 | "data": {}, 123 | "metadata": {} 124 | } 125 | 126 | # Validate required parameters 127 | if not symbol or not symbol.strip(): 128 | raise ValueError("Stock symbol is required and cannot be empty") 129 | 130 | symbol = symbol.strip().upper() 131 | 132 | # Log request information 133 | user_id = user_info.get("user_id") if user_info else "anonymous" 134 | logging.info( 135 | f"Fundamentals request - User: {user_id}, Symbol: {symbol}" 136 | ) 137 | 138 | # Call yfinance API 139 | try: 140 | ticker = yf.Ticker(symbol) 141 | info = ticker.info 142 | except Exception as api_error: 143 | logging.error(f"yfinance API call failed: {str(api_error)}") 144 | raise ValueError(f"Failed to fetch data from yfinance: {str(api_error)}") 145 | 146 | if not info or len(info) == 0: 147 | logging.warning(f"No data returned for symbol: {symbol}") 148 | return { 149 | "success": True, 150 | "data": {}, 151 | "metadata": { 152 | "symbol": symbol, 153 | "query_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 154 | "data_source": "yfinance", 155 | "message": "No data available for the specified symbol" 156 | } 157 | } 158 | 159 | # Helper function to safely extract numeric values 160 | def get_numeric(key: str, default=None) -> Optional[float]: 161 | value = info.get(key) 162 | if value is None or value == 'null' or value == '': 163 | return default 164 | try: 165 | return float(value) 166 | except (ValueError, TypeError): 167 | return default 168 | 169 | # Helper function to safely extract string values 170 | def get_string(key: str, default=None) -> Optional[str]: 171 | value = info.get(key) 172 | return value if value and value != 'null' else default 173 | 174 | # Extract fundamental metrics 175 | metrics = { 176 | # Valuation Metrics 177 | "market_cap": get_numeric("marketCap"), 178 | "trailing_pe": get_numeric("trailingPE"), 179 | "forward_pe": get_numeric("forwardPE"), 180 | "price_to_book": get_numeric("priceToBook"), 181 | "price_to_sales": get_numeric("priceToSalesTrailing12Months"), 182 | "peg_ratio": get_numeric("pegRatio"), 183 | "enterprise_value": get_numeric("enterpriseValue"), 184 | "ev_to_revenue": get_numeric("enterpriseToRevenue"), 185 | "ev_to_ebitda": get_numeric("enterpriseToEbitda"), 186 | 187 | # Profitability Metrics 188 | "profit_margins": get_numeric("profitMargins"), 189 | "operating_margins": get_numeric("operatingMargins"), 190 | "gross_margins": get_numeric("grossMargins"), 191 | "return_on_equity": get_numeric("returnOnEquity"), 192 | "return_on_assets": get_numeric("returnOnAssets"), 193 | "ebitda": get_numeric("ebitda"), 194 | 195 | # Per-Share Metrics 196 | "trailing_eps": get_numeric("trailingEps"), 197 | "forward_eps": get_numeric("forwardEps"), 198 | "book_value": get_numeric("bookValue"), 199 | "revenue_per_share": get_numeric("revenuePerShare"), 200 | 201 | # Dividend Metrics 202 | "dividend_yield": get_numeric("dividendYield"), 203 | "dividend_rate": get_numeric("dividendRate"), 204 | "payout_ratio": get_numeric("payoutRatio"), 205 | 206 | # Financial Health 207 | "debt_to_equity": get_numeric("debtToEquity"), 208 | "current_ratio": get_numeric("currentRatio"), 209 | "quick_ratio": get_numeric("quickRatio"), 210 | "total_cash": get_numeric("totalCash"), 211 | "total_debt": get_numeric("totalDebt"), 212 | "free_cashflow": get_numeric("freeCashflow"), 213 | 214 | # Growth & Other 215 | "revenue_growth": get_numeric("revenueGrowth"), 216 | "earnings_growth": get_numeric("earningsGrowth"), 217 | "beta": get_numeric("beta"), 218 | "shares_outstanding": get_numeric("sharesOutstanding"), 219 | "float_shares": get_numeric("floatShares"), 220 | "held_percent_insiders": get_numeric("heldPercentInsiders"), 221 | "held_percent_institutions": get_numeric("heldPercentInstitutions"), 222 | } 223 | 224 | # Extract metadata 225 | metadata = { 226 | "symbol": symbol, 227 | "query_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 228 | "data_source": "yfinance", 229 | "currency": get_string("currency", "USD"), 230 | "company_name": get_string("longName"), 231 | "sector": get_string("sector"), 232 | "industry": get_string("industry"), 233 | } 234 | 235 | # Count non-null metrics 236 | non_null_count = sum(1 for v in metrics.values() if v is not None) 237 | 238 | logging.info( 239 | f"Successfully fetched {non_null_count} fundamental metrics for {symbol}" 240 | ) 241 | 242 | return { 243 | "success": True, 244 | "data": metrics, 245 | "metadata": metadata 246 | } 247 | 248 | except ValueError as ve: 249 | # Handle validation errors 250 | logging.error(f"Validation error: {str(ve)}") 251 | return { 252 | "success": False, 253 | "error": str(ve), 254 | "data": {}, 255 | "metadata": { 256 | "symbol": symbol if 'symbol' in locals() else None, 257 | "query_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") 258 | } 259 | } 260 | 261 | except Exception as e: 262 | # Handle unexpected errors 263 | logging.error(f"Unexpected error in get_key_metrics: {str(e)}", exc_info=True) 264 | return { 265 | "success": False, 266 | "error": f"An unexpected error occurred: {str(e)}", 267 | "data": {}, 268 | "metadata": { 269 | "symbol": symbol if 'symbol' in locals() else None, 270 | "query_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /user_case/1m_users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Create test users and test API 4 | Complete in one script: create users → configure agents → create profiles → test API calls 5 | """ 6 | 7 | import argparse 8 | import asyncio 9 | import asyncpg 10 | import jwt 11 | import json 12 | import time 13 | import requests 14 | import yaml 15 | import sys 16 | 17 | # Hardcoded JWT key (should match config.optional.yaml) 18 | # Decrypted from: gAAAAABpJiizBb7BmtHTYPTQc7kcKb8qNulxcTcOCPXxXw2zPo6w4H-QCb-htLdQMZl0ZuFFZx2y0jzwZufZ3Nz6s0DvkI7G6A== 19 | HARDCODED_JWT_KEY = "myjwtkey" 20 | 21 | USER_CONFIGS = [ 22 | { 23 | "email": "demo_user_alpha@test.com", 24 | "name": "Test User Alpha", 25 | "gender": 1, 26 | "agents": { 27 | "agent1": { 28 | "system_prompt": "You are a professional health advisor who excels at analyzing user health data and providing personalized recommendations. Your answers should be based on scientific evidence while considering the user's individual circumstances.", 29 | "temperature": 0.7, 30 | "max_tokens": 2000 31 | } 32 | }, 33 | "profile": "This is a male user in his 30s who is focused on cardiovascular health and weight management. He hopes to improve his health through scientific methods, including proper diet and regular exercise. The user has a strong interest in health data analysis and seeks personalized health advice." 34 | }, 35 | { 36 | "email": "demo_user_beta@test.com", 37 | "name": "Test User Beta", 38 | "gender": 2, 39 | "agents": { 40 | "agent2": { 41 | "system_prompt": "You are an experienced health advisor specializing in women's health management and nutritional consulting. You provide comprehensive health advice, including diet, exercise, and lifestyle.", 42 | "temperature": 0.7, 43 | "max_tokens": 2000 44 | }, 45 | "agent3": { 46 | "system_prompt": "You are a professional fitness coach who helps users develop scientific exercise plans and supervises their execution. You provide personalized training programs based on the user's physical condition and goals.", 47 | "temperature": 0.8, 48 | "max_tokens": 2500 49 | } 50 | }, 51 | "profile": "This is a female user in her 30s who is particularly concerned about women's health, nutritional balance, and mental health. She hopes to maintain a balance between work and life and improve overall well-being through a healthy lifestyle. The user is interested in yoga and aerobic exercise, and also pays attention to dietary nutrition." 52 | } 53 | ] 54 | 55 | 56 | async def get_db_connection(): 57 | """Get database connection""" 58 | # Load config 59 | config = {} 60 | for config_file in ['config.required.yaml', 'config.optional.yaml']: 61 | try: 62 | with open(config_file, 'r') as f: 63 | file_config = yaml.safe_load(f) 64 | if file_config: 65 | config.update(file_config) 66 | except FileNotFoundError: 67 | pass 68 | 69 | # Database connection 70 | pg_host = config.get('PG_HOST', 'localhost') 71 | if pg_host == 'db': 72 | pg_host = 'localhost' # Use localhost when running outside Docker 73 | 74 | pg_password = config.get('PG_PASSWORD', 'holistic_password') 75 | if isinstance(pg_password, str) and pg_password.startswith('gAAAAAB'): 76 | # Encrypted password, use Docker default 77 | pg_password = 'holistic_password' 78 | 79 | conn = await asyncpg.connect( 80 | host=pg_host, 81 | port=config.get('PG_PORT', 5432), 82 | user=config.get('PG_USER', 'holistic_user'), 83 | password=pg_password, 84 | database=config.get('PG_DBNAME', 'holistic_db') 85 | ) 86 | 87 | return conn 88 | 89 | 90 | async def create_or_get_user(conn, user_config): 91 | """Create or get user""" 92 | email = user_config['email'] 93 | name = user_config['name'] 94 | gender = user_config['gender'] 95 | 96 | # Check if user already exists 97 | user = await conn.fetchrow(''' 98 | SELECT id, email, name, gender 99 | FROM theta_ai.health_app_user 100 | WHERE email = $1 AND is_del = false 101 | ''', email) 102 | 103 | if user: 104 | print(f" ℹ️ User already exists: {email} (ID: {user['id']})") 105 | return user['id'] 106 | 107 | # Create new user 108 | user_id = await conn.fetchval(''' 109 | INSERT INTO theta_ai.health_app_user (email, name, gender, is_del) 110 | VALUES ($1, $2, $3, false) 111 | RETURNING id 112 | ''', email, name, gender) 113 | 114 | print(f" ✓ Created successfully: {email} (ID: {user_id})") 115 | return user_id 116 | 117 | 118 | async def upsert_user_agents(conn, user_id, agents): 119 | """Insert or update user agents configuration""" 120 | await conn.execute(''' 121 | INSERT INTO theta_ai.user_agent_prompt (user_id, prompt, created_at, updated_at) 122 | VALUES ($1, $2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) 123 | ON CONFLICT (user_id) 124 | DO UPDATE SET 125 | prompt = EXCLUDED.prompt, 126 | updated_at = CURRENT_TIMESTAMP 127 | ''', str(user_id), json.dumps(agents)) 128 | 129 | agent_names = ', '.join(agents.keys()) 130 | print(f" ✓ Agents configuration completed: {agent_names}") 131 | 132 | 133 | async def upsert_user_profile(conn, user_id, name, profile): 134 | await conn.execute(f"delete from theta_ai.health_user_profile_by_system where user_id ='{user_id}'") 135 | await conn.execute(''' 136 | INSERT INTO theta_ai.health_user_profile_by_system 137 | (user_id, name, version, common_part, create_time, last_update_time, is_deleted, last_execute_doc_id) 138 | VALUES ($1, $2, 1, $3, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, false, 0) 139 | ''', str(user_id), f'{name} Health Profile', profile) 140 | 141 | print(f" ✓ Health profile created") 142 | 143 | 144 | def generate_jwt_token(email, user_id): 145 | """Generate JWT token""" 146 | jwt_key = HARDCODED_JWT_KEY 147 | 148 | payload = { 149 | 'sub': email, 150 | 'user_id': str(user_id), 151 | 'iss': 'theta_oauth', 152 | 'aud': 'theta', 153 | 'iat': int(time.time()), 154 | 'exp': int(time.time()) + 3600 155 | } 156 | 157 | token = jwt.encode(payload, jwt_key, algorithm='HS256') 158 | return token 159 | 160 | 161 | def call_chat_api(token, question, agent_name, server='http://localhost:18080'): 162 | """Call chat API""" 163 | url = f"{server}/api/chat" 164 | headers = { 165 | 'Content-Type': 'application/json', 166 | 'Authorization': f'Bearer {token}' 167 | } 168 | 169 | payload = { 170 | 'question': question, 171 | 'prompt_name': agent_name 172 | } 173 | 174 | try: 175 | response = requests.post(url, json=payload, headers=headers, timeout=60, stream=True) 176 | 177 | if response.status_code == 200: 178 | content_type = response.headers.get('Content-Type', '') 179 | 180 | # Check if it's a streaming response (SSE) 181 | if 'text/event-stream' in content_type or 'stream' in content_type.lower(): 182 | print(f" 📡 Streaming response:") 183 | print(f" {'─'*50}") 184 | full_content = "" 185 | has_content = False 186 | 187 | for line in response.iter_lines(): 188 | if line: 189 | line_str = line.decode('utf-8') 190 | # SSE format: data: {...} 191 | if line_str.startswith('data: '): 192 | data_str = line_str[6:] 193 | try: 194 | data = json.loads(data_str) 195 | if 'content' in data and data['content']: 196 | print(data['content'], end='', flush=True) 197 | full_content += data['content'] 198 | has_content = True 199 | elif 'delta' in data and 'content' in data['delta']: 200 | content = data['delta']['content'] 201 | if content: 202 | print(content, end='', flush=True) 203 | full_content += content 204 | has_content = True 205 | elif 'error' in data: 206 | print(f"\n ❌ Error: {json.dumps(data['error'], ensure_ascii=False)}") 207 | has_content = True 208 | except json.JSONDecodeError: 209 | if data_str.strip(): 210 | print(data_str, end='', flush=True) 211 | full_content += data_str 212 | has_content = True 213 | 214 | print(f"\n {'─'*50}") 215 | 216 | if not has_content: 217 | print(" ⚠️ Warning: Received empty response") 218 | return None 219 | 220 | return {'content': full_content, 'type': 'stream'} 221 | else: 222 | # Regular JSON response 223 | result = response.json() 224 | print(f" ✓ Response: {json.dumps(result, ensure_ascii=False)[:100]}...") 225 | return result 226 | else: 227 | print(f" ❌ API call failed: {response.status_code}") 228 | print(f" Response: {response.text[:200]}") 229 | return None 230 | 231 | except Exception as e: 232 | print(f" ❌ Call error: {e}") 233 | return None 234 | 235 | 236 | async def process_user(conn, user_config, args): 237 | """Process single user: create → configure → test""" 238 | print(f"\n{'='*60}") 239 | print(f"📝 Processing user: {user_config['name']} ({user_config['email']})") 240 | print(f"{'='*60}") 241 | 242 | try: 243 | # 1. Create user 244 | print("\n▶ Creating user account...") 245 | user_id = await create_or_get_user(conn, user_config) 246 | 247 | # 2. Configure agents 248 | print("\n▶ Configuring user agents...") 249 | await upsert_user_agents(conn, user_id, user_config['agents']) 250 | 251 | # 3. Create health profile 252 | print("\n▶ Creating health profile...") 253 | await upsert_user_profile(conn, user_id, user_config['name'], user_config['profile']) 254 | 255 | # 4. Generate JWT token 256 | print("\n▶ Generating JWT token...") 257 | token = generate_jwt_token(user_config['email'], user_id) 258 | print(f" ✓ Token generated successfully (first 50 chars): {token[:50]}...") 259 | 260 | # 5. Test API (if not skipped) 261 | if not args.skip_test: 262 | print("\n▶ Testing API call...") 263 | # Get first agent name 264 | first_agent = list(user_config['agents'].keys())[0] 265 | print(f" Agent: {first_agent}") 266 | print(f" Question: {args.question}") 267 | 268 | result = call_chat_api(token, args.question, first_agent, args.server) 269 | 270 | if result: 271 | print(f"\n ✅ API test successful!") 272 | else: 273 | print(f"\n ❌ API test failed!") 274 | 275 | print(f"\n✅ User {user_config['name']} processing completed!") 276 | print(f" ID: {user_id}") 277 | print(f" Email: {user_config['email']}") 278 | print(f" Agents: {', '.join(user_config['agents'].keys())}") 279 | 280 | return True 281 | 282 | except Exception as e: 283 | print(f"\n❌ User processing failed: {e}") 284 | import traceback 285 | traceback.print_exc() 286 | return False 287 | 288 | 289 | async def main(): 290 | parser = argparse.ArgumentParser(description='Create test users and test API') 291 | parser.add_argument('--question', '-q', default='Hello, please introduce yourself', help='Test question') 292 | parser.add_argument('--server', default='http://localhost:18080', help='Server address') 293 | parser.add_argument('--skip-test', action='store_true', help='Skip API test') 294 | 295 | args = parser.parse_args() 296 | 297 | print("="*60) 298 | print("🚀 Creating test users and data") 299 | print("="*60) 300 | 301 | # Connect to database 302 | try: 303 | conn = await get_db_connection() 304 | print("✓ Database connection successful\n") 305 | except Exception as e: 306 | print(f"❌ Database connection failed: {e}") 307 | sys.exit(1) 308 | 309 | # Process all users 310 | success_count = 0 311 | fail_count = 0 312 | 313 | for user_config in USER_CONFIGS: 314 | success = await process_user(conn, user_config, args) 315 | if success: 316 | success_count += 1 317 | else: 318 | fail_count += 1 319 | 320 | # Close connection 321 | await conn.close() 322 | 323 | # Display summary 324 | print("\n" + "="*60) 325 | print("📊 Creation completed") 326 | print("="*60) 327 | print(f"✅ Successful: {success_count} users") 328 | if fail_count > 0: 329 | print(f"❌ Failed: {fail_count} users") 330 | 331 | print("\n💡 Next steps:") 332 | print(" 1. Login with the following emails:") 333 | for user_config in USER_CONFIGS: 334 | print(f" - {user_config['email']}") 335 | print(" 2. Verification code: 000000") 336 | print(" 3. Start using the configured agents") 337 | print() 338 | 339 | if fail_count > 0: 340 | sys.exit(1) 341 | 342 | 343 | if __name__ == '__main__': 344 | asyncio.run(main()) 345 | 346 | -------------------------------------------------------------------------------- /tools/health/genetic_service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Genetic Service 4 | Responsible for genetic data management and querying 5 | """ 6 | 7 | import traceback 8 | from typing import Any, Dict, List, Optional, Union 9 | 10 | from mirobody.utils.data import DataConverter 11 | from mirobody.utils.db import execute_query 12 | from mirobody.utils.log import log_setter 13 | 14 | 15 | class GeneticService(): 16 | """Genetic data service""" 17 | 18 | # Constants 19 | MAX_NEARBY_VARIANTS_PER_QUERY = 20 20 | DEFAULT_NEARBY_RANGE = 1000000 # 1M base pairs 21 | 22 | def __init__(self): 23 | self.name = "Genetic Service" 24 | self.version = "1.0.0" 25 | self.data_converter = DataConverter() 26 | 27 | def _build_rsid_conditions( 28 | self, rsid: Union[str, List[str]], params: Dict[str, Any] 29 | ) -> str: 30 | """ 31 | Build SQL conditions for rsid parameter. 32 | 33 | Args: 34 | rsid: Variant identifier(s), supports single string or string list 35 | params: Parameter dictionary to update 36 | 37 | Returns: 38 | SQL condition string 39 | """ 40 | # Handle comma-separated string 41 | if isinstance(rsid, str) and "," in rsid: 42 | rsid_list = [r.strip() for r in rsid.split(",") if r.strip()] 43 | placeholders = [f":rsid_{i}" for i in range(len(rsid_list))] 44 | for i, r in enumerate(rsid_list): 45 | params[f"rsid_{i}"] = r 46 | return f" AND rsid IN ({', '.join(placeholders)})" 47 | 48 | # Handle list 49 | elif isinstance(rsid, list): 50 | if len(rsid) == 1: 51 | params["rsid"] = rsid[0] 52 | return " AND rsid = :rsid" 53 | else: 54 | placeholders = [f":rsid_{i}" for i in range(len(rsid))] 55 | for i, r in enumerate(rsid): 56 | params[f"rsid_{i}"] = r 57 | return f" AND rsid IN ({', '.join(placeholders)})" 58 | 59 | # Single rsid 60 | else: 61 | params["rsid"] = rsid 62 | return " AND rsid = :rsid" 63 | 64 | def _format_compact_record( 65 | self, record: Dict[str, Any], distance: Optional[int] = None, query_rsid: Optional[str] = None 66 | ) -> Dict[str, Any]: 67 | """ 68 | Format record into compact format. 69 | 70 | Args: 71 | record: Original record 72 | distance: Distance from query position (for nearby variants) 73 | query_rsid: Query rsid (for nearby variants) 74 | 75 | Returns: 76 | Compact formatted record 77 | """ 78 | compact_record = { 79 | "r": record.get("rsid"), 80 | "c": record.get("chromosome"), 81 | "p": record.get("position"), 82 | "g": record.get("genotype"), 83 | } 84 | 85 | if distance is not None: 86 | compact_record["d"] = distance 87 | if query_rsid is not None: 88 | compact_record["q"] = query_rsid 89 | 90 | # Keep only non-null values 91 | return {k: v for k, v in compact_record.items() if v is not None} 92 | 93 | async def _query_nearby_variants( 94 | self, 95 | user_id: str, 96 | queried_positions: Dict[str, List[int]], 97 | queried_rsids: set, 98 | nearby_range: int, 99 | limit: int, 100 | result: List[Dict[str, Any]], 101 | ) -> List[Dict[str, Any]]: 102 | """ 103 | Query nearby variants for all queried positions in a single optimized query. 104 | 105 | Args: 106 | user_id: User identifier 107 | queried_positions: Dictionary of chromosome -> positions 108 | queried_rsids: Set of already queried rsids to exclude 109 | nearby_range: Search range in base pairs 110 | limit: Maximum results per variant 111 | result: Original query results 112 | 113 | Returns: 114 | List of nearby variants in compact format 115 | """ 116 | if not queried_positions: 117 | return [] 118 | 119 | nearby_results = [] 120 | nearby_limit = min(self.MAX_NEARBY_VARIANTS_PER_QUERY, limit) 121 | 122 | # Build optimized query using UNION ALL for multiple positions 123 | union_queries = [] 124 | union_params = {"user_id": user_id, "nearby_limit": nearby_limit} 125 | param_index = 0 126 | 127 | for chr_key, positions in queried_positions.items(): 128 | for pos in positions: 129 | # Create unique parameter names for each position 130 | chr_param = f"chr_{param_index}" 131 | min_pos_param = f"min_pos_{param_index}" 132 | max_pos_param = f"max_pos_{param_index}" 133 | target_pos_param = f"target_pos_{param_index}" 134 | 135 | union_params[chr_param] = chr_key 136 | union_params[min_pos_param] = pos - nearby_range 137 | union_params[max_pos_param] = pos + nearby_range 138 | union_params[target_pos_param] = pos 139 | 140 | # Build exclude clause 141 | exclude_clause = "" 142 | if queried_rsids: 143 | exclude_rsids_list = list(queried_rsids) 144 | exclude_params = [f"exclude_{param_index}_{i}" for i in range(len(exclude_rsids_list))] 145 | for i, rsid in enumerate(exclude_rsids_list): 146 | union_params[f"exclude_{param_index}_{i}"] = rsid 147 | exclude_clause = f"AND rsid NOT IN ({', '.join([':' + p for p in exclude_params])})" 148 | 149 | union_queries.append(f""" 150 | SELECT 151 | rsid, chromosome, position, genotype, 152 | ABS(position - :{target_pos_param}) as distance, 153 | :{target_pos_param} as query_position, 154 | :{chr_param} as query_chromosome 155 | FROM theta_ai.th_series_data_genetic 156 | WHERE user_id = :user_id 157 | AND is_deleted = false 158 | AND chromosome = :{chr_param} 159 | AND position BETWEEN :{min_pos_param} AND :{max_pos_param} 160 | {exclude_clause} 161 | """) 162 | 163 | param_index += 1 164 | 165 | # Combine all queries with UNION ALL and apply global ordering and limit 166 | if union_queries: 167 | combined_sql = f""" 168 | SELECT * FROM ( 169 | {' UNION ALL '.join(union_queries)} 170 | ) AS combined 171 | ORDER BY query_chromosome, query_position, distance 172 | LIMIT :nearby_limit 173 | """ 174 | 175 | nearby_data = await execute_query( 176 | combined_sql, 177 | union_params, 178 | mode="async", 179 | query_type="select", 180 | ) 181 | 182 | if nearby_data: 183 | nearby_converted = await self.data_converter.convert_list(nearby_data) 184 | 185 | # Format results 186 | for nearby_record in nearby_converted: 187 | # Find the corresponding query rsid 188 | query_pos = nearby_record.get("query_position") 189 | query_chr = nearby_record.get("query_chromosome") 190 | query_rsid = next( 191 | (r.get("rsid") for r in result 192 | if r.get("position") == query_pos and r.get("chromosome") == query_chr), 193 | None 194 | ) 195 | 196 | compact_record = self._format_compact_record( 197 | nearby_record, 198 | distance=nearby_record.get("distance"), 199 | query_rsid=query_rsid 200 | ) 201 | nearby_results.append(compact_record) 202 | 203 | return nearby_results 204 | 205 | async def get_genetic_data( 206 | self, 207 | rsid: Union[str, List[str]], 208 | user_info: Dict[str, Any], 209 | chromosome: Optional[str] = None, 210 | position: Optional[int] = None, 211 | genotype: Optional[str] = None, 212 | limit: int = 100, 213 | offset: int = 0, 214 | include_nearby: bool = True, 215 | nearby_range: Optional[int] = None, 216 | ) -> Dict[str, Any]: 217 | """ 218 | Retrieve variant information by rsid, with optional lookup of nearby related variants. 219 | 220 | Args: 221 | rsid: Variant identifier(s), supports single string or string list 222 | chromosome: Chromosome reference 223 | position: Genomic position 224 | genotype: Genotype information (if available) 225 | limit: Maximum number of records to return 226 | offset: Pagination offset 227 | include_nearby: Whether to include nearby variants 228 | nearby_range: Search range for nearby variants (in base pairs) 229 | 230 | Returns: 231 | Dictionary containing variant data for the requested rsid(s), optionally 232 | including related variants in the specified nearby range. 233 | """ 234 | try: 235 | # Get user ID from user_info 236 | user_id = user_info.get("user_id") 237 | if not user_id: 238 | return { 239 | "success": False, 240 | "error": "User ID is required", 241 | "data": None, 242 | } 243 | 244 | # Set default nearby range 245 | if nearby_range is None: 246 | nearby_range = self.DEFAULT_NEARBY_RANGE 247 | 248 | # Build basic query 249 | sql = """ 250 | SELECT id, user_id, rsid, chromosome, position, genotype, 251 | create_time, update_time 252 | FROM theta_ai.th_series_data_genetic 253 | WHERE user_id = :user_id AND is_deleted = false 254 | """ 255 | 256 | # Build parameter dictionary 257 | params = {"user_id": user_id} 258 | 259 | # Handle rsid parameter using helper method 260 | sql += self._build_rsid_conditions(rsid, params) 261 | 262 | if chromosome: 263 | sql += " AND chromosome = :chromosome" 264 | params["chromosome"] = chromosome 265 | 266 | if position: 267 | sql += " AND position = :position" 268 | params["position"] = position 269 | 270 | if genotype: 271 | sql += " AND genotype = :genotype" 272 | params["genotype"] = genotype 273 | 274 | # Add sorting and pagination 275 | sql += " ORDER BY chromosome, position" 276 | sql += " LIMIT :limit OFFSET :offset" 277 | params["limit"] = limit 278 | params["offset"] = offset 279 | 280 | # Execute query 281 | result = await execute_query(sql, params, mode="async", query_type="select") 282 | 283 | # Debug logging 284 | log_setter(level="info", _input=f"Query results type: {type(result)}, length: {len(result) if result else 0}") 285 | 286 | # Data conversion 287 | result = await self.data_converter.convert_list(result) 288 | 289 | # Convert to compact format using helper method 290 | compact_result = [self._format_compact_record(record) for record in result] 291 | 292 | # Collect queried variant information (optimized) 293 | queried_positions = {} 294 | queried_rsids = {record.get("rsid") for record in result if record.get("rsid")} 295 | 296 | for record in result: 297 | if record.get("chromosome") and record.get("position"): 298 | chr_key = record["chromosome"] 299 | queried_positions.setdefault(chr_key, []).append(record["position"]) 300 | 301 | # Query nearby variants using optimized single-query approach 302 | nearby_results = [] 303 | if include_nearby and queried_positions: 304 | nearby_results = await self._query_nearby_variants( 305 | user_id=user_id, 306 | queried_positions=queried_positions, 307 | queried_rsids=queried_rsids, 308 | nearby_range=nearby_range, 309 | limit=limit, 310 | result=result, 311 | ) 312 | 313 | log_setter(level="info", _input=f"Query completed, returning {len(result)} genetic records, {len(nearby_results)} nearby variants") 314 | 315 | # Fallback strategy: if no genetic data 316 | if not result: 317 | log_setter(level="info", _input="No genetic data found, returning structured no-data response") 318 | 319 | return { 320 | "success": True, 321 | "message": "No genetic data found. To access genetic analysis including SNPs, genotypes, chromosomes, and positions, please upload your genetic information first.", 322 | "data": "No genetic data available for the requested variant(s). Please upload your genetic test results from services like 23andMe, AncestryDNA, or medical genetic testing to access personalized genetic insights.", 323 | "limit": limit, 324 | "offset": offset, 325 | "upload_suggestion": { 326 | "message": "No genetic data available for analysis. To get personalized genetic insights and understand your genetic variations, please upload your genetic test results.", 327 | "upload_url": "https://localhost:18080/drive", 328 | "instructions": "Upload your genetic test results from services like 23andMe, AncestryDNA, or medical genetic testing to enable comprehensive genetic analysis and health risk assessment.", 329 | }, 330 | "redirect_to_upload": True, 331 | } 332 | 333 | # Apply data truncation with compact format 334 | response_data = { 335 | "success": True, 336 | "data": { 337 | "q": compact_result, # queried variants 338 | "n": nearby_results if include_nearby else [], # nearby variants 339 | "s": { # summary 340 | "tq": len(result), # total queried 341 | "tn": len(nearby_results) if include_nearby else 0, # total nearby 342 | "chr": list(queried_positions.keys()), # chromosomes 343 | "range_kb": nearby_range // 1000 if include_nearby else 0, # range in kb 344 | }, 345 | "_legend": { 346 | "r": "rsid", 347 | "c": "chromosome", 348 | "p": "position", 349 | "g": "genotype", 350 | "d": "distance_from_query", 351 | "q": "query_rsid", 352 | "tq": "total_queried", 353 | "tn": "total_nearby", 354 | "chr": "chromosomes", 355 | }, 356 | }, 357 | "limit": limit, 358 | "offset": offset, 359 | } 360 | return response_data 361 | 362 | except Exception as e: 363 | log_setter(level="error", _input=f"{str(e)}, {traceback.format_exc()}") 364 | 365 | return { 366 | "success": False, 367 | "error": f"Failed to get genetic data: {str(e)}", 368 | "data": None, 369 | "redirect_to_upload": True, 370 | } 371 | -------------------------------------------------------------------------------- /htdoc/github-markdown-light.css: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | -ms-text-size-adjust: 100%; 3 | -webkit-text-size-adjust: 100%; 4 | margin: 0; 5 | color: #24292f; 6 | background-color: #ffffff; 7 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; 8 | font-size: 16px; 9 | line-height: 1.5; 10 | word-wrap: break-word; 11 | } 12 | 13 | .markdown-body .octicon { 14 | display: inline-block; 15 | fill: currentColor; 16 | vertical-align: text-bottom; 17 | } 18 | 19 | .markdown-body h1:hover .anchor .octicon-link:before, 20 | .markdown-body h2:hover .anchor .octicon-link:before, 21 | .markdown-body h3:hover .anchor .octicon-link:before, 22 | .markdown-body h4:hover .anchor .octicon-link:before, 23 | .markdown-body h5:hover .anchor .octicon-link:before, 24 | .markdown-body h6:hover .anchor .octicon-link:before { 25 | width: 16px; 26 | height: 16px; 27 | content: ' '; 28 | display: inline-block; 29 | background-color: currentColor; 30 | -webkit-mask-image: url("data:image/svg+xml,"); 31 | mask-image: url("data:image/svg+xml,"); 32 | } 33 | 34 | .markdown-body details, 35 | .markdown-body figcaption, 36 | .markdown-body figure { 37 | display: block; 38 | } 39 | 40 | .markdown-body summary { 41 | display: list-item; 42 | } 43 | 44 | .markdown-body [hidden] { 45 | display: none !important; 46 | } 47 | 48 | .markdown-body a { 49 | background-color: transparent; 50 | color: #0969da; 51 | text-decoration: none; 52 | } 53 | 54 | .markdown-body abbr[title] { 55 | border-bottom: none; 56 | text-decoration: underline dotted; 57 | } 58 | 59 | .markdown-body b, 60 | .markdown-body strong { 61 | font-weight: 600; 62 | } 63 | 64 | .markdown-body dfn { 65 | font-style: italic; 66 | } 67 | 68 | .markdown-body h1 { 69 | margin: .67em 0; 70 | font-weight: 600; 71 | padding-bottom: .3em; 72 | font-size: 2em; 73 | border-bottom: 1px solid hsla(210,18%,87%,1); 74 | } 75 | 76 | .markdown-body mark { 77 | background-color: #fff8c5; 78 | color: #24292f; 79 | } 80 | 81 | .markdown-body small { 82 | font-size: 90%; 83 | } 84 | 85 | .markdown-body sub, 86 | .markdown-body sup { 87 | font-size: 75%; 88 | line-height: 0; 89 | position: relative; 90 | vertical-align: baseline; 91 | } 92 | 93 | .markdown-body sub { 94 | bottom: -0.25em; 95 | } 96 | 97 | .markdown-body sup { 98 | top: -0.5em; 99 | } 100 | 101 | .markdown-body img { 102 | border-style: none; 103 | max-width: 100%; 104 | box-sizing: content-box; 105 | background-color: #ffffff; 106 | } 107 | 108 | .markdown-body code, 109 | .markdown-body kbd, 110 | .markdown-body pre, 111 | .markdown-body samp { 112 | font-family: monospace; 113 | font-size: 1em; 114 | } 115 | 116 | .markdown-body figure { 117 | margin: 1em 40px; 118 | } 119 | 120 | .markdown-body hr { 121 | box-sizing: content-box; 122 | overflow: hidden; 123 | background: transparent; 124 | border-bottom: 1px solid hsla(210,18%,87%,1); 125 | height: .25em; 126 | padding: 0; 127 | margin: 24px 0; 128 | background-color: #d0d7de; 129 | border: 0; 130 | } 131 | 132 | .markdown-body input { 133 | font: inherit; 134 | margin: 0; 135 | overflow: visible; 136 | font-family: inherit; 137 | font-size: inherit; 138 | line-height: inherit; 139 | } 140 | 141 | .markdown-body [type=button], 142 | .markdown-body [type=reset], 143 | .markdown-body [type=submit] { 144 | -webkit-appearance: button; 145 | } 146 | 147 | .markdown-body [type=checkbox], 148 | .markdown-body [type=radio] { 149 | box-sizing: border-box; 150 | padding: 0; 151 | } 152 | 153 | .markdown-body [type=number]::-webkit-inner-spin-button, 154 | .markdown-body [type=number]::-webkit-outer-spin-button { 155 | height: auto; 156 | } 157 | 158 | .markdown-body [type=search]::-webkit-search-cancel-button, 159 | .markdown-body [type=search]::-webkit-search-decoration { 160 | -webkit-appearance: none; 161 | } 162 | 163 | .markdown-body ::-webkit-input-placeholder { 164 | color: inherit; 165 | opacity: .54; 166 | } 167 | 168 | .markdown-body ::-webkit-file-upload-button { 169 | -webkit-appearance: button; 170 | font: inherit; 171 | } 172 | 173 | .markdown-body a:hover { 174 | text-decoration: underline; 175 | } 176 | 177 | .markdown-body ::placeholder { 178 | color: #6e7781; 179 | opacity: 1; 180 | } 181 | 182 | .markdown-body hr::before { 183 | display: table; 184 | content: ""; 185 | } 186 | 187 | .markdown-body hr::after { 188 | display: table; 189 | clear: both; 190 | content: ""; 191 | } 192 | 193 | .markdown-body table { 194 | border-spacing: 0; 195 | border-collapse: collapse; 196 | display: block; 197 | width: max-content; 198 | max-width: 100%; 199 | overflow: auto; 200 | } 201 | 202 | .markdown-body td, 203 | .markdown-body th { 204 | padding: 0; 205 | } 206 | 207 | .markdown-body details summary { 208 | cursor: pointer; 209 | } 210 | 211 | .markdown-body details:not([open])>*:not(summary) { 212 | display: none !important; 213 | } 214 | 215 | .markdown-body a:focus, 216 | .markdown-body [role=button]:focus, 217 | .markdown-body input[type=radio]:focus, 218 | .markdown-body input[type=checkbox]:focus { 219 | outline: 2px solid #0969da; 220 | outline-offset: -2px; 221 | box-shadow: none; 222 | } 223 | 224 | .markdown-body a:focus:not(:focus-visible), 225 | .markdown-body [role=button]:focus:not(:focus-visible), 226 | .markdown-body input[type=radio]:focus:not(:focus-visible), 227 | .markdown-body input[type=checkbox]:focus:not(:focus-visible) { 228 | outline: solid 1px transparent; 229 | } 230 | 231 | .markdown-body a:focus-visible, 232 | .markdown-body [role=button]:focus-visible, 233 | .markdown-body input[type=radio]:focus-visible, 234 | .markdown-body input[type=checkbox]:focus-visible { 235 | outline: 2px solid #0969da; 236 | outline-offset: -2px; 237 | box-shadow: none; 238 | } 239 | 240 | .markdown-body a:not([class]):focus, 241 | .markdown-body a:not([class]):focus-visible, 242 | .markdown-body input[type=radio]:focus, 243 | .markdown-body input[type=radio]:focus-visible, 244 | .markdown-body input[type=checkbox]:focus, 245 | .markdown-body input[type=checkbox]:focus-visible { 246 | outline-offset: 0; 247 | } 248 | 249 | .markdown-body kbd { 250 | display: inline-block; 251 | padding: 3px 5px; 252 | font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; 253 | line-height: 10px; 254 | color: #24292f; 255 | vertical-align: middle; 256 | background-color: #f6f8fa; 257 | border: solid 1px rgba(175,184,193,0.2); 258 | border-bottom-color: rgba(175,184,193,0.2); 259 | border-radius: 6px; 260 | box-shadow: inset 0 -1px 0 rgba(175,184,193,0.2); 261 | } 262 | 263 | .markdown-body h1, 264 | .markdown-body h2, 265 | .markdown-body h3, 266 | .markdown-body h4, 267 | .markdown-body h5, 268 | .markdown-body h6 { 269 | margin-top: 24px; 270 | margin-bottom: 16px; 271 | font-weight: 600; 272 | line-height: 1.25; 273 | } 274 | 275 | .markdown-body h2 { 276 | font-weight: 600; 277 | padding-bottom: .3em; 278 | font-size: 1.5em; 279 | border-bottom: 1px solid hsla(210,18%,87%,1); 280 | } 281 | 282 | .markdown-body h3 { 283 | font-weight: 600; 284 | font-size: 1.25em; 285 | } 286 | 287 | .markdown-body h4 { 288 | font-weight: 600; 289 | font-size: 1em; 290 | } 291 | 292 | .markdown-body h5 { 293 | font-weight: 600; 294 | font-size: .875em; 295 | } 296 | 297 | .markdown-body h6 { 298 | font-weight: 600; 299 | font-size: .85em; 300 | color: #57606a; 301 | } 302 | 303 | .markdown-body p { 304 | margin-top: 0; 305 | margin-bottom: 10px; 306 | } 307 | 308 | .markdown-body blockquote { 309 | margin: 0; 310 | padding: 0 1em; 311 | color: #57606a; 312 | border-left: .25em solid #d0d7de; 313 | } 314 | 315 | .markdown-body ul, 316 | .markdown-body ol { 317 | margin-top: 0; 318 | margin-bottom: 0; 319 | padding-left: 2em; 320 | } 321 | 322 | .markdown-body ol ol, 323 | .markdown-body ul ol { 324 | list-style-type: lower-roman; 325 | } 326 | 327 | .markdown-body ul ul ol, 328 | .markdown-body ul ol ol, 329 | .markdown-body ol ul ol, 330 | .markdown-body ol ol ol { 331 | list-style-type: lower-alpha; 332 | } 333 | 334 | .markdown-body dd { 335 | margin-left: 0; 336 | } 337 | 338 | .markdown-body tt, 339 | .markdown-body code, 340 | .markdown-body samp { 341 | font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; 342 | font-size: 12px; 343 | } 344 | 345 | .markdown-body pre { 346 | margin-top: 0; 347 | margin-bottom: 0; 348 | font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; 349 | font-size: 12px; 350 | word-wrap: normal; 351 | } 352 | 353 | .markdown-body .octicon { 354 | display: inline-block; 355 | overflow: visible !important; 356 | vertical-align: text-bottom; 357 | fill: currentColor; 358 | } 359 | 360 | .markdown-body input::-webkit-outer-spin-button, 361 | .markdown-body input::-webkit-inner-spin-button { 362 | margin: 0; 363 | -webkit-appearance: none; 364 | appearance: none; 365 | } 366 | 367 | .markdown-body::before { 368 | display: table; 369 | content: ""; 370 | } 371 | 372 | .markdown-body::after { 373 | display: table; 374 | clear: both; 375 | content: ""; 376 | } 377 | 378 | .markdown-body>*:first-child { 379 | margin-top: 0 !important; 380 | } 381 | 382 | .markdown-body>*:last-child { 383 | margin-bottom: 0 !important; 384 | } 385 | 386 | .markdown-body a:not([href]) { 387 | color: inherit; 388 | text-decoration: none; 389 | } 390 | 391 | .markdown-body .absent { 392 | color: #cf222e; 393 | } 394 | 395 | .markdown-body .anchor { 396 | float: left; 397 | padding-right: 4px; 398 | margin-left: -20px; 399 | line-height: 1; 400 | } 401 | 402 | .markdown-body .anchor:focus { 403 | outline: none; 404 | } 405 | 406 | .markdown-body p, 407 | .markdown-body blockquote, 408 | .markdown-body ul, 409 | .markdown-body ol, 410 | .markdown-body dl, 411 | .markdown-body table, 412 | .markdown-body pre, 413 | .markdown-body details { 414 | margin-top: 0; 415 | margin-bottom: 16px; 416 | } 417 | 418 | .markdown-body blockquote>:first-child { 419 | margin-top: 0; 420 | } 421 | 422 | .markdown-body blockquote>:last-child { 423 | margin-bottom: 0; 424 | } 425 | 426 | .markdown-body h1 .octicon-link, 427 | .markdown-body h2 .octicon-link, 428 | .markdown-body h3 .octicon-link, 429 | .markdown-body h4 .octicon-link, 430 | .markdown-body h5 .octicon-link, 431 | .markdown-body h6 .octicon-link { 432 | color: #24292f; 433 | vertical-align: middle; 434 | visibility: hidden; 435 | } 436 | 437 | .markdown-body h1:hover .anchor, 438 | .markdown-body h2:hover .anchor, 439 | .markdown-body h3:hover .anchor, 440 | .markdown-body h4:hover .anchor, 441 | .markdown-body h5:hover .anchor, 442 | .markdown-body h6:hover .anchor { 443 | text-decoration: none; 444 | } 445 | 446 | .markdown-body h1:hover .anchor .octicon-link, 447 | .markdown-body h2:hover .anchor .octicon-link, 448 | .markdown-body h3:hover .anchor .octicon-link, 449 | .markdown-body h4:hover .anchor .octicon-link, 450 | .markdown-body h5:hover .anchor .octicon-link, 451 | .markdown-body h6:hover .anchor .octicon-link { 452 | visibility: visible; 453 | } 454 | 455 | .markdown-body h1 tt, 456 | .markdown-body h1 code, 457 | .markdown-body h2 tt, 458 | .markdown-body h2 code, 459 | .markdown-body h3 tt, 460 | .markdown-body h3 code, 461 | .markdown-body h4 tt, 462 | .markdown-body h4 code, 463 | .markdown-body h5 tt, 464 | .markdown-body h5 code, 465 | .markdown-body h6 tt, 466 | .markdown-body h6 code { 467 | padding: 0 .2em; 468 | font-size: inherit; 469 | } 470 | 471 | .markdown-body summary h1, 472 | .markdown-body summary h2, 473 | .markdown-body summary h3, 474 | .markdown-body summary h4, 475 | .markdown-body summary h5, 476 | .markdown-body summary h6 { 477 | display: inline-block; 478 | } 479 | 480 | .markdown-body summary h1 .anchor, 481 | .markdown-body summary h2 .anchor, 482 | .markdown-body summary h3 .anchor, 483 | .markdown-body summary h4 .anchor, 484 | .markdown-body summary h5 .anchor, 485 | .markdown-body summary h6 .anchor { 486 | margin-left: -40px; 487 | } 488 | 489 | .markdown-body summary h1, 490 | .markdown-body summary h2 { 491 | padding-bottom: 0; 492 | border-bottom: 0; 493 | } 494 | 495 | .markdown-body ul.no-list, 496 | .markdown-body ol.no-list { 497 | padding: 0; 498 | list-style-type: none; 499 | } 500 | 501 | .markdown-body ol[type=a] { 502 | list-style-type: lower-alpha; 503 | } 504 | 505 | .markdown-body ol[type=A] { 506 | list-style-type: upper-alpha; 507 | } 508 | 509 | .markdown-body ol[type=i] { 510 | list-style-type: lower-roman; 511 | } 512 | 513 | .markdown-body ol[type=I] { 514 | list-style-type: upper-roman; 515 | } 516 | 517 | .markdown-body ol[type="1"] { 518 | list-style-type: decimal; 519 | } 520 | 521 | .markdown-body div>ol:not([type]) { 522 | list-style-type: decimal; 523 | } 524 | 525 | .markdown-body ul ul, 526 | .markdown-body ul ol, 527 | .markdown-body ol ol, 528 | .markdown-body ol ul { 529 | margin-top: 0; 530 | margin-bottom: 0; 531 | } 532 | 533 | .markdown-body li>p { 534 | margin-top: 16px; 535 | } 536 | 537 | .markdown-body li+li { 538 | margin-top: .25em; 539 | } 540 | 541 | .markdown-body dl { 542 | padding: 0; 543 | } 544 | 545 | .markdown-body dl dt { 546 | padding: 0; 547 | margin-top: 16px; 548 | font-size: 1em; 549 | font-style: italic; 550 | font-weight: 600; 551 | } 552 | 553 | .markdown-body dl dd { 554 | padding: 0 16px; 555 | margin-bottom: 16px; 556 | } 557 | 558 | .markdown-body table th { 559 | font-weight: 600; 560 | } 561 | 562 | .markdown-body table th, 563 | .markdown-body table td { 564 | padding: 6px 13px; 565 | border: 1px solid #d0d7de; 566 | } 567 | 568 | .markdown-body table tr { 569 | background-color: #ffffff; 570 | border-top: 1px solid hsla(210,18%,87%,1); 571 | } 572 | 573 | .markdown-body table tr:nth-child(2n) { 574 | background-color: #f6f8fa; 575 | } 576 | 577 | .markdown-body table img { 578 | background-color: transparent; 579 | } 580 | 581 | .markdown-body img[align=right] { 582 | padding-left: 20px; 583 | } 584 | 585 | .markdown-body img[align=left] { 586 | padding-right: 20px; 587 | } 588 | 589 | .markdown-body .emoji { 590 | max-width: none; 591 | vertical-align: text-top; 592 | background-color: transparent; 593 | } 594 | 595 | .markdown-body span.frame { 596 | display: block; 597 | overflow: hidden; 598 | } 599 | 600 | .markdown-body span.frame>span { 601 | display: block; 602 | float: left; 603 | width: auto; 604 | padding: 7px; 605 | margin: 13px 0 0; 606 | overflow: hidden; 607 | border: 1px solid #d0d7de; 608 | } 609 | 610 | .markdown-body span.frame span img { 611 | display: block; 612 | float: left; 613 | } 614 | 615 | .markdown-body span.frame span span { 616 | display: block; 617 | padding: 5px 0 0; 618 | clear: both; 619 | color: #24292f; 620 | } 621 | 622 | .markdown-body span.align-center { 623 | display: block; 624 | overflow: hidden; 625 | clear: both; 626 | } 627 | 628 | .markdown-body span.align-center>span { 629 | display: block; 630 | margin: 13px auto 0; 631 | overflow: hidden; 632 | text-align: center; 633 | } 634 | 635 | .markdown-body span.align-center span img { 636 | margin: 0 auto; 637 | text-align: center; 638 | } 639 | 640 | .markdown-body span.align-right { 641 | display: block; 642 | overflow: hidden; 643 | clear: both; 644 | } 645 | 646 | .markdown-body span.align-right>span { 647 | display: block; 648 | margin: 13px 0 0; 649 | overflow: hidden; 650 | text-align: right; 651 | } 652 | 653 | .markdown-body span.align-right span img { 654 | margin: 0; 655 | text-align: right; 656 | } 657 | 658 | .markdown-body span.float-left { 659 | display: block; 660 | float: left; 661 | margin-right: 13px; 662 | overflow: hidden; 663 | } 664 | 665 | .markdown-body span.float-left span { 666 | margin: 13px 0 0; 667 | } 668 | 669 | .markdown-body span.float-right { 670 | display: block; 671 | float: right; 672 | margin-left: 13px; 673 | overflow: hidden; 674 | } 675 | 676 | .markdown-body span.float-right>span { 677 | display: block; 678 | margin: 13px auto 0; 679 | overflow: hidden; 680 | text-align: right; 681 | } 682 | 683 | .markdown-body code, 684 | .markdown-body tt { 685 | padding: .2em .4em; 686 | margin: 0; 687 | font-size: 85%; 688 | white-space: break-spaces; 689 | background-color: rgba(175,184,193,0.2); 690 | border-radius: 6px; 691 | } 692 | 693 | .markdown-body code br, 694 | .markdown-body tt br { 695 | display: none; 696 | } 697 | 698 | .markdown-body del code { 699 | text-decoration: inherit; 700 | } 701 | 702 | .markdown-body samp { 703 | font-size: 85%; 704 | } 705 | 706 | .markdown-body pre code { 707 | font-size: 100%; 708 | } 709 | 710 | .markdown-body pre>code { 711 | padding: 0; 712 | margin: 0; 713 | word-break: normal; 714 | white-space: pre; 715 | background: transparent; 716 | border: 0; 717 | } 718 | 719 | .markdown-body .highlight { 720 | margin-bottom: 16px; 721 | } 722 | 723 | .markdown-body .highlight pre { 724 | margin-bottom: 0; 725 | word-break: normal; 726 | } 727 | 728 | .markdown-body .highlight pre, 729 | .markdown-body pre { 730 | padding: 16px; 731 | overflow: auto; 732 | font-size: 85%; 733 | line-height: 1.45; 734 | background-color: #f6f8fa; 735 | border-radius: 6px; 736 | } 737 | 738 | .markdown-body pre code, 739 | .markdown-body pre tt { 740 | display: inline; 741 | max-width: auto; 742 | padding: 0; 743 | margin: 0; 744 | overflow: visible; 745 | line-height: inherit; 746 | word-wrap: normal; 747 | background-color: transparent; 748 | border: 0; 749 | } 750 | 751 | .markdown-body .csv-data td, 752 | .markdown-body .csv-data th { 753 | padding: 5px; 754 | overflow: hidden; 755 | font-size: 12px; 756 | line-height: 1; 757 | text-align: left; 758 | white-space: nowrap; 759 | } 760 | 761 | .markdown-body .csv-data .blob-num { 762 | padding: 10px 8px 9px; 763 | text-align: right; 764 | background: #ffffff; 765 | border: 0; 766 | } 767 | 768 | .markdown-body .csv-data tr { 769 | border-top: 0; 770 | } 771 | 772 | .markdown-body .csv-data th { 773 | font-weight: 600; 774 | background: #f6f8fa; 775 | border-top: 0; 776 | } 777 | 778 | .markdown-body [data-footnote-ref]::before { 779 | content: "["; 780 | } 781 | 782 | .markdown-body [data-footnote-ref]::after { 783 | content: "]"; 784 | } 785 | 786 | .markdown-body .footnotes { 787 | font-size: 12px; 788 | color: #57606a; 789 | border-top: 1px solid #d0d7de; 790 | } 791 | 792 | .markdown-body .footnotes ol { 793 | padding-left: 16px; 794 | } 795 | 796 | .markdown-body .footnotes ol ul { 797 | display: inline-block; 798 | padding-left: 16px; 799 | margin-top: 16px; 800 | } 801 | 802 | .markdown-body .footnotes li { 803 | position: relative; 804 | } 805 | 806 | .markdown-body .footnotes li:target::before { 807 | position: absolute; 808 | top: -8px; 809 | right: -8px; 810 | bottom: -8px; 811 | left: -24px; 812 | pointer-events: none; 813 | content: ""; 814 | border: 2px solid #0969da; 815 | border-radius: 6px; 816 | } 817 | 818 | .markdown-body .footnotes li:target { 819 | color: #24292f; 820 | } 821 | 822 | .markdown-body .footnotes .data-footnote-backref g-emoji { 823 | font-family: monospace; 824 | } 825 | 826 | .markdown-body .pl-c { 827 | color: #6e7781; 828 | } 829 | 830 | .markdown-body .pl-c1, 831 | .markdown-body .pl-s .pl-v { 832 | color: #0550ae; 833 | } 834 | 835 | .markdown-body .pl-e, 836 | .markdown-body .pl-en { 837 | color: #8250df; 838 | } 839 | 840 | .markdown-body .pl-smi, 841 | .markdown-body .pl-s .pl-s1 { 842 | color: #24292f; 843 | } 844 | 845 | .markdown-body .pl-ent { 846 | color: #116329; 847 | } 848 | 849 | .markdown-body .pl-k { 850 | color: #cf222e; 851 | } 852 | 853 | .markdown-body .pl-s, 854 | .markdown-body .pl-pds, 855 | .markdown-body .pl-s .pl-pse .pl-s1, 856 | .markdown-body .pl-sr, 857 | .markdown-body .pl-sr .pl-cce, 858 | .markdown-body .pl-sr .pl-sre, 859 | .markdown-body .pl-sr .pl-sra { 860 | color: #0a3069; 861 | } 862 | 863 | .markdown-body .pl-v, 864 | .markdown-body .pl-smw { 865 | color: #953800; 866 | } 867 | 868 | .markdown-body .pl-bu { 869 | color: #82071e; 870 | } 871 | 872 | .markdown-body .pl-ii { 873 | color: #f6f8fa; 874 | background-color: #82071e; 875 | } 876 | 877 | .markdown-body .pl-c2 { 878 | color: #f6f8fa; 879 | background-color: #cf222e; 880 | } 881 | 882 | .markdown-body .pl-sr .pl-cce { 883 | font-weight: bold; 884 | color: #116329; 885 | } 886 | 887 | .markdown-body .pl-ml { 888 | color: #3b2300; 889 | } 890 | 891 | .markdown-body .pl-mh, 892 | .markdown-body .pl-mh .pl-en, 893 | .markdown-body .pl-ms { 894 | font-weight: bold; 895 | color: #0550ae; 896 | } 897 | 898 | .markdown-body .pl-mi { 899 | font-style: italic; 900 | color: #24292f; 901 | } 902 | 903 | .markdown-body .pl-mb { 904 | font-weight: bold; 905 | color: #24292f; 906 | } 907 | 908 | .markdown-body .pl-md { 909 | color: #82071e; 910 | background-color: #ffebe9; 911 | } 912 | 913 | .markdown-body .pl-mi1 { 914 | color: #116329; 915 | background-color: #dafbe1; 916 | } 917 | 918 | .markdown-body .pl-mc { 919 | color: #953800; 920 | background-color: #ffd8b5; 921 | } 922 | 923 | .markdown-body .pl-mi2 { 924 | color: #eaeef2; 925 | background-color: #0550ae; 926 | } 927 | 928 | .markdown-body .pl-mdr { 929 | font-weight: bold; 930 | color: #8250df; 931 | } 932 | 933 | .markdown-body .pl-ba { 934 | color: #57606a; 935 | } 936 | 937 | .markdown-body .pl-sg { 938 | color: #8c959f; 939 | } 940 | 941 | .markdown-body .pl-corl { 942 | text-decoration: underline; 943 | color: #0a3069; 944 | } 945 | 946 | .markdown-body g-emoji { 947 | display: inline-block; 948 | min-width: 1ch; 949 | font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 950 | font-size: 1em; 951 | font-style: normal !important; 952 | font-weight: 400; 953 | line-height: 1; 954 | vertical-align: -0.075em; 955 | } 956 | 957 | .markdown-body g-emoji img { 958 | width: 1em; 959 | height: 1em; 960 | } 961 | 962 | .markdown-body .task-list-item { 963 | list-style-type: none; 964 | } 965 | 966 | .markdown-body .task-list-item label { 967 | font-weight: 400; 968 | } 969 | 970 | .markdown-body .task-list-item.enabled label { 971 | cursor: pointer; 972 | } 973 | 974 | .markdown-body .task-list-item+.task-list-item { 975 | margin-top: 4px; 976 | } 977 | 978 | .markdown-body .task-list-item .handle { 979 | display: none; 980 | } 981 | 982 | .markdown-body .task-list-item-checkbox { 983 | margin: 0 .2em .25em -1.4em; 984 | vertical-align: middle; 985 | } 986 | 987 | .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { 988 | margin: 0 -1.6em .25em .2em; 989 | } 990 | 991 | .markdown-body .contains-task-list { 992 | position: relative; 993 | } 994 | 995 | .markdown-body .contains-task-list:hover .task-list-item-convert-container, 996 | .markdown-body .contains-task-list:focus-within .task-list-item-convert-container { 997 | display: block; 998 | width: auto; 999 | height: 24px; 1000 | overflow: visible; 1001 | clip: auto; 1002 | } 1003 | 1004 | .markdown-body ::-webkit-calendar-picker-indicator { 1005 | filter: invert(50%); 1006 | } 1007 | -------------------------------------------------------------------------------- /htdoc/assets/polyfills-legacy-DglrV6q4.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var r,t,e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n={};function i(){if(t)return r;t=1;var n=function(r){return r&&r.Math===Math&&r};return r=n("object"==typeof globalThis&&globalThis)||n("object"==typeof window&&window)||n("object"==typeof self&&self)||n("object"==typeof e&&e)||n("object"==typeof r&&r)||function(){return this}()||Function("return this")()}var o,u,a,c,f,s,l,h,v={};function p(){return u?o:(u=1,o=function(r){try{return!!r()}catch(t){return!0}})}function d(){if(c)return a;c=1;var r=p();return a=!r(function(){return 7!==Object.defineProperty({},1,{get:function(){return 7}})[1]})}function y(){if(s)return f;s=1;var r=p();return f=!r(function(){var r=function(){}.bind();return"function"!=typeof r||r.hasOwnProperty("prototype")})}function g(){if(h)return l;h=1;var r=y(),t=Function.prototype.call;return l=r?t.bind(t):function(){return t.apply(t,arguments)},l}var w,m,b,E,S,O,A,R,I,T,x,k,D,j,P,C,_,M,U,B,L,N,F,z,W,H,V,Y,G,q,$,J,Q,X,K,Z,rr,tr,er,nr,ir,or={};function ur(){return b?m:(b=1,m=function(r,t){return{enumerable:!(1&r),configurable:!(2&r),writable:!(4&r),value:t}})}function ar(){if(S)return E;S=1;var r=y(),t=Function.prototype,e=t.call,n=r&&t.bind.bind(e,e);return E=r?n:function(r){return function(){return e.apply(r,arguments)}},E}function cr(){if(A)return O;A=1;var r=ar(),t=r({}.toString),e=r("".slice);return O=function(r){return e(t(r),8,-1)}}function fr(){if(I)return R;I=1;var r=ar(),t=p(),e=cr(),n=Object,i=r("".split);return R=t(function(){return!n("z").propertyIsEnumerable(0)})?function(r){return"String"===e(r)?i(r,""):n(r)}:n}function sr(){return x?T:(x=1,T=function(r){return null==r})}function lr(){if(D)return k;D=1;var r=sr(),t=TypeError;return k=function(e){if(r(e))throw new t("Can't call method on "+e);return e}}function hr(){if(P)return j;P=1;var r=fr(),t=lr();return j=function(e){return r(t(e))}}function vr(){if(_)return C;_=1;var r="object"==typeof document&&document.all;return C=void 0===r&&void 0!==r?function(t){return"function"==typeof t||t===r}:function(r){return"function"==typeof r}}function pr(){if(U)return M;U=1;var r=vr();return M=function(t){return"object"==typeof t?null!==t:r(t)}}function dr(){if(L)return B;L=1;var r=i(),t=vr();return B=function(e,n){return arguments.length<2?(i=r[e],t(i)?i:void 0):r[e]&&r[e][n];var i},B}function yr(){if(F)return N;F=1;var r=ar();return N=r({}.isPrototypeOf)}function gr(){if(W)return z;W=1;var r=i().navigator,t=r&&r.userAgent;return z=t?String(t):""}function wr(){if(V)return H;V=1;var r,t,e=i(),n=gr(),o=e.process,u=e.Deno,a=o&&o.versions||u&&u.version,c=a&&a.v8;return c&&(t=(r=c.split("."))[0]>0&&r[0]<4?1:+(r[0]+r[1])),!t&&n&&(!(r=n.match(/Edge\/(\d+)/))||r[1]>=74)&&(r=n.match(/Chrome\/(\d+)/))&&(t=+r[1]),H=t}function mr(){if(G)return Y;G=1;var r=wr(),t=p(),e=i().String;return Y=!!Object.getOwnPropertySymbols&&!t(function(){var t=Symbol("symbol detection");return!e(t)||!(Object(t)instanceof Symbol)||!Symbol.sham&&r&&r<41})}function br(){if($)return q;$=1;var r=mr();return q=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator}function Er(){if(Q)return J;Q=1;var r=dr(),t=vr(),e=yr(),n=br(),i=Object;return J=n?function(r){return"symbol"==typeof r}:function(n){var o=r("Symbol");return t(o)&&e(o.prototype,i(n))}}function Sr(){if(K)return X;K=1;var r=String;return X=function(t){try{return r(t)}catch(e){return"Object"}}}function Or(){if(rr)return Z;rr=1;var r=vr(),t=Sr(),e=TypeError;return Z=function(n){if(r(n))return n;throw new e(t(n)+" is not a function")}}function Ar(){if(er)return tr;er=1;var r=Or(),t=sr();return tr=function(e,n){var i=e[n];return t(i)?void 0:r(i)}}function Rr(){if(ir)return nr;ir=1;var r=g(),t=vr(),e=pr(),n=TypeError;return nr=function(i,o){var u,a;if("string"===o&&t(u=i.toString)&&!e(a=r(u,i)))return a;if(t(u=i.valueOf)&&!e(a=r(u,i)))return a;if("string"!==o&&t(u=i.toString)&&!e(a=r(u,i)))return a;throw new n("Can't convert object to primitive value")}}var Ir,Tr,xr,kr,Dr,jr,Pr,Cr,_r,Mr,Ur,Br,Lr,Nr,Fr,zr,Wr,Hr,Vr,Yr,Gr,qr,$r,Jr,Qr={exports:{}};function Xr(){return Tr?Ir:(Tr=1,Ir=!1)}function Kr(){if(kr)return xr;kr=1;var r=i(),t=Object.defineProperty;return xr=function(e,n){try{t(r,e,{value:n,configurable:!0,writable:!0})}catch(i){r[e]=n}return n}}function Zr(){if(Dr)return Qr.exports;Dr=1;var r=Xr(),t=i(),e=Kr(),n="__core-js_shared__",o=Qr.exports=t[n]||e(n,{});return(o.versions||(o.versions=[])).push({version:"3.45.1",mode:r?"pure":"global",copyright:"© 2014-2025 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.45.1/LICENSE",source:"https://github.com/zloirock/core-js"}),Qr.exports}function rt(){if(Pr)return jr;Pr=1;var r=Zr();return jr=function(t,e){return r[t]||(r[t]=e||{})}}function tt(){if(_r)return Cr;_r=1;var r=lr(),t=Object;return Cr=function(e){return t(r(e))}}function et(){if(Ur)return Mr;Ur=1;var r=ar(),t=tt(),e=r({}.hasOwnProperty);return Mr=Object.hasOwn||function(r,n){return e(t(r),n)}}function nt(){if(Lr)return Br;Lr=1;var r=ar(),t=0,e=Math.random(),n=r(1.1.toString);return Br=function(r){return"Symbol("+(void 0===r?"":r)+")_"+n(++t+e,36)}}function it(){if(Fr)return Nr;Fr=1;var r=i(),t=rt(),e=et(),n=nt(),o=mr(),u=br(),a=r.Symbol,c=t("wks"),f=u?a.for||a:a&&a.withoutSetter||n;return Nr=function(r){return e(c,r)||(c[r]=o&&e(a,r)?a[r]:f("Symbol."+r)),c[r]}}function ot(){if(Wr)return zr;Wr=1;var r=g(),t=pr(),e=Er(),n=Ar(),i=Rr(),o=it(),u=TypeError,a=o("toPrimitive");return zr=function(o,c){if(!t(o)||e(o))return o;var f,s=n(o,a);if(s){if(void 0===c&&(c="default"),f=r(s,o,c),!t(f)||e(f))return f;throw new u("Can't convert object to primitive value")}return void 0===c&&(c="number"),i(o,c)}}function ut(){if(Vr)return Hr;Vr=1;var r=ot(),t=Er();return Hr=function(e){var n=r(e,"string");return t(n)?n:n+""}}function at(){if(Gr)return Yr;Gr=1;var r=i(),t=pr(),e=r.document,n=t(e)&&t(e.createElement);return Yr=function(r){return n?e.createElement(r):{}}}function ct(){if($r)return qr;$r=1;var r=d(),t=p(),e=at();return qr=!r&&!t(function(){return 7!==Object.defineProperty(e("div"),"a",{get:function(){return 7}}).a})}function ft(){if(Jr)return v;Jr=1;var r=d(),t=g(),e=function(){if(w)return or;w=1;var r={}.propertyIsEnumerable,t=Object.getOwnPropertyDescriptor,e=t&&!r.call({1:2},1);return or.f=e?function(r){var e=t(this,r);return!!e&&e.enumerable}:r,or}(),n=ur(),i=hr(),o=ut(),u=et(),a=ct(),c=Object.getOwnPropertyDescriptor;return v.f=r?c:function(r,f){if(r=i(r),f=o(f),a)try{return c(r,f)}catch(s){}if(u(r,f))return n(!t(e.f,r,f),r[f])},v}var st,lt,ht,vt,pt,dt,yt,gt={};function wt(){if(lt)return st;lt=1;var r=d(),t=p();return st=r&&t(function(){return 42!==Object.defineProperty(function(){},"prototype",{value:42,writable:!1}).prototype})}function mt(){if(vt)return ht;vt=1;var r=pr(),t=String,e=TypeError;return ht=function(n){if(r(n))return n;throw new e(t(n)+" is not an object")}}function bt(){if(pt)return gt;pt=1;var r=d(),t=ct(),e=wt(),n=mt(),i=ut(),o=TypeError,u=Object.defineProperty,a=Object.getOwnPropertyDescriptor,c="enumerable",f="configurable",s="writable";return gt.f=r?e?function(r,t,e){if(n(r),t=i(t),n(e),"function"==typeof r&&"prototype"===t&&"value"in e&&s in e&&!e[s]){var o=a(r,t);o&&o[s]&&(r[t]=e.value,e={configurable:f in e?e[f]:o[f],enumerable:c in e?e[c]:o[c],writable:!1})}return u(r,t,e)}:u:function(r,e,a){if(n(r),e=i(e),n(a),t)try{return u(r,e,a)}catch(c){}if("get"in a||"set"in a)throw new o("Accessors not supported");return"value"in a&&(r[e]=a.value),r},gt}function Et(){if(yt)return dt;yt=1;var r=d(),t=bt(),e=ur();return dt=r?function(r,n,i){return t.f(r,n,e(1,i))}:function(r,t,e){return r[t]=e,r}}var St,Ot,At,Rt,It,Tt,xt,kt,Dt,jt,Pt,Ct,_t,Mt,Ut,Bt={exports:{}};function Lt(){if(Rt)return At;Rt=1;var r=ar(),t=vr(),e=Zr(),n=r(Function.toString);return t(e.inspectSource)||(e.inspectSource=function(r){return n(r)}),At=e.inspectSource}function Nt(){if(kt)return xt;kt=1;var r=rt(),t=nt(),e=r("keys");return xt=function(r){return e[r]||(e[r]=t(r))}}function Ft(){return jt?Dt:(jt=1,Dt={})}function zt(){if(Ct)return Pt;Ct=1;var r,t,e,n=function(){if(Tt)return It;Tt=1;var r=i(),t=vr(),e=r.WeakMap;return It=t(e)&&/native code/.test(String(e))}(),o=i(),u=pr(),a=Et(),c=et(),f=Zr(),s=Nt(),l=Ft(),h="Object already initialized",v=o.TypeError,p=o.WeakMap;if(n||f.state){var d=f.state||(f.state=new p);d.get=d.get,d.has=d.has,d.set=d.set,r=function(r,t){if(d.has(r))throw new v(h);return t.facade=r,d.set(r,t),t},t=function(r){return d.get(r)||{}},e=function(r){return d.has(r)}}else{var y=s("state");l[y]=!0,r=function(r,t){if(c(r,y))throw new v(h);return t.facade=r,a(r,y,t),t},t=function(r){return c(r,y)?r[y]:{}},e=function(r){return c(r,y)}}return Pt={set:r,get:t,has:e,enforce:function(n){return e(n)?t(n):r(n,{})},getterFor:function(r){return function(e){var n;if(!u(e)||(n=t(e)).type!==r)throw new v("Incompatible receiver, "+r+" required");return n}}}}function Wt(){if(_t)return Bt.exports;_t=1;var r=ar(),t=p(),e=vr(),n=et(),i=d(),o=function(){if(Ot)return St;Ot=1;var r=d(),t=et(),e=Function.prototype,n=r&&Object.getOwnPropertyDescriptor,i=t(e,"name"),o=i&&"something"===function(){}.name,u=i&&(!r||r&&n(e,"name").configurable);return St={EXISTS:i,PROPER:o,CONFIGURABLE:u}}().CONFIGURABLE,u=Lt(),a=zt(),c=a.enforce,f=a.get,s=String,l=Object.defineProperty,h=r("".slice),v=r("".replace),y=r([].join),g=i&&!t(function(){return 8!==l(function(){},"length",{value:8}).length}),w=String(String).split("String"),m=Bt.exports=function(r,t,e){"Symbol("===h(s(t),0,7)&&(t="["+v(s(t),/^Symbol\(([^)]*)\).*$/,"$1")+"]"),e&&e.getter&&(t="get "+t),e&&e.setter&&(t="set "+t),(!n(r,"name")||o&&r.name!==t)&&(i?l(r,"name",{value:t,configurable:!0}):r.name=t),g&&e&&n(e,"arity")&&r.length!==e.arity&&l(r,"length",{value:e.arity});try{e&&n(e,"constructor")&&e.constructor?i&&l(r,"prototype",{writable:!1}):r.prototype&&(r.prototype=void 0)}catch(a){}var u=c(r);return n(u,"source")||(u.source=y(w,"string"==typeof t?t:"")),r};return Function.prototype.toString=m(function(){return e(this)&&f(this).source||u(this)},"toString"),Bt.exports}function Ht(){if(Ut)return Mt;Ut=1;var r=vr(),t=bt(),e=Wt(),n=Kr();return Mt=function(i,o,u,a){a||(a={});var c=a.enumerable,f=void 0!==a.name?a.name:o;if(r(u)&&e(u,f,a),a.global)c?i[o]=u:n(o,u);else{try{a.unsafe?i[o]&&(c=!0):delete i[o]}catch(s){}c?i[o]=u:t.f(i,o,{value:u,enumerable:!1,configurable:!a.nonConfigurable,writable:!a.nonWritable})}return i}}var Vt,Yt,Gt,qt,$t,Jt,Qt,Xt,Kt,Zt,re,te,ee,ne,ie,oe,ue,ae={};function ce(){if(qt)return Gt;qt=1;var r=function(){if(Yt)return Vt;Yt=1;var r=Math.ceil,t=Math.floor;return Vt=Math.trunc||function(e){var n=+e;return(n>0?t:r)(n)}}();return Gt=function(t){var e=+t;return e!=e||0===e?0:r(e)}}function fe(){if(Jt)return $t;Jt=1;var r=ce(),t=Math.max,e=Math.min;return $t=function(n,i){var o=r(n);return o<0?t(o+i,0):e(o,i)}}function se(){if(Xt)return Qt;Xt=1;var r=ce(),t=Math.min;return Qt=function(e){var n=r(e);return n>0?t(n,9007199254740991):0}}function le(){if(Zt)return Kt;Zt=1;var r=se();return Kt=function(t){return r(t.length)}}function he(){if(ne)return ee;ne=1;var r=ar(),t=et(),e=hr(),n=function(){if(te)return re;te=1;var r=hr(),t=fe(),e=le(),n=function(n){return function(i,o,u){var a=r(i),c=e(a);if(0===c)return!n&&-1;var f,s=t(u,c);if(n&&o!=o){for(;c>s;)if((f=a[s++])!=f)return!0}else for(;c>s;s++)if((n||s in a)&&a[s]===o)return n||s||0;return!n&&-1}};return re={includes:n(!0),indexOf:n(!1)}}().indexOf,i=Ft(),o=r([].push);return ee=function(r,u){var a,c=e(r),f=0,s=[];for(a in c)!t(i,a)&&t(c,a)&&o(s,a);for(;u.length>f;)t(c,a=u[f++])&&(~n(s,a)||o(s,a));return s}}function ve(){return oe?ie:(oe=1,ie=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"])}var pe,de,ye,ge,we,me,be,Ee,Se,Oe,Ae,Re,Ie,Te,xe,ke,De={};function je(){if(ye)return de;ye=1;var r=dr(),t=ar(),e=function(){if(ue)return ae;ue=1;var r=he(),t=ve().concat("length","prototype");return ae.f=Object.getOwnPropertyNames||function(e){return r(e,t)},ae}(),n=(pe||(pe=1,De.f=Object.getOwnPropertySymbols),De),i=mt(),o=t([].concat);return de=r("Reflect","ownKeys")||function(r){var t=e.f(i(r)),u=n.f;return u?o(t,u(r)):t}}function Pe(){if(we)return ge;we=1;var r=et(),t=je(),e=ft(),n=bt();return ge=function(i,o,u){for(var a=t(o),c=n.f,f=e.f,s=0;s9007199254740991)throw r("Maximum allowed index exceeded");return t}}!function(){if(ke)return n;ke=1;var r=Ce(),t=tt(),e=le(),i=function(){if(Ie)return Re;Ie=1;var r=d(),t=_e(),e=TypeError,n=Object.getOwnPropertyDescriptor,i=r&&!function(){if(void 0!==this)return!0;try{Object.defineProperty([],"length",{writable:!1}).length=1}catch(r){return r instanceof TypeError}}();return Re=i?function(r,i){if(t(r)&&!n(r,"length").writable)throw new e("Cannot set read only .length");return r.length=i}:function(r,t){return r.length=t}}(),o=Me();r({target:"Array",proto:!0,arity:1,forced:p()(function(){return 4294967297!==[].push.call({length:4294967296},1)})||!function(){try{Object.defineProperty([],"length",{writable:!1}).push()}catch(r){return r instanceof TypeError}}()},{push:function(r){var n=t(this),u=e(n),a=arguments.length;o(u+a);for(var c=0;c92||"NODE"===n&&e>94||"BROWSER"===n&&e>97)return!1;var r=new ArrayBuffer(8),t=o(r,{transfer:[r]});return 0!==r.byteLength||8!==t.byteLength})}function An(){if(vn)return hn;vn=1;var r,t,e,n,o=i(),u=Sn(),a=On(),c=o.structuredClone,f=o.ArrayBuffer,s=o.MessageChannel,l=!1;if(a)l=function(r){c(r,{transfer:[r]})};else if(f)try{s||(r=u("worker_threads"))&&(s=r.MessageChannel),s&&(t=new s,e=new f(2),n=function(r){t.port1.postMessage(null,[r])},2===e.byteLength&&(n(e),0===e.byteLength&&(l=n)))}catch(h){}return hn=l}function Rn(){if(dn)return pn;dn=1;var r=i(),t=ar(),e=Qe(),n=wn(),o=mn(),u=Xe(),a=An(),c=On(),f=r.structuredClone,s=r.ArrayBuffer,l=r.DataView,h=Math.min,v=s.prototype,p=l.prototype,d=t(v.slice),y=e(v,"resizable","get"),g=e(v,"maxByteLength","get"),w=t(p.getInt8),m=t(p.setInt8);return pn=(c||a)&&function(r,t,e){var i,v=u(r),p=void 0===t?v:n(t),b=!y||!y(r);if(o(r),c&&(r=f(r,{transfer:[r]}),v===p&&(e||b)))return r;if(v>=p&&(!e||b))i=d(r,0,p);else{var E=e&&!b&&g?{maxByteLength:g(r)}:void 0;i=new s(p,E);for(var S=new l(r),O=new l(i),A=h(p,v),R=0;Rs;)e.f(r,u=c[s++],a[u]);return r},Qn}(),n=ve(),i=Ft(),o=Kn(),u=at(),a=Nt(),c="prototype",f="script",s=a("IE_PROTO"),l=function(){},h=function(r){return"<"+f+">"+r+""},v=function(r){r.write(h("")),r.close();var t=r.parentWindow.Object;return r=null,t},p=function(){try{r=new ActiveXObject("htmlfile")}catch(s){}var t,e,i;p="undefined"!=typeof document?document.domain&&r?v(r):(e=u("iframe"),i="java"+f+":",e.style.display="none",o.appendChild(e),e.src=String(i),(t=e.contentWindow.document).open(),t.write(h("document.F=Object")),t.close(),t.F):v(r);for(var a=n.length;a--;)delete p[c][n[a]];return p()};return i[s]=!0,Yn=Object.create||function(r,n){var i;return null!==r?(l[c]=t(r),i=new l,l[c]=null,i[s]=r):i=p(),void 0===n?i:e.f(i,n)}}function ri(){if($n)return qn;$n=1;var r,t,e,n=p(),i=vr(),o=pr(),u=Zn(),a=Ln(),c=Ht(),f=it(),s=Xr(),l=f("iterator"),h=!1;return[].keys&&("next"in(e=[].keys())?(t=a(a(e)))!==Object.prototype&&(r=t):h=!0),!o(r)||n(function(){var t={};return r[l].call(t)!==t})?r={}:s&&(r=u(r)),i(r[l])||c(r,l,function(){return this}),qn={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:h}}!function(){if(Jn)return Un;Jn=1;var r=Ce(),t=i(),e=Bn(),n=mt(),o=vr(),u=Ln(),a=$e(),c=Nn(),f=p(),s=et(),l=it(),h=ri().IteratorPrototype,v=d(),y=Xr(),g="constructor",w="Iterator",m=l("toStringTag"),b=TypeError,E=t[w],S=y||!o(E)||E.prototype!==h||!f(function(){E({})}),O=function(){if(e(this,h),u(this)===h)throw new b("Abstract class Iterator not directly constructable")},A=function(r,t){v?a(h,r,{configurable:!0,get:function(){return t},set:function(t){if(n(this),this===h)throw new b("You can't redefine this property");s(this,r)?this[r]=t:c(this,r,t)}}):h[r]=t};s(h,m)||A(m,w),!S&&s(h,g)&&h[g]!==Object||A(g,O),O.prototype=h,r({global:!0,constructor:!0,forced:S},{Iterator:O})}();var ti,ei,ni,ii,oi,ui,ai,ci,fi,si,li,hi,vi,pi,di,yi,gi,wi,mi,bi,Ei,Si,Oi,Ai,Ri,Ii={};function Ti(){if(ii)return ni;ii=1;var r=function(){if(ei)return ti;ei=1;var r=cr(),t=ar();return ti=function(e){if("Function"===r(e))return t(e)}}(),t=Or(),e=y(),n=r(r.bind);return ni=function(r,i){return t(r),void 0===i?r:e?n(r,i):function(){return r.apply(i,arguments)}},ni}function xi(){return ui?oi:(ui=1,oi={})}function ki(){if(ci)return ai;ci=1;var r=it(),t=xi(),e=r("iterator"),n=Array.prototype;return ai=function(r){return void 0!==r&&(t.Array===r||n[e]===r)}}function Di(){if(hi)return li;hi=1;var r=function(){if(si)return fi;si=1;var r={};return r[it()("toStringTag")]="z",fi="[object z]"===String(r)}(),t=vr(),e=cr(),n=it()("toStringTag"),i=Object,o="Arguments"===e(function(){return arguments}());return li=r?e:function(r){var u,a,c;return void 0===r?"Undefined":null===r?"Null":"string"==typeof(a=function(r,t){try{return r[t]}catch(e){}}(u=i(r),n))?a:o?e(u):"Object"===(c=e(u))&&t(u.callee)?"Arguments":c}}function ji(){if(pi)return vi;pi=1;var r=Di(),t=Ar(),e=sr(),n=xi(),i=it()("iterator");return vi=function(o){if(!e(o))return t(o,i)||t(o,"@@iterator")||n[r(o)]}}function Pi(){if(yi)return di;yi=1;var r=g(),t=Or(),e=mt(),n=Sr(),i=ji(),o=TypeError;return di=function(u,a){var c=arguments.length<2?i(u):a;if(t(c))return e(r(c,u));throw new o(n(u)+" is not iterable")},di}function Ci(){if(wi)return gi;wi=1;var r=g(),t=mt(),e=Ar();return gi=function(n,i,o){var u,a;t(n);try{if(!(u=e(n,"return"))){if("throw"===i)throw o;return o}u=r(u,n)}catch(c){a=!0,u=c}if("throw"===i)throw o;if(a)throw u;return t(u),o}}function _i(){if(bi)return mi;bi=1;var r=Ti(),t=g(),e=mt(),n=Sr(),i=ki(),o=le(),u=yr(),a=Pi(),c=ji(),f=Ci(),s=TypeError,l=function(r,t){this.stopped=r,this.result=t},h=l.prototype;return mi=function(v,p,d){var y,g,w,m,b,E,S,O=d&&d.that,A=!(!d||!d.AS_ENTRIES),R=!(!d||!d.IS_RECORD),I=!(!d||!d.IS_ITERATOR),T=!(!d||!d.INTERRUPTED),x=r(p,O),k=function(r){return y&&f(y,"normal"),new l(!0,r)},D=function(r){return A?(e(r),T?x(r[0],r[1],k):x(r[0],r[1])):T?x(r,k):x(r)};if(R)y=v.iterator;else if(I)y=v;else{if(!(g=c(v)))throw new s(n(v)+" is not iterable");if(i(g)){for(w=0,m=o(v);m>w;w++)if((b=D(v[w]))&&u(h,b))return b;return new l(!1)}y=a(v,g)}for(E=R?v.next:y.next;!(S=t(E,y)).done;){try{b=D(S.value)}catch(j){f(y,"throw",j)}if("object"==typeof b&&b&&u(h,b))return b}return new l(!1)}}function Mi(){return Si?Ei:(Si=1,Ei=function(r){return{iterator:r,next:r.next,done:!1}})}function Ui(){if(Ai)return Oi;Ai=1;var r=i();return Oi=function(t,e){var n=r.Iterator,i=n&&n.prototype,o=i&&i[t],u=!1;if(o)try{o.call({next:function(){return{done:!0}},return:function(){u=!0}},-1)}catch(a){a instanceof e||(u=!1)}if(!u)return o}}!function(){if(Ri)return Ii;Ri=1;var r=Ce(),t=g(),e=_i(),n=Or(),i=mt(),o=Mi(),u=Ci(),a=Ui()("every",TypeError);r({target:"Iterator",proto:!0,real:!0,forced:a},{every:function(r){i(this);try{n(r)}catch(s){u(this,"throw",s)}if(a)return t(a,this,r);var c=o(this),f=0;return!e(c,function(t,e){if(!r(t,f++))return e()},{IS_RECORD:!0,INTERRUPTED:!0}).stopped}})}();var Bi,Li,Ni,Fi,zi,Wi,Hi,Vi,Yi,Gi,qi,$i,Ji,Qi={};function Xi(){if(Li)return Bi;Li=1;var r=Ht();return Bi=function(t,e,n){for(var i in e)r(t,i,e[i],n);return t}}function Ki(){return Fi?Ni:(Fi=1,Ni=function(r,t){return{value:r,done:t}})}function Zi(){if(Wi)return zi;Wi=1;var r=Ci();return zi=function(t,e,n){for(var i=t.length-1;i>=0;i--)if(void 0!==t[i])try{n=r(t[i].iterator,e,n)}catch(o){e="throw",n=o}if("throw"===e)throw n;return n}}function ro(){if(Vi)return Hi;Vi=1;var r=g(),t=Zn(),e=Et(),n=Xi(),i=it(),o=zt(),u=Ar(),a=ri().IteratorPrototype,c=Ki(),f=Ci(),s=Zi(),l=i("toStringTag"),h="IteratorHelper",v="WrapForValidIterator",p="normal",d="throw",y=o.set,w=function(e){var i=o.getterFor(e?v:h);return n(t(a),{next:function(){var r=i(this);if(e)return r.nextHandler();if(r.done)return c(void 0,!0);try{var t=r.nextHandler();return r.returnHandlerResult?t:c(t,r.done)}catch(n){throw r.done=!0,n}},return:function(){var t=i(this),n=t.iterator;if(t.done=!0,e){var o=u(n,"return");return o?r(o,n):c(void 0,!0)}if(t.inner)try{f(t.inner.iterator,p)}catch(a){return f(n,d,a)}if(t.openIters)try{s(t.openIters,p)}catch(a){return f(n,d,a)}return n&&f(n,p),c(void 0,!0)}})},m=w(!0),b=w(!1);return e(b,l,"Iterator Helper"),Hi=function(r,t,e){var n=function(n,i){i?(i.iterator=n.iterator,i.next=n.next):i=n,i.type=t?v:h,i.returnHandlerResult=!!e,i.nextHandler=r,i.counter=0,i.done=!1,y(this,i)};return n.prototype=t?m:b,n}}function to(){if(Gi)return Yi;Gi=1;var r=mt(),t=Ci();return Yi=function(e,n,i,o){try{return o?n(r(i)[0],i[1]):n(i)}catch(u){t(e,"throw",u)}}}function eo(){return $i?qi:($i=1,qi=function(r,t){var e="function"==typeof Iterator&&Iterator.prototype[r];if(e)try{e.call({next:null},t).next()}catch(n){return!0}})}!function(){if(Ji)return Qi;Ji=1;var r=Ce(),t=g(),e=Or(),n=mt(),i=Mi(),o=ro(),u=to(),a=Xr(),c=Ci(),f=eo(),s=Ui(),l=!a&&!f("filter",function(){}),h=!a&&!l&&s("filter",TypeError),v=a||l||h,p=o(function(){for(var r,e,i=this.iterator,o=this.predicate,a=this.next;;){if(r=n(t(a,i)),this.done=!!r.done)return;if(e=r.value,u(i,o,[e,this.counter++],!0))return e}});r({target:"Iterator",proto:!0,real:!0,forced:v},{filter:function(r){n(this);try{e(r)}catch(o){c(this,"throw",o)}return h?t(h,this,r):new p(i(this),{predicate:r})}})}();var no,io={};!function(){if(no)return io;no=1;var r=Ce(),t=g(),e=_i(),n=Or(),i=mt(),o=Mi(),u=Ci(),a=Ui()("find",TypeError);r({target:"Iterator",proto:!0,real:!0,forced:a},{find:function(r){i(this);try{n(r)}catch(s){u(this,"throw",s)}if(a)return t(a,this,r);var c=o(this),f=0;return e(c,function(t,e){if(r(t,f++))return e(t)},{IS_RECORD:!0,INTERRUPTED:!0}).result}})}();var oo,uo,ao,co={};function fo(){if(uo)return oo;uo=1;var r=g(),t=mt(),e=Mi(),n=ji();return oo=function(i,o){o&&"string"==typeof i||t(i);var u=n(i);return e(t(void 0!==u?r(u,i):i))}}!function(){if(ao)return co;ao=1;var r=Ce(),t=g(),e=Or(),n=mt(),i=Mi(),o=fo(),u=ro(),a=Ci(),c=Xr(),f=eo(),s=Ui(),l=!c&&!f("flatMap",function(){}),h=!c&&!l&&s("flatMap",TypeError),v=c||l||h,p=u(function(){for(var r,e,i=this.iterator,u=this.mapper;;){if(e=this.inner)try{if(!(r=n(t(e.next,e.iterator))).done)return r.value;this.inner=null}catch(c){a(i,"throw",c)}if(r=n(t(this.next,i)),this.done=!!r.done)return;try{this.inner=o(u(r.value,this.counter++),!1)}catch(c){a(i,"throw",c)}}});r({target:"Iterator",proto:!0,real:!0,forced:v},{flatMap:function(r){n(this);try{e(r)}catch(o){a(this,"throw",o)}return h?t(h,this,r):new p(i(this),{mapper:r,inner:null})}})}();var so,lo={};!function(){if(so)return lo;so=1;var r=Ce(),t=g(),e=_i(),n=Or(),i=mt(),o=Mi(),u=Ci(),a=Ui()("forEach",TypeError);r({target:"Iterator",proto:!0,real:!0,forced:a},{forEach:function(r){i(this);try{n(r)}catch(s){u(this,"throw",s)}if(a)return t(a,this,r);var c=o(this),f=0;e(c,function(t){r(t,f++)},{IS_RECORD:!0})}})}();var ho,vo={};!function(){if(ho)return vo;ho=1;var r=Ce(),t=g(),e=Or(),n=mt(),i=Mi(),o=ro(),u=to(),a=Ci(),c=eo(),f=Ui(),s=Xr(),l=!s&&!c("map",function(){}),h=!s&&!l&&f("map",TypeError),v=s||l||h,p=o(function(){var r=this.iterator,e=n(t(this.next,r));if(!(this.done=!!e.done))return u(r,this.mapper,[e.value,this.counter++],!0)});r({target:"Iterator",proto:!0,real:!0,forced:v},{map:function(r){n(this);try{e(r)}catch(o){a(this,"throw",o)}return h?t(h,this,r):new p(i(this),{mapper:r})}})}();var po,yo,go,wo={};function mo(){if(yo)return po;yo=1;var r=y(),t=Function.prototype,e=t.apply,n=t.call;return po="object"==typeof Reflect&&Reflect.apply||(r?n.bind(e):function(){return n.apply(e,arguments)}),po}!function(){if(go)return wo;go=1;var r=Ce(),t=_i(),e=Or(),n=mt(),i=Mi(),o=Ci(),u=Ui(),a=mo(),c=p(),f=TypeError,s=c(function(){[].keys().reduce(function(){},void 0)}),l=!s&&u("reduce",f);r({target:"Iterator",proto:!0,real:!0,forced:s||l},{reduce:function(r){n(this);try{e(r)}catch(v){o(this,"throw",v)}var u=arguments.length<2,c=u?void 0:arguments[1];if(l)return a(l,this,u?[r]:[r,c]);var s=i(this),h=0;if(t(s,function(t){u?(u=!1,c=t):c=r(c,t,h),h++},{IS_RECORD:!0}),u)throw new f("Reduce of empty iterator with no initial value");return c}})}();var bo,Eo={};!function(){if(bo)return Eo;bo=1;var r=Ce(),t=g(),e=_i(),n=Or(),i=mt(),o=Mi(),u=Ci(),a=Ui()("some",TypeError);r({target:"Iterator",proto:!0,real:!0,forced:a},{some:function(r){i(this);try{n(r)}catch(s){u(this,"throw",s)}if(a)return t(a,this,r);var c=o(this),f=0;return e(c,function(t,e){if(r(t,f++))return e()},{IS_RECORD:!0,INTERRUPTED:!0}).stopped}})}();var So,Oo={};!function(){if(So)return Oo;So=1;var r=Ce(),t=mt(),e=_i(),n=Mi(),i=[].push;r({target:"Iterator",proto:!0,real:!0},{toArray:function(){var r=[];return e(n(t(this)),i,{that:r,IS_RECORD:!0}),r}})}();var Ao,Ro,Io,To,xo,ko={};function Do(){if(Ro)return Ao;Ro=1;var r=i(),t=p(),e=r.RegExp,n=!t(function(){var r=!0;try{e(".","d")}catch(c){r=!1}var t={},n="",i=r?"dgimsy":"gimsy",o=function(r,e){Object.defineProperty(t,r,{get:function(){return n+=e,!0}})},u={dotAll:"s",global:"g",ignoreCase:"i",multiline:"m",sticky:"y"};for(var a in r&&(u.hasIndices="d"),u)o(a,u[a]);return Object.getOwnPropertyDescriptor(e.prototype,"flags").get.call(t)!==i||n!==i});return Ao={correct:n}}function jo(){if(To)return Io;To=1;var r=mt();return Io=function(){var t=r(this),e="";return t.hasIndices&&(e+="d"),t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.dotAll&&(e+="s"),t.unicode&&(e+="u"),t.unicodeSets&&(e+="v"),t.sticky&&(e+="y"),e}}!function(){if(xo)return ko;xo=1;var r=d(),t=$e(),e=Do(),n=jo();r&&!e.correct&&(t(RegExp.prototype,"flags",{configurable:!0,get:n}),e.correct=!0)}();var Po,Co,_o,Mo,Uo,Bo,Lo,No,Fo,zo,Wo,Ho,Vo,Yo,Go,qo,$o,Jo,Qo,Xo={};function Ko(){if(Co)return Po;Co=1;var r=ar(),t=Set.prototype;return Po={Set:Set,add:r(t.add),has:r(t.has),remove:r(t.delete),proto:t}}function Zo(){if(Mo)return _o;Mo=1;var r=Ko().has;return _o=function(t){return r(t),t}}function ru(){if(Bo)return Uo;Bo=1;var r=g();return Uo=function(t,e,n){for(var i,o,u=n?t:t.iterator,a=t.next;!(i=r(a,u)).done;)if(void 0!==(o=e(i.value)))return o}}function tu(){if(No)return Lo;No=1;var r=ar(),t=ru(),e=Ko(),n=e.Set,i=e.proto,o=r(i.forEach),u=r(i.keys),a=u(new n).next;return Lo=function(r,e,n){return n?t({iterator:u(r),next:a},e):o(r,e)}}function eu(){if(zo)return Fo;zo=1;var r=Ko(),t=tu(),e=r.Set,n=r.add;return Fo=function(r){var i=new e;return t(r,function(r){n(i,r)}),i}}function nu(){if(Ho)return Wo;Ho=1;var r=Qe(),t=Ko();return Wo=r(t.proto,"size","get")||function(r){return r.size}}function iu(){if(Yo)return Vo;Yo=1;var r=Or(),t=mt(),e=g(),n=ce(),i=Mi(),o="Invalid size",u=RangeError,a=TypeError,c=Math.max,f=function(t,e){this.set=t,this.size=c(e,0),this.has=r(t.has),this.keys=r(t.keys)};return f.prototype={getIterator:function(){return i(t(e(this.keys,this.set)))},includes:function(r){return e(this.has,this.set,r)}},Vo=function(r){t(r);var e=+r.size;if(e!=e)throw new a(o);var i=n(e);if(i<0)throw new u(o);return new f(r,i)}}function ou(){if(Jo)return $o;Jo=1;var r=dr(),t=function(r){return{size:r,has:function(){return!1},keys:function(){return{next:function(){return{done:!0}}}}}},e=function(r){return{size:r,has:function(){return!0},keys:function(){throw new Error("e")}}};return $o=function(n,i){var o=r("Set");try{(new o)[n](t(0));try{return(new o)[n](t(-1)),!1}catch(a){if(!i)return!0;try{return(new o)[n](e(-1/0)),!1}catch(c){var u=new o;return u.add(1),u.add(2),i(u[n](e(1/0)))}}}catch(c){return!1}}}!function(){if(Qo)return Xo;Qo=1;var r=Ce(),t=function(){if(qo)return Go;qo=1;var r=Zo(),t=Ko(),e=eu(),n=nu(),i=iu(),o=tu(),u=ru(),a=t.has,c=t.remove;return Go=function(t){var f=r(this),s=i(t),l=e(f);return n(f)<=s.size?o(f,function(r){s.includes(r)&&c(l,r)}):u(s.getIterator(),function(r){a(l,r)&&c(l,r)}),l}}(),e=p();r({target:"Set",proto:!0,real:!0,forced:!ou()("difference",function(r){return 0===r.size})||e(function(){var r={size:1,has:function(){return!0},keys:function(){var r=0;return{next:function(){var e=r++>1;return t.has(1)&&t.clear(),{done:e,value:2}}}}},t=new Set([1,2,3,4]);return 3!==t.difference(r).size})},{difference:t})}();var uu,au,cu,fu={};!function(){if(cu)return fu;cu=1;var r=Ce(),t=p(),e=function(){if(au)return uu;au=1;var r=Zo(),t=Ko(),e=nu(),n=iu(),i=tu(),o=ru(),u=t.Set,a=t.add,c=t.has;return uu=function(t){var f=r(this),s=n(t),l=new u;return e(f)>s.size?o(s.getIterator(),function(r){c(f,r)&&a(l,r)}):i(f,function(r){s.includes(r)&&a(l,r)}),l}}();r({target:"Set",proto:!0,real:!0,forced:!ou()("intersection",function(r){return 2===r.size&&r.has(1)&&r.has(2)})||t(function(){return"3,2"!==String(Array.from(new Set([1,2,3]).intersection(new Set([3,2]))))})},{intersection:e})}();var su,lu,hu,vu={};!function(){if(hu)return vu;hu=1;var r=Ce(),t=function(){if(lu)return su;lu=1;var r=Zo(),t=Ko().has,e=nu(),n=iu(),i=tu(),o=ru(),u=Ci();return su=function(a){var c=r(this),f=n(a);if(e(c)<=f.size)return!1!==i(c,function(r){if(f.includes(r))return!1},!0);var s=f.getIterator();return!1!==o(s,function(r){if(t(c,r))return u(s,"normal",!1)})}}();r({target:"Set",proto:!0,real:!0,forced:!ou()("isDisjointFrom",function(r){return!r})},{isDisjointFrom:t})}();var pu,du,yu,gu={};!function(){if(yu)return gu;yu=1;var r=Ce(),t=function(){if(du)return pu;du=1;var r=Zo(),t=nu(),e=tu(),n=iu();return pu=function(i){var o=r(this),u=n(i);return!(t(o)>u.size)&&!1!==e(o,function(r){if(!u.includes(r))return!1},!0)}}();r({target:"Set",proto:!0,real:!0,forced:!ou()("isSubsetOf",function(r){return r})},{isSubsetOf:t})}();var wu,mu,bu,Eu={};!function(){if(bu)return Eu;bu=1;var r=Ce(),t=function(){if(mu)return wu;mu=1;var r=Zo(),t=Ko().has,e=nu(),n=iu(),i=ru(),o=Ci();return wu=function(u){var a=r(this),c=n(u);if(e(a)2?n:r(e),u=new t(o);o>i;)u[i]=e[i++];return u},Qu}!function(){if(Ku)return Zu;Ku=1;var r=Ju(),t=ar(),e=Or(),n=ra(),i=r.aTypedArray,o=r.getTypedArrayConstructor,u=r.exportTypedArrayMethod,a=t(r.TypedArrayPrototype.sort);u("toSorted",function(r){void 0!==r&&e(r);var t=i(this),u=n(o(t),t);return a(u,r)})}();var ta,ea,na,ia,oa,ua,aa,ca={};function fa(){if(ea)return ta;ea=1;var r=le(),t=ce(),e=RangeError;return ta=function(n,i,o,u){var a=r(n),c=t(o),f=c<0?a+c:c;if(f>=a||f<0)throw new e("Incorrect index");for(var s=new i(a),l=0;lE;E++)d=g[E],(p=i(w(d,E,y)))in m?f(m[p],d):m[p]=[d];if(h&&(v=h(y))!==c)for(p in m)m[p]=a(v,m[p]);return m}}(),e=wa();r({target:"Array",proto:!0},{group:function(r){return t(this,r,arguments.length>1?arguments[1]:void 0)}}),e("group")}();var ma,ba,Ea,Sa,Oa,Aa={};function Ra(){if(ba)return ma;ba=1;var r=Di(),t=String;return ma=function(e){if("Symbol"===r(e))throw new TypeError("Cannot convert a Symbol value to a string");return t(e)}}function Ia(){if(Sa)return Ea;Sa=1;var r=ar(),t=et(),e=SyntaxError,n=parseInt,i=String.fromCharCode,o=r("".charAt),u=r("".slice),a=r(/./.exec),c={'\\"':'"',"\\\\":"\\","\\/":"/","\\b":"\b","\\f":"\f","\\n":"\n","\\r":"\r","\\t":"\t"},f=/^[\da-f]{4}$/i,s=/^[\u0000-\u001F]$/;return Ea=function(r,l){for(var h=!0,v="";l>16&255,i>>8&255,255&i];if(2===n){if(e&&0!==o[1])throw new l("Extra bits");return[o[0]]}if(3===n){if(e&&0!==o[2])throw new l("Extra bits");return[o[0],o[1]]}return o},y=function(r,t,e){for(var n=t.length,i=0;i0){if("stop-before-partial"===w)break;if("loose"!==w)throw new l("Missing padding");if(1===O.length)throw new l("Malformed padding: exactly one additional character");E=y(b,d(O,g,!1),E)}S=m;break}var R=v(r,A);if(++A,"="===R){if(O.length<2)throw new l("Padding is too early");if(A=p(r,A),2===O.length){if(A===m){if("stop-before-partial"===w)break;throw new l("Malformed padding: only one =")}"="===v(r,A)&&(++A,A=p(r,A))}if(A1?arguments[1]:void 0,this,this.length);return{read:t.read,written:t.written}}}),za}Fa||(Fa=1,qa());var $a,Ja,Qa,Xa,Ka={};function Za(){if(Qa)return Ka;Qa=1;var r=Ce(),t=i(),e=Ha(),n=Ga(),o=mn(),u=function(){if(Ja)return $a;Ja=1;var r=i(),t=ar(),e=r.Uint8Array,n=r.SyntaxError,o=r.parseInt,u=Math.min,a=/[^\da-f]/i,c=t(a.exec),f=t("".slice);return $a=function(r,t){var i=r.length;if(i%2!=0)throw new n("String should be an even number of characters");for(var s=t?u(t.length,i/2):i/2,l=t||new e(s),h=0,v=0;v>6*r&63)};v+2i,l=n(o)?o:f(o),h=s?a(arguments,i):[],v=s?function(){e(l,this,h)}:l;return t?r(v,u):r(v)}:r},Pc}Mc||(Mc=1,function(){if(Tc)return xc;Tc=1;var r=Ce(),t=i(),e=jc().clear;r({global:!0,bind:!0,enumerable:!0,forced:t.clearImmediate!==e},{clearImmediate:e})}(),function(){if(_c)return Uc;_c=1;var r=Ce(),t=i(),e=jc().set,n=Bc(),o=t.setImmediate?n(e,!1):e;r({global:!0,bind:!0,enumerable:!0,forced:t.setImmediate!==o},{setImmediate:o})}());var Lc,Nc,Fc,zc,Wc,Hc,Vc,Yc,Gc,qc={};function $c(){if(Nc)return Lc;Nc=1;var r=ar(),t=p(),e=vr(),n=Di(),i=dr(),o=Lt(),u=function(){},a=i("Reflect","construct"),c=/^\s*(?:class|function)\b/,f=r(c.exec),s=!c.test(u),l=function(r){if(!e(r))return!1;try{return a(u,[],r),!0}catch(t){return!1}},h=function(r){if(!e(r))return!1;switch(n(r)){case"AsyncFunction":case"GeneratorFunction":case"AsyncGeneratorFunction":return!1}try{return s||!!f(c,o(r))}catch(t){return!0}};return h.sham=!0,Lc=!a||t(function(){var r;return l(l.call)||!l(Object)||!l(function(){r=!0})||r})?h:l}function Jc(){if(Hc)return Wc;Hc=1;var r=ar(),t=Map.prototype;return Wc={Map:Map,set:r(t.set),get:r(t.get),has:r(t.has),remove:r(t.delete),proto:t}}function Qc(){if(Yc)return Vc;Yc=1;var r=p(),t=ur();return Vc=!r(function(){var r=new Error("a");return!("stack"in r)||(Object.defineProperty(r,"stack",t(1,7)),7!==r.stack)})}!function(){if(Gc)return qc;Gc=1;var r,t=Xr(),e=Ce(),n=i(),o=dr(),u=ar(),a=p(),c=nt(),f=vr(),s=$c(),l=sr(),h=pr(),v=Er(),d=_i(),y=mt(),w=Di(),m=et(),b=Nn(),E=Et(),S=le(),O=Dc(),A=function(){if(zc)return Fc;zc=1;var r=g(),t=et(),e=yr(),n=Do(),i=jo(),o=RegExp.prototype;return Fc=n.correct?function(r){return r.flags}:function(u){return n.correct||!e(o,u)||t(u,"flags")?u.flags:r(i,u)}}(),R=Jc(),I=Ko(),T=tu(),x=An(),k=Qc(),D=On(),j=n.Object,P=n.Array,C=n.Date,_=n.Error,M=n.TypeError,U=n.PerformanceMark,B=o("DOMException"),L=R.Map,N=R.has,F=R.get,z=R.set,W=I.Set,H=I.add,V=I.has,Y=o("Object","keys"),G=u([].push),q=u((!0).valueOf),$=u(1.1.valueOf),J=u("".valueOf),Q=u(C.prototype.getTime),X=c("structuredClone"),K="DataCloneError",Z="Transferring",rr=function(r){return!a(function(){var t=new n.Set([7]),e=r(t),i=r(j(7));return e===t||!e.has(7)||!h(i)||7!==+i})&&r},tr=function(r,t){return!a(function(){var e=new t,n=r({a:e,b:e});return!(n&&n.a===n.b&&n.a instanceof t&&n.a.stack===e.stack)})},er=n.structuredClone,nr=t||!tr(er,_)||!tr(er,B)||(r=er,!!a(function(){var t=r(new n.AggregateError([1],X,{cause:3}));return"AggregateError"!==t.name||1!==t.errors[0]||t.message!==X||3!==t.cause})),ir=!er&&rr(function(r){return new U(X,{detail:r}).detail}),or=rr(er)||ir,ur=function(r){throw new B("Uncloneable type: "+r,K)},cr=function(r,t){throw new B((t||"Cloning")+" of "+r+" cannot be properly polyfilled in this engine",K)},fr=function(r,t){return or||cr(t),or(r)},lr=function(r,t,e){if(N(t,r))return F(t,r);var i,o,u,a,c,s;if("SharedArrayBuffer"===(e||w(r)))i=or?or(r):r;else{var l=n.DataView;l||f(r.slice)||cr("ArrayBuffer");try{if(f(r.slice)&&!r.resizable)i=r.slice(0);else{o=r.byteLength,u="maxByteLength"in r?{maxByteLength:r.maxByteLength}:void 0,i=new ArrayBuffer(o,u),a=new l(r),c=new l(i);for(s=0;s1&&!l(arguments[1])?y(arguments[1]):void 0,o=i?i.transfer:void 0;void 0!==o&&(e=function(r,t){if(!h(r))throw new M("Transfer option cannot be converted to a sequence");var e=[];d(r,function(r){G(e,y(r))});for(var i,o,u,a,c,l=0,v=S(e),p=new W;le.length&&"/"!==n[n.length-1]))return n+r.slice(e.length);c("W2",e,n)}}function c(t,e,n){console.warn(r(t,[n,e].join(", ")))}function f(r,t,e){for(var n=r.scopes,i=e&&u(e,n);i;){var o=a(t,n[i]);if(o)return o;i=u(i.slice(0,i.lastIndexOf("/")),n)}return a(t,r.imports)||-1!==t.indexOf(":")&&t}function s(){this[I]={}}function l(t,e,n,i){var o=t[I][e];if(o)return o;var u=[],a=Object.create(null);R&&Object.defineProperty(a,R,{value:"Module"});var c=Promise.resolve().then(function(){return t.instantiate(e,n,i)}).then(function(n){if(!n)throw Error(r(2,e));var i=n[1](function(r,t){o.h=!0;var e=!1;if("string"==typeof r)r in a&&a[r]===t||(a[r]=t,e=!0);else{for(var n in r)t=r[n],n in a&&a[n]===t||(a[n]=t,e=!0);r&&r.__esModule&&(a.__esModule=r.__esModule)}if(e)for(var i=0;i-1){var e=document.createEvent("Event");e.initEvent("error",!1,!1),t.dispatchEvent(e)}return Promise.reject(r)})}else if("systemjs-importmap"===t.type){t.sp=!0;var e=t.src?(System.fetch||fetch)(t.src,{integrity:t.integrity,priority:t.fetchPriority,passThrough:!0}).then(function(r){if(!r.ok)throw Error(r.status);return r.text()}).catch(function(e){return e.message=r("W4",t.src)+"\n"+e.message,console.warn(e),"function"==typeof t.onerror&&t.onerror(),"{}"}):t.innerHTML;j=j.then(function(){return e}).then(function(e){!function(t,e,n){var i={};try{i=JSON.parse(e)}catch(a){console.warn(Error(r("W5")))}o(i,n,t)}(P,e,t.src||y)})}})}var y,g="undefined"!=typeof Symbol,w="undefined"!=typeof self,m="undefined"!=typeof document,b=w?self:e;if(m){var E=document.querySelector("base[href]");E&&(y=E.href)}if(!y&&"undefined"!=typeof location){var S=(y=location.href.split("#")[0].split("?")[0]).lastIndexOf("/");-1!==S&&(y=y.slice(0,S+1))}var O,A=/\\/g,R=g&&Symbol.toStringTag,I=g?Symbol():"@",T=s.prototype;T.import=function(r,t,e){var n=this;return t&&"object"==typeof t&&(e=t,t=void 0),Promise.resolve(n.prepareImport()).then(function(){return n.resolve(r,t,e)}).then(function(r){var t=l(n,r,void 0,e);return t.C||v(n,t)})},T.createContext=function(r){var t=this;return{url:r,resolve:function(e,n){return Promise.resolve(t.resolve(e,n||r))}}},T.register=function(r,t,e){O=[r,t,e]},T.getRegister=function(){var r=O;return O=void 0,r};var x=Object.freeze(Object.create(null));b.System=new s;var k,D,j=Promise.resolve(),P={imports:{},scopes:{},depcache:{},integrity:{}},C=m;if(T.prepareImport=function(r){return(C||r)&&(d(),C=!1),j},T.getImportMap=function(){return JSON.parse(JSON.stringify(P))},m&&(d(),window.addEventListener("DOMContentLoaded",d)),T.addImportMap=function(r,t){o(r,t||y,P)},m){window.addEventListener("error",function(r){M=r.filename,U=r.error});var _=location.origin}T.createScript=function(r){var t=document.createElement("script");t.async=!0,r.indexOf(_+"/")&&(t.crossOrigin="anonymous");var e=P.integrity[r];return e&&(t.integrity=e),t.src=r,t};var M,U,B={},L=T.register;T.register=function(r,t){if(m&&"loading"===document.readyState&&"string"!=typeof r){var e=document.querySelectorAll("script[src]"),n=e[e.length-1];if(n){k=r;var i=this;D=setTimeout(function(){B[n.src]=[r,t],i.import(n.src)})}}else k=void 0;return L.call(this,r,t)},T.instantiate=function(t,e){var n=B[t];if(n)return delete B[t],n;var i=this;return Promise.resolve(T.createScript(t)).then(function(n){return new Promise(function(o,u){n.addEventListener("error",function(){u(Error(r(3,[t,e].join(", "))))}),n.addEventListener("load",function(){if(document.head.removeChild(n),M===t)u(U);else{var r=i.getRegister(t);r&&r[0]===k&&clearTimeout(D),o(r)}}),document.head.appendChild(n)})})},T.shouldFetch=function(){return!1},"undefined"!=typeof fetch&&(T.fetch=fetch);var N=T.instantiate,F=/^(text|application)\/(x-)?javascript(;|$)/;T.instantiate=function(t,e,n){var i=this;return this.shouldFetch(t,e,n)?this.fetch(t,{credentials:"same-origin",integrity:P.integrity[t],meta:n}).then(function(n){if(!n.ok)throw Error(r(7,[n.status,n.statusText,t,e].join(", ")));var o=n.headers.get("content-type");if(!o||!F.test(o))throw Error(r(4,o));return n.text().then(function(r){return r.indexOf("//# sourceURL=")<0&&(r+="\n//# sourceURL="+t),(0,eval)(r),i.getRegister(t)})}):N.apply(this,arguments)},T.resolve=function(e,n){return f(P,t(e,n=n||y)||e,n)||function(t,e){throw Error(r(8,[t,e].join(", ")))}(e,n)};var z=T.instantiate;T.instantiate=function(r,t,e){var n=P.depcache[r];if(n)for(var i=0;i