diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b34570..b4c1d94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,93 @@ # Changelog +## [1.4.0] + +### Added +- **Schema-aware initialization** at *Initialize Knowledge Graph* time, with three modes: skip schema, generate a draft from sample documents, or paste a GSQL schema. Drafts are reviewed in a form-mode editor before being applied as a single atomic schema-change job that never drops existing types. +- **Schema-aware extraction**: when an extracted entity or relationship matches a declared domain type or pair, ECC populates the domain vertex / edge directly. A configurable strict mode drops non-schema extractions instead of falling back to the raw `Entity` layer. +- **Typed-relationship metadata layer** (`EntityType` / `RelationshipType` vertices linked by `IS_HEAD_OF` / `HAS_TAIL`) carrying type names and human-readable definitions; available to retrievers and to the chat agent. The layer auto-fills from extracted free-text types when no domain types are declared (with case / suffix / plural deduplication), and is restricted to declared types only when a domain schema exists. +- **Customizable schema-extraction prompt** in the `/prompts` API alongside the existing chatbot, entity-relationship, community-summarization, and query-generation prompts. Per-graph overrides supported. +- **Schema definitions threaded into LLM prompts** for query generation (Cypher / GSQL) and entity-relationship extraction, so the model sees per-type descriptions alongside the schema rep. +- **JSONL caching shared between schema extraction and ingest** — files uploaded for schema extraction are reused by the ingest flow without re-conversion. +- **Parallel image description** during PDF processing (default 8 workers, env-overridable). +- **Async embedding-store initialization** — service startup no longer blocks on the TigerGraph connection; status surfaces as `initializing` / `ok` / `error`. +- **Auto retrieval method selection** — new "Auto" option in the chat dropdown picks among Similarity / Contextual / Hybrid / Community per question + - Two-stage selector: deterministic regex rules cover common cases; LLM fallback handles the rest with a subset-aware prompt + - Selection visible via a chip below each bot reply (method icon + label; reason and source in hover tooltip) + - Manual method selection still works as override during the transition +- **Method selection telemetry** — Prometheus counter `llm_method_selection_total` with `selected_method` and `selection_source` labels +- **Out-of-corpus short-circuit** — when the chosen retriever returns no results, the system returns an honest "couldn't find relevant info" message instead of letting the LLM hallucinate from empty context +- **In-lane retrieval fallback** — when a chunk-based search method (similarity / contextual / hybrid) returns fewer than `top_k` chunks, the system tries a second method via a subset-aware fallback table (similarity → hybrid, contextual → hybrid, hybrid → community). Single retry, skipped for manual mode and community search. +- **Cross-lane fallback to vector search** — when `generate_function` or `generate_cypher` retries are exhausted (3 rewrite cycles), the system falls back to auto-selected vector search instead of going straight to the apology message. Forces auto-selection regardless of configured method, so even manual users get the best vector option in this recovery path. Toggleable per-graph via `graphrag_config.enable_router_fallback` (default `true`); also editable from the GraphRAG config page in the admin UI. +- **Trace Logs UI** — new admin page that captures and displays the full agent execution trace for each chat turn (per-node inputs/outputs, durations in seconds, citations, token usage by node) + - Citations tab (now shown first), Token Overview tab, and a per-node detail view + - Role-gated "View Trace" entry from the chat reply; superuser-only access on the trace endpoint + - Per-user ownership check on `/ui/trace/{message_id}` and 30-day automatic cleanup of stored traces + - Routed through nginx at `/trace` +- **Excel and CSV ingestion** — `.xlsx` / `.xls` / `.csv` accepted in document ingestion; the upload UI shows a clear warning when an unsupported file type is selected + - Headerless Excel sheets preserve all rows; CSV extraction handles non-UTF-8 encodings without dropping content + +### Changed +- **All customizable prompts now ship as in-code defaults**, packaged inside the LLM service. Provider prompt directories are kept (empty) for backward compatibility; per-graph and global overrides still win when present. +- **`prompt_path` is a top-level `llm_config` field**, applied across LLM-prompted services automatically. Per-service `prompt_path` entries are still honored on disk but no longer needed. +- **Permissive schema parser** accepts both `DIRECTED` and `UNDIRECTED` edges and rejects names that collide with GraphRAG structural types or GSQL keywords. +- **Server-side prompt validation**: `/prompts` POST rejects edits missing required placeholders and auto-escapes stray `{token}` occurrences in user content. +- **`apply_proposal` reports a real failure** when the GSQL server returns a known error marker, instead of falsely reporting success. +- **TigerGraph embedding store skips redundant GDS install** when the `gds.vector` package is already installed, eliminating multi-minute catalog-lock stalls on container restart. +- **TigerGraph version mismatch** raises a clear `ValueError` at ECC startup instead of leaving the embedding store undefined. +- **`check_embedding_store_status()`** in the inquiryai / supportai routers raises HTTP 503 instead of swallowing the exception. +- **Bedrock `max_tokens` is auto-defaulted** per model family (Claude 3.x = 4096, Sonnet 3.5+ / 4.x = 8192, Titan / Cohere / Llama at their published caps), so schema extraction and other large-output prompts no longer truncate at the langchain-aws built-in 1024 default. Explicit `model_kwargs.max_tokens` and the existing `token_limit` config field both override the auto-default. +- **Hybrid / similarity retrievers surface domain vertex types** in the LLM context with a `: ` label, so type-aware questions (e.g. "which companies …") receive properly grounded answers. +- **Community / hybrid retrievers walk domain edges and domain VTs directly** when a schema exists. The `Entity` layer becomes scaffolding for Louvain; community memberships are mirrored from `Entity` onto matching domain-VT instances after community detection so retrievers reach community context without traversing the legacy layer. New `graphrag_config.retrieval_include_entity` flag controls whether `Entity` stays visible to the chat agent — when unset, defaults to `false` for graphs with a domain schema (typed-purist) and `true` otherwise (no-op fallback). +- **`apply_proposal` re-installs retriever queries** against the live domain schema, idempotently. Identical bodies are TG no-ops; new domain types or a changed `retrieval_include_entity` value re-render the affected queries on the next apply call. +- **Transitional-graph detection at schema apply**: when a domain schema is added to a graph that already has Entity-layer data (typical v1.3.x → v1.4.0 upgrade applying a schema for the first time), `apply_proposal` forces `retrieval_include_entity=True` for the rendered queries so existing Entity rows stay reachable. The result payload carries a `transitional` block (`entity_count`, `new_domain_vts`, `recommendation`) for the init-graph dialog to surface a "your existing entities won't be auto-typed — re-ingest for full schema awareness" prompt. Once the user clears derived data and re-ingests (planned v1.5 admin endpoint), the auto-default flips back to typed-purist on the next apply call. +- **Empty function-call results now trigger retry** — `generate_function` now treats an empty result as a generation failure (symmetric with `generate_cypher`). Rewrite-and-retry kicks in, and after 3 cycles the cross-lane vector fallback runs. Previously, empty function results passed through to answer generation and risked hallucinated narratives around the emptiness. +- **Default chat retrieval method is now `auto`** instead of `hybridsearch`. Existing graphs that did not configure a method explicitly will route through the new selector after upgrade. Manual mode (and any explicitly-selected method in the chat dropdown) overrides the default unchanged. +- **Schema parser drops attribute names that collide with GSQL reserved words** (e.g. `count`, `min`, `max`). LLM-extracted schemas previously failed schema-change with `Encountered "," at line N` when an attribute named after a keyword reached TG; the offending attribute is now silently skipped at parse time. +- **`apply_proposal` runs schema changes in two phases** — phase 1 issues every `ADD VERTEX` / `ADD EDGE`, phase 2 issues every `ALTER EDGE ADD PAIR`. TG validates an entire `SCHEMA_CHANGE JOB` upfront, so an `ALTER` referencing a vertex type created in the same job aborted with a parser error. Splitting the job means phase 2 runs against a graph where the new types already exist. The result payload now exposes both phase names via a new `job_names` list; the legacy `job_name` key remains the first phase that ran. +- **Schema-extraction sample budget now scales with the configured LLM**. Previously hardcoded at 200 KB (~50K tokens), causing later files in multi-file uploads to be silently truncated. The budget is now resolved from `llm_config.token_limit` if set, otherwise from a per-model context-window table (Claude family 200K / Opus 4.7 1M, GPT-4o 128K, Gemini 1.5 1M, etc.). Unknown models fall back to a similar family default with a warning. Within the resolved budget, characters are distributed across uploaded files using equal-share-with-rollover so every file contributes — the first file no longer crowds out the rest. +- **`/initialize_graph` is now an async-job endpoint**. POST returns 202-style `{"status": "submitted"}` immediately and the long-running work (structural schema, optional domain schema apply, retriever installs) runs in a `BackgroundTask`. Clients poll `GET /ui/{graphname}/initialize_status` for `state` (`queued` / `running` / `completed` / `error`) and the final result. Previously the endpoint was fully synchronous; long inits (TG schema-change + retriever installs ≥ 5 minutes) tripped the browser's idle-response cutoff with `net::ERR_TIMED_OUT` even when the backend completed successfully. +- **New `GET /ui/list_graphs`**. Returns the live list of graphs the authenticated user has access to. UI clients (`KGAdmin`, `IngestGraph`, `Setup`) now seed `availableGraphs` from `sessionStorage` for instant render and then refresh from the live list, so a graph created mid-session is visible without a re-login. +- **Init / extract dialogs pause the idle timer for the duration of the long-running call**. The dialog used to log the user out after 60 minutes of "no activity" even while a backend init or schema extraction was in flight; the existing `pauseIdleTimer()` / `resumeIdleTimer()` pattern is now wired into `handleExtractSchema` and `handleInitializeGraph`. +- **Removed two dead vertex-type references from the retriever queries**. `Content_Similarity_Search` and `Content_Similarity_Vector_Search` referenced `Relationship` (never a vertex type — it's the `RELATIONSHIP` edge) and `Concept` (removed in an earlier release); both queries now save as draft with TYP-152 errors against any v1.4.0 graph. The IF-branch is reduced to `s.type == "Entity"` and the existing `Community` branch. +- **Retriever-install error detection no longer false-positives on TG's normal output**. `install_retrievers` and `install_retrievers_async` were doing a substring `"error" in output.lower()` check, which trips on every successful install (TG output contains literals like `0 errors`, `no warnings`). Both now delegate to the existing `gsql_output_error()` helper that matches actual error markers (`SEMANTIC ERROR`, `Failed to create`, transport-level failures). +- **Suggested types in the Generate-from-samples dialog**. Two chip inputs ("Suggested Vertex Types", "Suggested Edge Types") let the user guide the schema extractor with structured hints. Vertex chips use `Name` or `Name: description`; edge chips additionally accept `Name (From -> To)` to pin direction. Hints render into a `## Suggested types` block injected before the prompt's `## Inputs` section, and the fully-rendered prompt is auto-saved as the graph's per-graph `schema_extraction.txt` override after a successful init — future re-extractions for the same graph reuse the same guidance. +- **Inline rejection of reserved names in suggested type chips**. New `GET /ui/schema_reserved_names` returns GSQL reserved words and GraphRAG structural type names; the dialog rejects suggestions that would collide with either, before the call is made. Previously such names were silently dropped by the downstream parser, leaving the user wondering why a suggested type didn't appear in the draft. +- **Schema Extraction prompt is now editable on the Customize Prompts page**, alongside the existing four prompts. Global and per-graph scope both work — the per-graph file is what the auto-save from the suggested-types flow writes. +- **Per-card collapse in the draft-schema review form**. Each vertex / edge card has a chevron toggle that hides everything except its name row; section headers expose "Collapse all vertex types" / "Collapse all edge types" buttons. Keeps a 30+ type proposal readable without losing edit access. +- **Multimodal image-description LLM calls are now distinguishable in the log**. `describe_image_with_llm` emits `multimodal_describe: image= model=` before each call and a paired `done` line after, so the per-image vision calls can be filtered out of the chat-completions stream (a single PDF can produce hundreds of them). +- **Ingest no longer fails with `Data path not found: None`** when the "Ingest Documents into Knowledge Graph" button is clicked without first running the two-step ingest flow. The UI handler now calls `/create_ingest` first when the cached job state is empty, so the backend always receives a well-shaped configuration with the resolved JSONL temp folder. +- **Query Guidance** — a free-form, optional partial that the user edits on the Customize Prompts page. Empty by default; when configured, the rendered block is injected after the hard rules in `map_question_to_schema`, `generate_function`, `generate_cypher`, and `generate_gsql`. Length-capped at 8000 characters and brace-escaped server-side. The page is also reordered by graph lifecycle (setup → ingest → rebuild → query) and the now-redundant "Configured LLM Provider" field is removed. +- **Stop aborting graph rebuilds on TigerGraph's normal success line**. `ecc.app.graphrag.util.install_queries` and its supportai sibling were raising on the literal `"failed" in res.lower()` substring — TG's success line `succeeded: N, skipped: 0, failed: 0` tripped that check and rolled back the rebuild. Both now use the existing `gsql_output_error()` helper. +- **Image version stamped at build time**. Each image now carries the repo-root `VERSION` file plus a `/code/BUILD_DATE` written at build time; `GET /ui/version` aggregates the three components for support checks, and the Setup pages show a small "Version " line at the bottom-center. Plain `docker compose build` works as-is; no env vars or helper scripts required. +- **Prompt-customization E2E test always reverts on failure**. The schema-extraction round-trip test could leak its `[E2E TEST EDIT — schema_extraction]` marker into `configs/prompts/schema_extraction.txt` whenever a mid-flight assertion failed; both the chatbot-response and schema-extraction tests now wrap their save-then-assert in `try/finally` (or `try/except`) so the revert always runs. +- **Knowledge Graph Setup cards centered**. The three setup cards (Initialize / Ingest / Refresh) drop from a 4-column grid at large breakpoints to a 3-column grid so they fill the row evenly instead of leaving an empty fourth column. +- **Per-stage progress for graph rebuild.** The refresh dialog now shows individual phases — chunking, entity extraction, community detection, domain-type update — with per-stage heartbeats so a long phase never looks stalled. +- **Reclassified data-bearing log lines from INFO to DEBUG.** Rebuild logs in steady state now carry only metadata and counts; lines that included entity names, chunk identifiers, or edge payloads drop to DEBUG. Typical INFO volume falls from a few thousand lines per rebuild to under 150. +- **Query-generation prompts see user-supplied type descriptions.** The descriptions / definitions a user attaches to vertex and edge types via Initialize Graph or Customize Prompts now reach every query-side LLM call, not just the cypher/gsql ones. +- **Graph picker stays in sync across dialogs.** Changing the selected graph in chat, refresh, ingest, or customize-prompts updates the others immediately — they no longer drift apart. +- **Chat WebSocket fails gracefully when the embedding store is unavailable.** Clients receive a structured error and a Try-Again-Later close code instead of an instant disconnect. +- **Rebuilds survive chunk-creation races.** A transient empty response when a freshly created chunk's content row hasn't flushed yet is now retried instead of aborting that chunk. +- **Images in chat render correctly.** Multi-line LLM image captions used to break the markdown image syntax so chat showed the raw markup; alt text is now sanitized on insert and the image-description prompt asks for a single content-focused paragraph (text, charts, tables, diagrams, logos — no layout or decorative styling). + +> **Upgrading from a pre-release v1.4.0 build**: graphs that already +> have domain vertex types but were created before the multi-pair +> `IN_COMMUNITY` schema landed will see a "skipping community mirror +> for [...]: IN_COMMUNITY pair not on schema" warning during +> community detection. Re-run `/apply_proposal` with the existing +> schema once to backfill the missing pairs. v1.3.x graphs (no domain +> types) are unaffected — the mirror block is skipped entirely. + +### Removed +- **`RELATIONSHIP_TYPE` edge** between `EntityType` vertices — superseded by `IS_HEAD_OF` + `HAS_TAIL` through `RelationshipType`. + +### Configuration +- New `graphrag_config` keys: `schema_max_sample_files` (default 5), `schema_max_total_mb` (default 50), `strict_mode` (default false), `retrieval_include_entity` (auto: false when domain schema present, true otherwise), `enable_router_fallback` (default true). +- New env var: `PDF_IMAGE_CONCURRENCY` (default 8). +- `graphrag-ui` build now pins pnpm via `packageManager: "pnpm@9.15.0"` and ships an `.npmrc` allow-list for `@swc/core` / `esbuild` so the Docker image build does not trip pnpm 10's strict `[ERR_PNPM_IGNORED_BUILDS]` policy. + +> Implementation-level details for v1.4.0 (parser internals, endpoint contracts, dialog state machine, prompt-resolution chain, schema-aware ECC worker logic, etc.) live in `dev/plans/graphrag/v1.4.0_implementation_notes.md`. + ## [1.3.1] ### Changed diff --git a/README.md b/README.md index 15e2db8..ce4f10d 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ --- ## Releases +* **5/16/2026**: GraphRAG v1.4.0 released. Added schema-aware knowledge graphs, auto retrieval method selection, and a Trace Logs UI, along with many other improvements and bug fixes. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.4.0) for details. * **4/10/2026**: GraphRAG v1.3.0 released. Added an admin configuration UI with role-based access and per-graph chatbot LLM override, along with many other improvements and bug fixes. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.3.0) for details. * **2/28/2026**: GraphRAG v1.2.0 released. Added Admin UI for graph initialization, document ingestion, and knowledge graph rebuild, along with many other improvements and bug fixes. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.2.0) for details. * **9/22/2025**: GraphRAG is available now officially v1.1 (v1.1.0). AWS Bedrock support is completed with BDA integration for multimodal document ingestion. See [Release Notes](https://github.com/tigergraph/graphrag/releases/tag/v1.1.0) for details. @@ -478,6 +479,11 @@ Copy the below code into `configs/server_config.json`. You shouldn’t need to c | `chat_history_api` | string | `"http://chat-history:8002"` | URL of the chat history service. No change needed when using the provided Docker Compose file. | | `chunker` | string | `"semantic"` | Default document chunker. Options: `semantic`, `character`, `regex`, `markdown`, `html`, `recursive`. | | `extractor` | string | `"llm"` | Entity extraction method. Options: `llm`, `graphrag`. | +| `strict_mode` | bool | `false` | Dynamic-schema enforcement during extraction. When `true`, entities and relationships that don't match the domain schema are dropped. When `false` (default), unmatched nodes fall back to generic `Entity` vertices. | +| `retrieval_include_entity` | bool \| null | `null` (auto) | Whether retriever queries include the generic `Entity` vertex alongside domain types. When unset, the server uses `false` if a domain schema exists and `true` otherwise. Set explicitly to override. | +| `schema_max_sample_files` | int | `5` | Maximum number of sample documents accepted by the *Generate from sample documents* path on the *Initialize Knowledge Graph* dialog. | +| `schema_max_total_mb` | int | `50` | Combined upload cap (MB) across all sample files for schema extraction. Bounds the content sent to the LLM. A single file may use the full budget; no separate per-file cap. | +| `enable_router_fallback` | bool | `true` | When the function-call or Cypher path fails after 3 retries, fall back to vector search instead of failing the query. | | `chunker_config` | object | `{}` | Chunker-specific settings (see sub-parameters below). All settings are saved regardless of which chunker is selected as default. | | ↳ `chunk_size` | int | `2048` | Maximum number of characters per chunk. Used by `character`, `markdown`, `html`, and `recursive` chunkers. Larger values produce fewer, bigger chunks; smaller values produce more, finer-grained chunks. | | ↳ `overlap_size` | int | 1/8 of `chunk_size` | Number of overlapping characters between consecutive chunks. Used by `character`, `markdown`, `html`, and `recursive` chunkers. More overlap preserves cross-chunk context but increases total chunk count. Set to `0` for no overlap. | @@ -926,7 +932,9 @@ Today's primary lever is the **entity-extraction prompt**: - **Add 1–2 short domain examples** in the prompt. Even one well-chosen exemplar (an extracted entity with type and definition) dramatically improves consistency across chunks. - **List the canonical edge verbs you want.** Encourage `PUBLISHES`, `OWNS`, `ISSUES`, `MANAGES`, `REPORTS_ON` in the relationship-extraction prompt rather than letting the LLM emit ad-hoc nominal phrases. -If extraction quality is still poor after iterating on the prompt, the next-best option today is to clear the graph's domain types and re-ingest with the improved prompt — schema growth is currently driven entirely by what extraction produces. (A schema-aware initialization flow that lets you supply a curated schema up front is on the roadmap.) +If extraction quality is still poor after iterating on the prompt, declare a domain schema up front via the *Initialize Knowledge Graph* dialog (paste GSQL, or generate a draft from sample documents) so extraction populates the types you actually want instead of growing them organically from what the LLM happens to emit. See the Configuration table above for `strict_mode` and `retrieval_include_entity` for the schema-aware behavior knobs. + +**Note on LLM faithfulness.** Entity, relationship, and attribute extraction is best-effort and may include occasional errors, especially for well-known entities. For high-stakes applications, validate critical extracted values against your source documents before relying on them. ### 4. Retrieval — match context size to the question diff --git a/VERSION b/VERSION index 3a3cd8c..88c5fb8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.1 +1.4.0 diff --git a/common/config.py b/common/config.py index 3dc3be1..dd59ad1 100644 --- a/common/config.py +++ b/common/config.py @@ -166,6 +166,17 @@ def resolve_llm_services(llm_cfg: dict) -> dict: if svc_key in cfg and "region_name" not in cfg[svc_key]: cfg[svc_key]["region_name"] = top_region + # Inject top-level prompt_path into LLM-prompted service configs + # if missing. The UI never lets users set per-service prompt_paths; + # in practice they are always identical to completion's. + # ``embedding_service`` is excluded — embedding models never load + # prompt files (their class hierarchy has no prompt-property machinery). + top_prompt_path = cfg.get("prompt_path") + if top_prompt_path: + for svc_key in ["completion_service", "multimodal_service", "chat_service"]: + if svc_key in cfg and "prompt_path" not in cfg[svc_key]: + cfg[svc_key]["prompt_path"] = top_prompt_path + completion = cfg.get("completion_service", {}) # Resolve embedding: inherit provider-level config from completion @@ -366,6 +377,15 @@ def get_graphrag_config(graphname=None): if svc_key in llm_config and "region_name" not in llm_config[svc_key]: llm_config[svc_key]["region_name"] = llm_config["region_name"] +# Inject top-level prompt_path into LLM-prompted service configs if +# missing. Embedding service is excluded — embedding models never load +# prompt files. Per-service entries on disk are accepted for backward +# compat but never written by the UI. +if "prompt_path" in llm_config: + for svc_key in ["completion_service", "multimodal_service", "chat_service"]: + if svc_key in llm_config and "prompt_path" not in llm_config[svc_key]: + llm_config[svc_key]["prompt_path"] = llm_config["prompt_path"] + _comp = llm_config.get("completion_service") if _comp is None: raise Exception("completion_service is not found in llm_config") @@ -414,6 +434,8 @@ def get_graphrag_config(graphname=None): graphrag_config["chunker"] = "semantic" if "extractor" not in graphrag_config: graphrag_config["extractor"] = "llm" +# ``retrieval_include_entity`` is resolved at install time +# (see ``common.db.retriever_render.resolve_include_entity``). reuse_embedding = graphrag_config.get("reuse_embedding", True) doc_process_switch = graphrag_config.get("doc_process_switch", True) @@ -441,6 +463,15 @@ def get_graphrag_config(graphname=None): else: raise Exception("Embedding service not implemented") +def get_embedding_service(): + """Return the current embedding service instance. + + Use this instead of importing ``embedding_service`` directly so + consumers always read the latest instance after a config reload. + """ + return embedding_service + + def get_llm_service(service_config: dict) -> LLM_Model: """ Instantiate an LLM provider from a flat service config dict. @@ -474,25 +505,134 @@ def get_llm_service(service_config: dict) -> LLM_Model: raise Exception(f"LLM service '{service_name}' not supported") -if os.getenv("INIT_EMBED_STORE", "true") == "true": +# Module-level ``embedding_store`` is the back-compat default for +# direct importers (``from common.config import embedding_store``). +# It's populated by the background init thread below. +# +# ``_embedding_stores`` is the per-graph cache used by chatbot +# retrievers via ``get_embedding_store(graphname=...)``. Each entry +# has its own ``TigerGraphConnection`` bound to that graphname for +# its lifetime — no in-place ``set_graphname`` mutation — so +# concurrent chat across different graphs can't race over a shared +# connection. +embedding_store = None +_embedding_store_ready = threading.Event() +_embedding_stores: dict = {} +_embedding_stores_lock = threading.Lock() +service_status["embedding_store"] = { + "status": "initializing", + "error": "Embedding store is still initializing", +} + + +def _build_embedding_store(graphname: str = "") -> TigerGraphEmbeddingStore: + """Construct a fresh ``TigerGraphEmbeddingStore`` bound to *graphname*. + + Uses the live globals (``db_config`` for the connection and + ``embedding_service`` for the model) so the result reflects the + current config. + """ conn = TigerGraphConnection( host=db_config.get("hostname", "http://tigergraph"), username=db_config.get("username", "tigergraph"), password=db_config.get("password", "tigergraph"), gsPort=db_config.get("gsPort", "14240"), restppPort=db_config.get("restppPort", "9000"), - graphname=db_config.get("graphname", ""), + graphname=graphname or db_config.get("graphname", ""), apiToken=db_config.get("apiToken", ""), ) if not db_config.get("apiToken") and db_config.get("getToken"): conn.getToken() - embedding_store = TigerGraphEmbeddingStore( + store = TigerGraphEmbeddingStore( conn, embedding_service, support_ai_instance=True, ) - service_status["embedding_store"] = {"status": "ok", "error": None} + if graphname: + # Runs the GDS check and per-graph vector-query install. + store.set_graphname(graphname) + return store + + +def _init_embedding_store(): + """Background thread target. Builds the default embedding store + without blocking module import — TigerGraph may be slow on first + connect, and we don't want app startup to wait on it. + """ + global embedding_store + try: + embedding_store = _build_embedding_store() + service_status["embedding_store"] = {"status": "ok", "error": None} + except Exception as e: + service_status["embedding_store"] = {"status": "error", "error": str(e)} + logger.error(f"Failed to initialize embedding store: {e}") + finally: + _embedding_store_ready.set() + + +def get_embedding_store(graphname: str | None = None, timeout: float = 0): + """Return an embedding store. + + Args: + graphname: When supplied, returns a per-graph instance built + and cached on first request (each cache entry has its own + connection bound to *graphname* for its lifetime). + timeout: Seconds to wait for the default-store init when + *graphname* is not supplied. Default 0 (non-blocking — + raises immediately if still initializing). + + Raises: + RuntimeError: if not yet ready, timed out, or initialization failed. + """ + if graphname: + with _embedding_stores_lock: + cached = _embedding_stores.get(graphname) + if cached is not None: + return cached + # Build outside the lock so first-time setup for one graph + # doesn't serialize first-time setup for another. + store = _build_embedding_store(graphname) + with _embedding_stores_lock: + existing = _embedding_stores.get(graphname) + if existing is not None: + return existing # racing thread won + _embedding_stores[graphname] = store + return store + + if not _embedding_store_ready.wait(timeout=timeout): + raise RuntimeError( + "Embedding store is still initializing. Please try again shortly." + ) + if embedding_store is None: + error = service_status.get("embedding_store", {}).get("error", "Unknown error") + raise RuntimeError(f"Embedding store failed to initialize: {error}") + return embedding_store + + +def reset_embedding_store() -> None: + """Drop the per-graph cache and the default store, then re-run the + background init so a config reload picks up the new + ``embedding_service`` and ``db_config``. Callers should swap the + inputs before calling. No-op when ``INIT_EMBED_STORE`` is disabled + (e.g. ECC). + """ + global embedding_store + if os.getenv("INIT_EMBED_STORE", "true") != "true": + return + with _embedding_stores_lock: + _embedding_stores.clear() + embedding_store = None + _embedding_store_ready.clear() + service_status["embedding_store"] = { + "status": "initializing", + "error": "Embedding store is still initializing", + } + threading.Thread(target=_init_embedding_store, daemon=True).start() + + +if os.getenv("INIT_EMBED_STORE", "true") == "true": + threading.Thread(target=_init_embedding_store, daemon=True).start() def reload_llm_config(new_llm_config: dict = None): @@ -550,6 +690,14 @@ def reload_llm_config(new_llm_config: dict = None): if svc_key in new_llm_config and "region_name" not in new_llm_config[svc_key]: new_llm_config[svc_key]["region_name"] = new_llm_config["region_name"] + # Inject top-level prompt_path into LLM-prompted service configs + # if missing. Embedding service is excluded — embedding models + # never load prompt files. + if "prompt_path" in new_llm_config: + for svc_key in ["completion_service", "multimodal_service", "chat_service"]: + if svc_key in new_llm_config and "prompt_path" not in new_llm_config[svc_key]: + new_llm_config[svc_key]["prompt_path"] = new_llm_config["prompt_path"] + new_completion_config = new_llm_config.get("completion_service") new_embedding_config = new_llm_config.get("embedding_service") @@ -595,6 +743,10 @@ def reload_llm_config(new_llm_config: dict = None): else: raise Exception("Embedding service not implemented") + # Clear per-graph cache + rebuild the default so callers don't + # keep references to the old embedding service. + reset_embedding_store() + return { "status": "success", "message": "LLM configuration reloaded successfully" @@ -645,6 +797,10 @@ def reload_db_config(new_db_config: dict = None): del db_config[k] db_config.update(new_db_config) + # Clear per-graph cache + rebuild the default so callers don't + # keep connections bound to the old credentials. + reset_embedding_store() + return { "status": "success", "message": "DB configuration reloaded successfully" diff --git a/common/db/retriever_render.py b/common/db/retriever_render.py new file mode 100644 index 0000000..c7567ab --- /dev/null +++ b/common/db/retriever_render.py @@ -0,0 +1,236 @@ +# Copyright (c) 2024-2026 TigerGraph, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +"""Render and install retrieval queries against the live domain schema.""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Callable, Iterable, Optional + +from common.db.schema_utils import gsql_output_error + +logger = logging.getLogger(__name__) + + +_RETRIEVER_DIR = "common/gsql/supportai/retrievers" + + +TEMPLATED_RETRIEVERS: tuple = ( + "GraphRAG_Hybrid_Search", + "GraphRAG_Hybrid_Vector_Search", + "GraphRAG_Community_Search", + "GraphRAG_Community_Vector_Search", +) + + +def _hop_edge_pattern(body: str, domain_edges: Iterable[str]) -> str: + """Append directed domain edges to the hybrid-walk hop pattern.""" + edges = sorted(set(e for e in domain_edges if e)) + if not edges: + return body + needle = "IS_AFTER>):e" + if needle not in body: + return body + addition = "|" + "|".join(f"{e}>" for e in edges) + return body.replace(needle, f"IS_AFTER>{addition}):e") + + +def _community_member_pattern( + body: str, + domain_vts: Iterable[str], + include_entity: bool, +) -> str: + """Expand the community-walk start type to include domain VTs.""" + vts = sorted(set(v for v in domain_vts if v)) + if not vts: + return body + types = (["Entity"] + vts) if include_entity else vts + member = types[0] if len(types) == 1 else "(" + "|".join(types) + ")" + needle = "CONTAINS_ENTITY>)- Entity:v -(IN_COMMUNITY>" + if needle not in body: + return body + return body.replace( + needle, + f"CONTAINS_ENTITY>)- {member}:v -(IN_COMMUNITY>", + ) + + +def resolve_include_entity( + graphrag_config_get, + has_domain_schema: bool, +) -> bool: + """Resolve effective ``retrieval_include_entity``. Default: ``False`` + when a schema exists, ``True`` otherwise. Explicit config wins. + """ + configured = graphrag_config_get("retrieval_include_entity") + if configured is None: + return not has_domain_schema + return bool(configured) + + +def render_retriever_body( + template_text: str, + *, + domain_vts: Iterable[str], + domain_edges: Iterable[str], + include_entity: bool, +) -> str: + """Apply every schema-aware substitution to one retriever body.""" + body = template_text + body = _hop_edge_pattern(body, domain_edges) + body = _community_member_pattern(body, domain_vts, include_entity=include_entity) + return body + + +def load_template(query_name: str, retriever_dir: str = _RETRIEVER_DIR) -> str: + return (Path(retriever_dir) / f"{query_name}.gsql").read_text() + + +def render_retrievers( + domain_vts: Iterable[str], + domain_edges: Iterable[str], + include_entity: bool, + retriever_dir: str = _RETRIEVER_DIR, +) -> dict: + """Return ``{query_name: rendered_body}`` for every templated retriever.""" + rendered: dict = {} + for q in TEMPLATED_RETRIEVERS: + try: + text = load_template(q, retriever_dir) + except FileNotFoundError: + logger.warning(f"render_retrievers: template not found for {q}, skipped") + continue + rendered[q] = render_retriever_body( + text, + domain_vts=domain_vts, + domain_edges=domain_edges, + include_entity=include_entity, + ) + return rendered + + +def _install_block(graphname: str, query_name: str, body: str) -> str: + return ( + f"USE GRAPH {graphname}\n" + f"{body}\n" + f"INSTALL QUERY {query_name}\n" + ) + + +def _summarize(out) -> str: + s = str(out) + s = s.replace("\n", " | ") + return s[:200] + + +def install_retrievers( + conn, + graphname: str, + domain_vts: Iterable[str], + domain_edges: Iterable[str], + include_entity: bool, + retriever_dir: str = _RETRIEVER_DIR, + progress: Optional["Callable[[str], None]"] = None, +) -> dict: + """Render and install every templated retriever (sync). + + *progress* is an optional callback invoked once per query with a + short status message; lets the caller surface per-query progress + in a UI (init dialog poll, etc.). + """ + rendered = render_retrievers( + domain_vts, domain_edges, include_entity, retriever_dir + ) + logger.info( + f"install_retrievers: graph={graphname} include_entity={include_entity} " + f"vts={len(list(domain_vts))} edges={len(list(domain_edges))} " + f"rendered={list(rendered.keys())}" + ) + # Group the four templated retrievers into two user-facing + # status messages — the text/vector variants of each family + # install back-to-back and a per-query message flickers too + # fast to be useful. The mapping is exhaustive over the + # current ``TEMPLATED_RETRIEVERS`` set; new entries fall + # through to a single "Installing retriever queries" message. + _GROUP_MESSAGE = { + "GraphRAG_Hybrid_Search": ("hybrid", "Installing hybrid retriever queries"), + "GraphRAG_Hybrid_Vector_Search": ("hybrid", "Installing hybrid retriever queries"), + "GraphRAG_Community_Search": ("community", "Installing community retriever queries"), + "GraphRAG_Community_Vector_Search": ("community", "Installing community retriever queries"), + } + + results: dict = {} + emitted_groups: set = set() + for query_name, body in rendered.items(): + if progress is not None: + group_key, group_msg = _GROUP_MESSAGE.get( + query_name, ("_other", "Installing retriever queries") + ) + if group_key not in emitted_groups: + try: + progress(group_msg) + except Exception: + pass + emitted_groups.add(group_key) + block = _install_block(graphname, query_name, body) + try: + out = conn.gsql(block) + results[query_name] = out + err = gsql_output_error(out) if isinstance(out, str) else None + if err: + logger.warning( + f"install_retrievers: {query_name} install reported " + f"errors: {_summarize(out)}" + ) + else: + logger.info( + f"install_retrievers: {query_name} OK: {_summarize(out)}" + ) + except Exception as e: + logger.error(f"install_retrievers: {query_name} install raised: {e}") + results[query_name] = f"ERROR: {e}" + return results + + +async def install_retrievers_async( + conn, + graphname: str, + domain_vts: Iterable[str], + domain_edges: Iterable[str], + include_entity: bool, + retriever_dir: str = _RETRIEVER_DIR, + sem: Optional["object"] = None, +) -> dict: + """Render and install every templated retriever (async).""" + rendered = render_retrievers( + domain_vts, domain_edges, include_entity, retriever_dir + ) + results: dict = {} + for query_name, body in rendered.items(): + block = _install_block(graphname, query_name, body) + try: + if sem is not None: + async with sem: + out = await conn.gsql(block) + else: + out = await conn.gsql(block) + results[query_name] = out + err = gsql_output_error(out) if isinstance(out, str) else None + if err: + logger.warning( + f"install_retrievers_async: {query_name} install " + f"reported errors: {str(out)[:300]}" + ) + except Exception as e: + logger.error( + f"install_retrievers_async: {query_name} install raised: {e}" + ) + results[query_name] = f"ERROR: {e}" + return results diff --git a/common/db/schema_extraction.py b/common/db/schema_extraction.py new file mode 100644 index 0000000..c1fe07c --- /dev/null +++ b/common/db/schema_extraction.py @@ -0,0 +1,345 @@ +# Copyright (c) 2024-2026 TigerGraph, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +"""Schema-extraction over sample documents (Phase 1, sample-doc path). + +The endpoint accepts up to N representative documents, this module +turns them into a single concatenated markdown blob and asks the LLM +to emit ``VERTEX`` / ``DIRECTED EDGE`` / ``UNDIRECTED EDGE`` +statements (the same GSQL form the *paste* path accepts), so both +sources funnel through ``schema_utils.parse_gsql_schema``. + +Prompt loading is delegated to +``common.llm_services.base_llm.LLM_Model.schema_extraction_prompt`` — +the same per-graph-override → provider-default resolution used by every +other customizable prompt. The prompt itself lives at +``/schema_extraction.txt`` with a per-graph override at +``configs/graph_configs//prompts/schema_extraction.txt``. +""" + +from __future__ import annotations + +import logging +import re +from typing import Iterable, List, Optional + +from langchain.prompts import PromptTemplate +from langchain_core.output_parsers import StrOutputParser + +from common.db.schema_utils import ( + GRAPHRAG_STRUCTURAL_EDGE_TYPES, + GRAPHRAG_STRUCTURAL_VERTEX_TYPES, + get_gsql_reserved_words, +) + +logger = logging.getLogger(__name__) + + +# Specific known model builds → context window in tokens. Matched by +# longest-prefix substring against the lowercased ``llm_model`` value. +# When a configured model hits this table, no warning is logged. +_MODEL_CONTEXT_TOKENS = { + # Anthropic Claude — Opus 4.7 1M is keyed first so its longer prefix + # wins over the 200K Opus 4.x default. + "claude-opus-4-7": 1_000_000, + "claude-opus-4": 200_000, + "claude-sonnet-4": 200_000, + "claude-haiku-4": 200_000, + "claude-3-5-sonnet": 200_000, + "claude-3-5-haiku": 200_000, + "claude-3-opus": 200_000, + "claude-3-sonnet": 200_000, + "claude-3-haiku": 200_000, + # OpenAI GPT-4 + "gpt-4o": 128_000, + "gpt-4-turbo": 128_000, + "gpt-4-1106": 128_000, + "gpt-4-0125": 128_000, + "gpt-4-32k": 32_000, + "gpt-4": 8_000, + # OpenAI GPT-3.5 + "gpt-3.5-turbo-16k": 16_000, + "gpt-3.5-turbo": 16_000, + "gpt-3.5": 4_000, + # Google Gemini + "gemini-1.5-pro": 1_000_000, + "gemini-1.5-flash": 1_000_000, + "gemini-1.0-pro": 32_000, + # Meta Llama + "llama-3.1": 128_000, + "llama-3": 8_000, + "llama-2": 4_000, +} +# Family-level fallbacks for unknown variants. When the specific-build +# table misses, the first matching family is used and a warning is +# logged so the operator knows the value is a guess. +_FAMILY_FALLBACK_TOKENS = [ + ("claude", 200_000), + ("gpt-4", 128_000), + ("gpt-3.5", 16_000), + ("gpt", 128_000), # unknown gpt-* — assume modern + ("gemini", 1_000_000), + ("llama", 128_000), # unknown llama — assume modern + ("mistral", 32_000), + ("mixtral", 32_000), + ("deepseek", 128_000), + ("qwen", 32_000), + ("titan", 32_000), + ("cohere", 128_000), + ("nova", 128_000), +] +_DEFAULT_CONTEXT_TOKENS_FALLBACK = 128_000 +# Tokens reserved for the prompt template, structural-types list, the +# reserved-words list, and the LLM's output. The remaining budget is +# spent on sample content. +_PROMPT_OVERHEAD_TOKENS = 4_000 +# Lower bound so unknown / tiny-context models still get *something*. +_MIN_SAMPLE_TOKENS = 1_000 +# Approximation used to convert the resolved token budget into the +# character budget that ``concatenate_samples`` consumes. English +# markdown averages ~4 chars/token. +_CHARS_PER_TOKEN = 4 + + +def _default_context_tokens(model_name: Optional[str]) -> int: + if not model_name: + logger.warning( + "schema_extraction: no llm_model configured; defaulting to %d tokens", + _DEFAULT_CONTEXT_TOKENS_FALLBACK, + ) + return _DEFAULT_CONTEXT_TOKENS_FALLBACK + name = model_name.lower() + # Longest-prefix substring match against the specific-build table. + for prefix in sorted(_MODEL_CONTEXT_TOKENS, key=len, reverse=True): + if prefix in name: + return _MODEL_CONTEXT_TOKENS[prefix] + # Specific table missed — pick a similar family and warn so the + # operator knows the value was guessed. + for family, tokens in _FAMILY_FALLBACK_TOKENS: + if family in name: + logger.warning( + "schema_extraction: model %r not in known-build table; " + "using %s-family default of %d tokens. Add it to " + "_MODEL_CONTEXT_TOKENS for an exact value.", + model_name, family, tokens, + ) + return tokens + logger.warning( + "schema_extraction: model %r unknown; using fallback default of %d tokens", + model_name, _DEFAULT_CONTEXT_TOKENS_FALLBACK, + ) + return _DEFAULT_CONTEXT_TOKENS_FALLBACK + + +def _resolve_sample_token_budget(llm_service) -> int: + """Pick the sample-text token budget from the LLM's configured + ``token_limit``, falling back to the model's default context window + when ``token_limit`` is not set. + + Reserves ``_PROMPT_OVERHEAD_TOKENS`` for the prompt scaffolding and + LLM output. Returns tokens — callers convert to characters at the + truncation boundary. + """ + cfg = getattr(llm_service, "config", None) or {} + token_budget = int(cfg.get("token_limit") or 0) + if token_budget <= 0: + token_budget = _default_context_tokens(cfg.get("llm_model")) + return max(token_budget - _PROMPT_OVERHEAD_TOKENS, _MIN_SAMPLE_TOKENS) + + +def _build_prompt(llm_service) -> PromptTemplate: + """Wrap *llm_service*'s ``schema_extraction_prompt`` text in a + ``PromptTemplate`` with the three required input variables. + """ + template_str = llm_service.schema_extraction_prompt + return PromptTemplate( + template=template_str, + input_variables=["samples", "structural_types", "tg_keywords"], + ) + + +def concatenate_samples( + samples: Iterable[dict], + max_tokens: int, +) -> str: + """Concatenate sample-doc markdown into a single blob, with each + document preceded by an ``# `` heading. + + The budget is expressed in *tokens*; this function converts to + characters internally at ~4 chars/token for ``len()``-based + truncation. The budget is distributed across files so every + uploaded sample contributes — files are not silently dropped when + the first file is large. Each file gets + ``remaining_budget // remaining_files`` characters of head sample; + if a file uses less, the leftover rolls forward to subsequent files. + + *samples* is an iterable of ``{"doc_id": str, "content": str}`` + dicts (the same shape ``extract_text_from_file_with_images_as_docs`` + returns). + """ + samples_list = list(samples) + n = len(samples_list) + if n == 0: + return "" + + max_chars = max_tokens * _CHARS_PER_TOKEN + parts: List[str] = [] + remaining_budget = max_chars + remaining_files = n + truncated_any = False + for s in samples_list: + doc_id = s.get("doc_id", "doc") + content = s.get("content", "") or "" + header = f"\n\n# {doc_id}\n\n" + per_file = remaining_budget // max(remaining_files, 1) + full = header + content + if len(full) > per_file: + truncated_any = True + chunk = full[:per_file] + parts.append(chunk) + remaining_budget -= len(chunk) + remaining_files -= 1 + + if truncated_any: + logger.warning( + "Schema-extraction samples truncated to fit %d-token budget across %d files", + max_tokens, + n, + ) + return "".join(parts).lstrip() + + +def render_type_hints_block( + vertex_hints: Optional[List[dict]] = None, + edge_hints: Optional[List[dict]] = None, +) -> str: + """Render structured type hints into a markdown block the LLM + can read. Empty inputs return an empty string so the prompt is + untouched when the user provides no hints. + + Each hint is a ``{"name": str, "description": str}`` dict. + Edge hints may additionally carry ``"fromType"`` and ``"toType"`` + when the user pinned a direction; the renderer emits + ``Name (From → To)`` in that case. + """ + def _row(h: dict, with_endpoints: bool) -> str: + name = (h.get("name") or "").strip() + if not name: + return "" + from_type = (h.get("fromType") or "").strip() if with_endpoints else "" + to_type = (h.get("toType") or "").strip() if with_endpoints else "" + desc = (h.get("description") or "").strip() + head = name + if from_type and to_type: + head = f"{name} ({from_type} → {to_type})" + return f"- {head}: {desc}" if desc else f"- {head}" + + def _block(items, label, action, with_endpoints): + rows = [r for r in (_row(h, with_endpoints) for h in items or []) if r] + if not rows: + return "" + return f"{label} {action}:\n" + "\n".join(rows) + + blocks = [] + v_block = _block( + vertex_hints, "Vertex types", + "to include if their instances appear in the documents", False, + ) + if v_block: + blocks.append(v_block) + e_block = _block( + edge_hints, "Edge types", + "to include if supported by the documents", True, + ) + if e_block: + blocks.append(e_block) + if not blocks: + return "" + return "## Suggested types\n\n" + "\n\n".join(blocks) + + +def _build_prompt_with_hints( + llm_service, hints_block: str +) -> tuple[PromptTemplate, str]: + """Build the prompt template, injecting *hints_block* before the + ``## Inputs`` section when non-empty. Falls back to appending if + no Inputs marker is found (defensive — the shipped default has it). + + Returns ``(prompt_template, full_template_text)`` so the caller + can persist the rendered text as a per-graph override after a + successful init. + """ + base = llm_service.schema_extraction_prompt + if hints_block: + m = re.search(r"^##\s*Inputs\b", base, re.MULTILINE) + if m: + template_str = base[: m.start()].rstrip() + "\n\n" + hints_block + "\n\n" + base[m.start():] + else: + template_str = base.rstrip() + "\n\n" + hints_block + "\n" + else: + template_str = base + return ( + PromptTemplate( + template=template_str, + input_variables=["samples", "structural_types", "tg_keywords"], + ), + template_str, + ) + + +def extract_schema_gsql( + llm_service, + samples: Iterable[dict], + max_tokens: Optional[int] = None, + vertex_hints: Optional[List[dict]] = None, + edge_hints: Optional[List[dict]] = None, +) -> tuple[str, str]: + """Run the schema-extraction prompt against *llm_service*. Returns + ``(gsql_text, rendered_prompt)``: the raw GSQL the model produced + (caller passes it to ``schema_utils.parse_gsql_schema``) and the + fully-rendered prompt template (so the caller can persist it as a + per-graph override after a successful init). + + *llm_service* must expose ``schema_extraction_prompt`` (from + :class:`common.llm_services.base_llm.LLM_Model`) and the standard + ``invoke_with_parser(prompt, parser, inputs, caller_name)`` entry + point. Per-graph prompt overrides are picked up automatically by + ``schema_extraction_prompt``'s resolution chain. + + When *max_tokens* is ``None`` (the production path), the sample + budget is resolved from ``llm_service.config.token_limit`` if set, + otherwise from the model's default context window. Tests can pass + an explicit *max_tokens* to pin behavior independently of config. + + *vertex_hints* / *edge_hints* are optional ``[{name, description}]`` + lists from the UI's TagInputs. When non-empty, a "Suggested types" + block is injected before the ``## Inputs`` section of the resolved + prompt so the LLM treats them as must-include candidates. + """ + if max_tokens is None: + max_tokens = _resolve_sample_token_budget(llm_service) + hints_block = render_type_hints_block(vertex_hints, edge_hints) + prompt, rendered_template = _build_prompt_with_hints(llm_service, hints_block) + samples_blob = concatenate_samples(samples, max_tokens=max_tokens) + structural_types = ", ".join( + sorted(GRAPHRAG_STRUCTURAL_VERTEX_TYPES | GRAPHRAG_STRUCTURAL_EDGE_TYPES) + ) + tg_keywords = ", ".join(sorted(get_gsql_reserved_words())) + + raw = llm_service.invoke_with_parser( + prompt, + StrOutputParser(), + { + "samples": samples_blob, + "structural_types": structural_types, + "tg_keywords": tg_keywords, + }, + caller_name="schema_extraction", + ) + gsql_text = raw.strip() if isinstance(raw, str) else str(raw).strip() + return gsql_text, rendered_template diff --git a/common/db/schema_utils.py b/common/db/schema_utils.py new file mode 100644 index 0000000..dc9c5af --- /dev/null +++ b/common/db/schema_utils.py @@ -0,0 +1,1857 @@ +# Copyright (c) 2024-2026 TigerGraph, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +""" +Schema proposal and persistence for the schema-aware initialize_graph flow. + +A "schema proposal" is the user-supplied (or LLM-derived) **domain** schema +the graph should adopt at init time, expressed as a small Python dict: + + { + "vertices": [ + {"name": "Company", "description": "..."}, + {"name": "Report", "description": "..."}, + ... + ], + "edges": [ + {"name": "PUBLISHES", + "description": "...", + "pairs": [("Company", "Report"), ("Company", "Filing")]}, + ... + ], + "domain_label": "Corporate Governance", # optional + } + +This module provides: + +* :data:`GRAPHRAG_STRUCTURAL_VERTEX_TYPES` / :data:`GRAPHRAG_STRUCTURAL_EDGE_TYPES` + — the GraphRAG-internal types that the user must not redefine. +* :func:`parse_gsql_schema` — permissive scanner that turns pasted GSQL + (``ADD VERTEX/EDGE`` statements *or* ``gsql ls`` output) into a proposal. +* :func:`emit_add_statements` — produce a list of ``ADD VERTEX/EDGE`` / + ``ALTER EDGE … ADD PAIR`` statements that bring an existing graph schema + up to the proposal (compare-and-add only; never drop). +* :func:`emit_preview_gsql` — render the proposal as a self-contained GSQL + block for the UI's "Preview as GSQL" tab. + +The module is intentionally dependency-light (regex, dataclasses, stdlib +only) so it's unit-testable without spinning up TigerGraph or the LLM. +""" + +from __future__ import annotations + +import re +import time +import uuid +from dataclasses import dataclass, field +from typing import Callable, Iterable, List, Optional, Sequence, Set, Tuple + + +# ----------------------------------------------------------------------------- +# Structural type registry +# ----------------------------------------------------------------------------- + +#: GraphRAG-internal vertex types. The user must not propose these as domain +#: types; the permissive parser silently drops any line that names one of +#: these (case-insensitive match). +GRAPHRAG_STRUCTURAL_VERTEX_TYPES: frozenset = frozenset({ + "Document", + "DocumentChunk", + "Entity", + "EntityType", + "RelationshipType", + "Content", + "Community", + "Image", +}) + + +#: GraphRAG-internal edge types. The user must not propose these as domain +#: types either. ``reverse_*`` companions are derived from ``WITH +#: REVERSE_EDGE=…`` declarations and shouldn't be hand-written. +GRAPHRAG_STRUCTURAL_EDGE_TYPES: frozenset = frozenset({ + "HAS_CONTENT", + "IS_HEAD_OF", + "HAS_TAIL", + "CONTAINS_ENTITY", + "MENTIONS_RELATIONSHIP", + "MENTIONS_ENTITY_TYPE", + "IS_AFTER", + "HAS_CHILD", + "ENTITY_HAS_TYPE", + "RELATIONSHIP", + "ENTITY_LINKS_TO", + "IN_COMMUNITY", + "LINKS_TO", + "HAS_PARENT", + "HAS_IMAGE", + "REFERENCES_IMAGE", +}) + + +# TigerGraph identifier pattern (graphs, jobs, vertex/edge types). Must +# match the route-level ``ValidGraphName`` regex so direct callers of +# the helpers below get the same protection the API layer enforces. +_GSQL_IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +_GSQL_RESERVED_CACHE: Optional[frozenset] = None + + +def get_gsql_reserved_words() -> frozenset: + """Return the GSQL reserved-keyword set sourced from + ``pyTigerGraph.TigerGraphConnection.getReservedKeywords()``. + + Memoized at first call. ``pyTigerGraph`` is a hard dependency of + this codebase, so an import failure here is a real configuration + error and we let it propagate. + """ + global _GSQL_RESERVED_CACHE + if _GSQL_RESERVED_CACHE is None: + from pyTigerGraph import TigerGraphConnection + + words = TigerGraphConnection.getReservedKeywords() + _GSQL_RESERVED_CACHE = ( + words if isinstance(words, frozenset) else frozenset(words) + ) + return _GSQL_RESERVED_CACHE + + +def is_reserved_word(name: str) -> bool: + """Return True if *name* (case-insensitive) collides with a GSQL + reserved word per pyTigerGraph. Used by the permissive parser to + drop names that would error at schema-change time anyway. + """ + if not name: + return False + return name.upper() in get_gsql_reserved_words() + + +#: Network / transport-level failure markers that ``conn.gsql()`` +#: surfaces as a string return rather than an exception. +_GSQL_TRANSPORT_FAILURE_MARKERS: tuple = ( + "Response ended prematurely", + "Connection refused", + "Connection reset", + "Read timed out", + "Internal Server Error", +) + + +#: Server-reported failure markers that ``conn.gsql()`` includes in +#: its string output without raising. Maintained locally — the +#: pyTigerGraph private helper ``_wrap_gsql_result`` is documented as +#: in flux upstream, so we don't depend on it. Keep this list aligned +#: with upstream's ``_GSQL_ERROR_PATTERNS`` when it stabilizes. +_GSQL_SERVER_ERROR_MARKERS: tuple = ( + 'Encountered "', + "SEMANTIC ERROR", + "Syntax Error", + "Failed to create", + "does not exist", + "is not a valid", + "already exists", + "Invalid syntax", +) + + +def gsql_output_error(output: str) -> Optional[str]: + """Return a short error description if *output* (the string returned + by ``pyTigerGraph.TigerGraphConnection.gsql()``) indicates failure, + else ``None``. + + Two layers, both checked locally so we don't depend on + pyTigerGraph private helpers: + + 1. Transport-level errors (``Response ended prematurely``, + ``Connection refused``, etc.) — pyTigerGraph surfaces these as + a string return rather than an exception. + 2. Server-reported errors (``SEMANTIC ERROR``, ``Failed to + create``, ``Invalid syntax``, etc.) — string markers in the + gsql output. + + Used by :func:`apply_proposal` to flip an "applied" return into + an error when the server reported a problem but pyTigerGraph + didn't raise. + """ + if not output: + return None + + folded = output.casefold() + for marker in _GSQL_TRANSPORT_FAILURE_MARKERS: + if marker.casefold() in folded: + idx = output.lower().find(marker.lower()) + snippet = output[max(0, idx - 40): idx + len(marker) + 200] + return f"GSQL transport error: {marker!r}. Excerpt: {snippet!r}" + + for marker in _GSQL_SERVER_ERROR_MARKERS: + if marker in output: + idx = output.find(marker) + snippet = output[max(0, idx - 40): idx + len(marker) + 200] + return f"GSQL server error: {snippet!r}" + + return None + + +def is_structural_type(name: str) -> bool: + """Return True if *name* (case-insensitive) is a GraphRAG structural + vertex or edge type, OR a ``reverse_*`` companion of one, OR a GSQL + reserved word that would fail at schema-change time. + """ + if not name: + return False + folded = name.casefold() + if folded.startswith("reverse_"): + return True + structural = {t.casefold() for t in GRAPHRAG_STRUCTURAL_VERTEX_TYPES} + structural |= {t.casefold() for t in GRAPHRAG_STRUCTURAL_EDGE_TYPES} + if folded in structural: + return True + return is_reserved_word(name) + + +# ----------------------------------------------------------------------------- +# Canonical proposal dataclass +# ----------------------------------------------------------------------------- + + +#: TigerGraph GSQL primitive attribute types we accept on proposals. +#: Anything else is dropped at parse time so the schema-change job +#: never receives a non-primitive type. +GSQL_PRIMITIVE_TYPES: frozenset = frozenset({ + "STRING", "INT", "UINT", "DOUBLE", "FLOAT", "BOOL", "DATETIME", +}) + + +@dataclass +class AttributeProposal: + """One ``(name, type)`` pair on a vertex or edge type.""" + + name: str + type: str = "STRING" + + def to_dict(self) -> dict: + return {"name": self.name, "type": self.type} + + +@dataclass +class VertexProposal: + name: str + description: str = "" + attributes: List[AttributeProposal] = field(default_factory=list) + + def to_dict(self) -> dict: + return { + "name": self.name, + "description": self.description, + "attributes": [a.to_dict() for a in self.attributes], + } + + +@dataclass +class EdgeProposal: + name: str + pairs: List[Tuple[str, str]] = field(default_factory=list) + description: str = "" + attributes: List[AttributeProposal] = field(default_factory=list) + # ``True`` for ``DIRECTED EDGE`` (default), ``False`` for + # ``UNDIRECTED EDGE``. Captured from the parser; propagated to the + # emitter so the schema-change job uses the right keyword and + # WITH-clause shape (undirected edges have no REVERSE_EDGE). + directed: bool = True + + def to_dict(self) -> dict: + return { + "name": self.name, + "description": self.description, + "pairs": [list(p) for p in self.pairs], + "attributes": [a.to_dict() for a in self.attributes], + "directed": self.directed, + } + + +@dataclass +class SchemaProposal: + """Canonical in-memory representation of a domain schema proposal.""" + + vertices: List[VertexProposal] = field(default_factory=list) + edges: List[EdgeProposal] = field(default_factory=list) + domain_label: Optional[str] = None + + # --- Construction helpers ----------------------------------------------- + + def add_vertex( + self, + name: str, + description: str = "", + attributes: Optional[Iterable[Tuple[str, str]]] = None, + ) -> VertexProposal: + existing = self.find_vertex(name) + if existing is not None: + if description and not existing.description: + existing.description = description + if attributes: + self._merge_attrs(existing.attributes, attributes) + return existing + v = VertexProposal(name=name, description=description) + if attributes: + self._merge_attrs(v.attributes, attributes) + self.vertices.append(v) + return v + + def add_edge_pair( + self, + name: str, + from_vt: str, + to_vt: str, + description: str = "", + attributes: Optional[Iterable[Tuple[str, str]]] = None, + directed: bool = True, + ) -> EdgeProposal: + existing = self.find_edge(name) + pair = (from_vt, to_vt) + if existing is None: + existing = EdgeProposal( + name=name, + pairs=[pair], + description=description, + directed=directed, + ) + if attributes: + self._merge_attrs(existing.attributes, attributes) + self.edges.append(existing) + else: + if pair not in existing.pairs: + existing.pairs.append(pair) + if description and not existing.description: + existing.description = description + if attributes: + self._merge_attrs(existing.attributes, attributes) + # If the same edge name appears twice with mismatched + # direction, prefer the first declaration's choice and + # log nothing — schema-change time will reject anyway. + return existing + + @staticmethod + def _merge_attrs( + target: List[AttributeProposal], + new_attrs: Iterable[Tuple[str, str]], + ) -> None: + """Merge new ``(name, type)`` tuples into *target*. Ignores + attributes whose name is already present (case-insensitive), + so the first declared type wins. Filters out attributes whose + type isn't a recognized GSQL primitive — those would error at + schema-change time, and we drop silently to keep the parser + permissive. + """ + existing_names = {a.name.casefold() for a in target} + for name, type_str in new_attrs: + if not name: + continue + if name.casefold() in existing_names: + continue + if type_str.upper() not in GSQL_PRIMITIVE_TYPES: + continue + target.append(AttributeProposal(name=name, type=type_str.upper())) + existing_names.add(name.casefold()) + + # --- Lookup helpers ----------------------------------------------------- + + def find_vertex(self, name: str) -> Optional[VertexProposal]: + folded = name.casefold() + return next( + (v for v in self.vertices if v.name.casefold() == folded), None + ) + + def find_edge(self, name: str) -> Optional[EdgeProposal]: + folded = name.casefold() + return next( + (e for e in self.edges if e.name.casefold() == folded), None + ) + + def vertex_names(self) -> Set[str]: + return {v.name for v in self.vertices} + + # --- Cleanup ------------------------------------------------------------ + + def drop_dangling_pairs(self) -> int: + """Remove ``(FROM, TO)`` pairs whose endpoints aren't in the + proposal's vertex set. Returns the number of pairs dropped. + Edges whose pair list becomes empty are removed entirely. + """ + names = {v.name for v in self.vertices} + names_folded = {n.casefold() for n in names} + dropped = 0 + kept_edges: List[EdgeProposal] = [] + for edge in self.edges: + kept_pairs: List[Tuple[str, str]] = [] + for src, tgt in edge.pairs: + if ( + src.casefold() in names_folded + and tgt.casefold() in names_folded + ): + kept_pairs.append((src, tgt)) + else: + dropped += 1 + if kept_pairs: + edge.pairs = kept_pairs + kept_edges.append(edge) + else: + dropped += 0 # whole edge dropped, not counted as a pair-drop + self.edges = kept_edges + return dropped + + # --- Serialization ------------------------------------------------------ + + def to_dict(self) -> dict: + out: dict = { + "vertices": [v.to_dict() for v in self.vertices], + "edges": [e.to_dict() for e in self.edges], + } + if self.domain_label: + out["domain_label"] = self.domain_label + return out + + @classmethod + def from_dict(cls, data: dict) -> "SchemaProposal": + prop = cls(domain_label=data.get("domain_label")) + for v in data.get("vertices", []) or []: + attrs = [ + (a.get("name", ""), a.get("type", "STRING")) + for a in v.get("attributes", []) or [] + ] + prop.add_vertex( + name=v["name"], + description=v.get("description", ""), + attributes=attrs, + ) + for e in data.get("edges", []) or []: + attrs = [ + (a.get("name", ""), a.get("type", "STRING")) + for a in e.get("attributes", []) or [] + ] + edge_directed = bool(e.get("directed", True)) + for pair in e.get("pairs", []) or []: + prop.add_edge_pair( + name=e["name"], + from_vt=pair[0], + to_vt=pair[1], + description=e.get("description", ""), + attributes=attrs, + directed=edge_directed, + ) + return prop + + +# ----------------------------------------------------------------------------- +# Permissive GSQL parser +# ----------------------------------------------------------------------------- + + +# A line that contains "VERTEX (...)" anywhere on it. +# Captures the name and (optionally) the parenthesized attribute list. +# Allows leading whitespace, optional dash, optional ADD prefix. +_VERTEX_LINE_RE = re.compile( + r""" + ^ # start of line (re.MULTILINE) + [\s\-]* # leading whitespace, optional dash + (?:add\s+)? # optional ADD + vertex # VERTEX + \s+ + (?P[A-Za-z_][A-Za-z0-9_]*) # type name + \s* + \( # opening paren of attribute list + (?P[^()]*) # attribute body (no nested parens) + \) # closing paren + """, + re.IGNORECASE | re.VERBOSE | re.MULTILINE | re.DOTALL, +) + + +# A line that contains "DIRECTED EDGE (...)" or +# "UNDIRECTED EDGE (...)". Captures the direction keyword (so +# the parser can preserve it on the proposal), the edge name, and the +# FROM/TO body. Attribute / WITH-clause text after the closing paren +# is intentionally not captured. +_EDGE_LINE_RE = re.compile( + r""" + ^ # start of line + [\s\-]* # leading whitespace, optional dash + (?:add\s+)? # optional ADD + (?Pdirected|undirected) # DIRECTED or UNDIRECTED + \s+edge + \s+ + (?P[A-Za-z_][A-Za-z0-9_]*) # edge name + \s* + \( # opening paren + (?P.*?) # FROM/TO body (non-greedy) + \) # closing paren + """, + re.IGNORECASE | re.VERBOSE | re.MULTILINE | re.DOTALL, +) + + +# Within an edge body, a single (FROM , TO ) clause. Multi-pair +# bodies are separated by `|`. +_EDGE_PAIR_RE = re.compile( + r""" + \bfrom\s+ + (?P[A-Za-z_][A-Za-z0-9_]*) + \s*,\s* + \bto\s+ + (?P[A-Za-z_][A-Za-z0-9_]*) + """, + re.IGNORECASE | re.VERBOSE, +) + + +# A single ``name `` token in an attribute body. +_ATTR_TOKEN_RE = re.compile( + r""" + \b + (?P[A-Za-z_][A-Za-z0-9_]*) + \s+ + (?PSTRING|INT|UINT|DOUBLE|FLOAT|BOOL|DATETIME) + \b + """, + re.IGNORECASE | re.VERBOSE, +) + + +# Strip ``PRIMARY_ID `` so the attribute +# scanner doesn't collect the id field. The system always auto-adds +# ``PRIMARY_ID id STRING``; user-supplied values are honored only if +# they appear as the literal PRIMARY_ID — otherwise they're treated +# as plain attributes. +_PRIMARY_ID_RE = re.compile( + r"\bPRIMARY_ID\b\s+[A-Za-z_][A-Za-z0-9_]*\s+(?:STRING|INT|UINT|DOUBLE|FLOAT|BOOL|DATETIME)", + re.IGNORECASE, +) + + +# Strip a ``FROM , TO `` clause so the attribute scanner doesn't +# accidentally pick up "FROM" / "TO" tokens or their vertex-type +# placeholders. Used when scanning edge attribute bodies. +_FROM_TO_CLAUSE_RE = re.compile( + r"\bfrom\s+[A-Za-z_][A-Za-z0-9_]*\s*,\s*to\s+[A-Za-z_][A-Za-z0-9_]*", + re.IGNORECASE, +) + + +def _extract_attributes(body: str, *, is_edge_body: bool) -> List[Tuple[str, str]]: + """Scan an attribute body and return ``(name, type)`` pairs that + look like primitive attribute declarations. Skips ``PRIMARY_ID`` + entries (the system auto-adds those) and, for edge bodies, FROM/TO + pair clauses. + """ + if not body: + return [] + cleaned = _PRIMARY_ID_RE.sub("", body) + if is_edge_body: + cleaned = _FROM_TO_CLAUSE_RE.sub("", cleaned) + seen: Set[str] = set() + out: List[Tuple[str, str]] = [] + for m in _ATTR_TOKEN_RE.finditer(cleaned): + name = m.group("name") + type_str = m.group("type").upper() + folded = name.casefold() + if folded in seen: + continue + # Skip GSQL keywords that may slip through (FROM/TO already + # stripped, but be defensive against other reserved tokens). + if folded in {"from", "to", "primary_id"}: + continue + # Drop attribute names that collide with GSQL reserved words + # (e.g. ``count``, ``min``, ``max``). The schema-change job + # would otherwise fail with "Encountered ',' ..." when TG's + # parser reads the keyword in attribute position. + if is_reserved_word(name): + continue + seen.add(folded) + out.append((name, type_str)) + return out + + +# Matches a comment block: +# * one or more `// ...` lines, or +# * a single `/* ... */` block. +# Used to find descriptions immediately preceding a VERTEX/EDGE line. +_COMMENT_BLOCK_RE = re.compile( + r""" + (?: + (?:^[ \t]*//[ \t]?(?P.*)$\n?)+ # one+ // lines + | + ^[ \t]*/\*(?P.*?)\*/[ \t]*\n # /* … */ block + ) + """, + re.MULTILINE | re.DOTALL | re.VERBOSE, +) + + +def _extract_description_for(text: str, decl_start: int) -> str: + """Return the comment block's text immediately preceding *decl_start* + in *text*, or an empty string if none is present. + + A comment block is one or more consecutive ``//`` line comments, or a + single ``/* … */`` block, separated from the declaration by at most + blank lines. + """ + # Walk backwards from decl_start over blank/whitespace-only lines. + cursor = decl_start + # Skip any whitespace immediately before the decl + while cursor > 0 and text[cursor - 1] in (" ", "\t"): + cursor -= 1 + # Walk back one or more blank lines + while cursor > 0 and text[cursor - 1] == "\n": + # Look at the line before this newline + prev_line_end = cursor - 1 + prev_line_start = text.rfind("\n", 0, prev_line_end) + 1 + prev_line = text[prev_line_start:prev_line_end] + if prev_line.strip() == "": + cursor = prev_line_start + continue + break + + # Now cursor points at the start of the line that's potentially a comment. + # Walk back over consecutive `//` lines collecting their bodies. + comment_lines: List[str] = [] + while cursor > 0: + line_end = cursor - 1 # newline before cursor + line_start = text.rfind("\n", 0, line_end) + 1 + line = text[line_start:line_end] + stripped = line.lstrip() + if stripped.startswith("//"): + comment_lines.insert(0, stripped[2:].lstrip()) + cursor = line_start + elif stripped.startswith("/*") or stripped.endswith("*/"): + # Try a /* … */ block ending on this line + block_end = text.rfind("*/", 0, cursor) + block_start = text.rfind("/*", 0, block_end) + if block_start == -1 or block_end == -1: + break + body = text[block_start + 2:block_end] + # Strip leading * on each line (typical /* * … */ style) + cleaned = re.sub(r"^\s*\*?\s?", "", body, flags=re.MULTILINE) + comment_lines.insert(0, cleaned.strip()) + break + else: + break + + return " ".join(s.strip() for s in comment_lines if s.strip()) + + +def parse_gsql_schema(text: str) -> SchemaProposal: + """Permissively scan *text* for ``VERTEX`` / ``DIRECTED EDGE`` + declarations and return a :class:`SchemaProposal`. + + The scanner ignores everything that doesn't match the two declaration + patterns: section headers (``Vertex Types:``, ``Edge Types:``), + ``Indexes:``, ``Queries:`` blocks, ``CREATE GRAPH`` / + ``INSTALL QUERY`` / ``ALTER`` lines, blank lines, etc. ``ADD`` + prefix and the ``- `` bullet from ``gsql ls`` output are both + accepted; ``;`` terminators are tolerated. + + Lines naming a structural type (case-insensitive) are silently dropped. + ``reverse_*`` edges (auto-generated by ``WITH REVERSE_EDGE=…``) are + silently dropped. ``(FROM, TO)`` pairs whose endpoints don't resolve + to a vertex extracted from the same payload are dropped after parsing + (see :meth:`SchemaProposal.drop_dangling_pairs`). + """ + proposal = SchemaProposal() + + # Pass 1: vertices + for m in _VERTEX_LINE_RE.finditer(text): + name = m.group("name") + if is_structural_type(name): + continue + desc = _extract_description_for(text, m.start()) + attrs = _extract_attributes(m.group("body") or "", is_edge_body=False) + proposal.add_vertex(name=name, description=desc, attributes=attrs) + + # Pass 2: edges + for m in _EDGE_LINE_RE.finditer(text): + name = m.group("name") + if is_structural_type(name): + continue + if name.lower().startswith("reverse_"): + continue + body = m.group("body") or "" + desc = _extract_description_for(text, m.start()) + attrs = _extract_attributes(body, is_edge_body=True) + directed = (m.group("dir") or "directed").lower() == "directed" + for pm in _EDGE_PAIR_RE.finditer(body): + from_vt = pm.group("from") + to_vt = pm.group("to") + if is_structural_type(from_vt) or is_structural_type(to_vt): + # Either endpoint is a structural type, a reverse_* + # auto-generated companion, or a GSQL reserved word — + # the pair would be invalid as a user-declared domain + # edge. ``drop_dangling_pairs`` would catch it later + # anyway; rejecting here keeps the proposal free of + # transient invalid state. + continue + proposal.add_edge_pair( + name=name, + from_vt=from_vt, + to_vt=to_vt, + description=desc, + attributes=attrs, + directed=directed, + ) + + # Filter dangling pairs (FROM/TO that don't resolve to a vertex we + # actually extracted from the same payload). + proposal.drop_dangling_pairs() + return proposal + + +# ----------------------------------------------------------------------------- +# GSQL emission +# ----------------------------------------------------------------------------- + + +@dataclass +class ExistingSchema: + """Snapshot of what's already on the graph, used by the diff emitter. + + ``vertex_types`` — vertex-type names currently on the graph. + ``edge_pairs`` — edge-type name → set of ``(FROM, TO)`` pairs. + ``directed_edges`` — subset of edge-type names with + ``IsDirected=True`` (consumed by the retriever renderer). + """ + + vertex_types: Set[str] = field(default_factory=set) + edge_pairs: dict = field(default_factory=dict) + directed_edges: Set[str] = field(default_factory=set) + + def has_vertex(self, name: str) -> bool: + folded = name.casefold() + return any(v.casefold() == folded for v in self.vertex_types) + + def has_edge(self, name: str) -> bool: + return name in self.edge_pairs or any( + k.casefold() == name.casefold() for k in self.edge_pairs + ) + + def has_edge_pair(self, name: str, from_vt: str, to_vt: str) -> bool: + # Edge name lookup is case-insensitive + edge_key = next( + (k for k in self.edge_pairs if k.casefold() == name.casefold()), + None, + ) + if edge_key is None: + return False + for src, tgt in self.edge_pairs.get(edge_key, set()): + if src.casefold() == from_vt.casefold() and tgt.casefold() == to_vt.casefold(): + return True + return False + + +@dataclass +class AllowedSchema: + """Domain-schema bundle handed to the LLM entity/relationship + extractor. Carries one text rendering for the LLM prompt and the + structured maps the worker layer uses for runtime coercion and + endpoint validation. + + All fields exclude GraphRAG structural types — only user-declared + domain types reach the extractor. + + Fields: + schema_rep — rendered schema text suitable for an LLM prompt + (vertex types with attributes, edge types with endpoints, + inline definitions). Reuses the same shape that + ``render_schema_rep`` produces for query-side tools. + vertex_types / edge_types — name lists for fast allow-checks. + vertex_attributes / edge_attributes — ``{type: {attr: tg_type}}`` + for typed-attribute coercion at upsert time. + vertex_definitions / edge_definitions — ``{type: description}`` + from EntityType / RelationshipType meta-vertices. + edge_endpoints — ``{edge: [(from_vt, to_vt), ...]}`` for the + worker's endpoint-pair validation. + """ + + schema_rep: str = "" + schema_version: Optional[int] = None + vertex_types: List[str] = field(default_factory=list) + edge_types: List[str] = field(default_factory=list) + vertex_attributes: dict = field(default_factory=dict) + edge_attributes: dict = field(default_factory=dict) + vertex_definitions: dict = field(default_factory=dict) + edge_definitions: dict = field(default_factory=dict) + edge_endpoints: dict = field(default_factory=dict) + + +# TG accepts only these types inside a DISCRIMINATOR(...) clause. +# DOUBLE / FLOAT / BOOL are rejected at schema-change time. +_DISCRIMINATOR_TYPES = frozenset({"INT", "UINT", "STRING", "DATETIME"}) + + +def _default_literal(tg_type: str) -> str: + """Return the GSQL literal for the per-type default — used inside + ``DISCRIMINATOR(... DEFAULT )`` clauses so the column is + non-nullable but per-instance upserts that omit the value still + succeed (the omitted attribute falls to the default). + """ + t = (tg_type or "").upper() + if t in ("INT", "UINT"): + return "0" + if t in ("DOUBLE", "FLOAT"): + return "0.0" + if t == "BOOL": + return "false" + if t == "DATETIME": + return '"1970-01-01 00:00:00"' + return '""' + + +def emit_add_statements( + proposal: SchemaProposal, + existing: Optional[ExistingSchema] = None, +) -> List[str]: + """Diff *proposal* against *existing* and return a list of GSQL + statements (sans trailing ``;``) that, when run inside a + ``SCHEMA_CHANGE JOB`` against a graph in the *existing* state, bring + the graph up to the proposal. + + Order is deterministic and dependency-safe: + + 1. ``ADD VERTEX (PRIMARY_ID id STRING) WITH PRIMARY_ID_AS_ATTRIBUTE="true"`` + for every domain vertex type that doesn't already exist. + 2. ``ADD DIRECTED EDGE (FROM , TO [| FROM …]) WITH REVERSE_EDGE="reverse_"`` + for every domain edge type that doesn't exist on the graph at all. + 3. ``ALTER EDGE ADD PAIR (FROM , TO )`` for every + ``(FROM, TO)`` pair on an existing edge type that's missing. + + No ``DROP``s are ever emitted — the diff is strictly additive. + """ + if existing is None: + existing = ExistingSchema() + + stmts: List[str] = [] + + # 1. New vertex types + for v in proposal.vertices: + if existing.has_vertex(v.name): + continue + attrs_part = "" + if v.attributes: + attrs_part = ", " + ", ".join( + f"{a.name} {a.type}" for a in v.attributes + ) + stmts.append( + f'ADD VERTEX {v.name} (PRIMARY_ID id STRING{attrs_part}) ' + f'WITH PRIMARY_ID_AS_ATTRIBUTE="true"' + ) + + # 2 + 3. Edges: fully new, or new pairs on an existing edge + for e in proposal.edges: + if not e.pairs: + continue + if not existing.has_edge(e.name): + pairs_str = " | ".join( + f"FROM {src}, TO {tgt}" for src, tgt in e.pairs + ) + # Promote discriminator-eligible attributes (per TG: INT, + # UINT, STRING, DATETIME) into a ``DISCRIMINATOR(...)`` + # clause with type defaults. Other attribute types stay as + # regular nullable columns outside the clause. + disc_attrs = [a for a in e.attributes if a.type.upper() in _DISCRIMINATOR_TYPES] + plain_attrs = [a for a in e.attributes if a.type.upper() not in _DISCRIMINATOR_TYPES] + parts: List[str] = [] + if disc_attrs: + parts.append("DISCRIMINATOR(" + ", ".join( + f"{a.name} {a.type} DEFAULT {_default_literal(a.type)}" + for a in disc_attrs + ) + ")") + parts.extend(f"{a.name} {a.type}" for a in plain_attrs) + attrs_part = (", " + ", ".join(parts)) if parts else "" + edge_kw = "DIRECTED EDGE" if e.directed else "UNDIRECTED EDGE" + # Undirected edges have no reverse companion, so omit the + # WITH REVERSE_EDGE clause. + with_clause = ( + f' WITH REVERSE_EDGE="reverse_{e.name}"' if e.directed else "" + ) + stmts.append( + f'ADD {edge_kw} {e.name} ({pairs_str}{attrs_part}){with_clause}' + ) + else: + # Existing edge: only ALTER ADD PAIR is supported here. + # Adding attributes on an existing edge needs a separate + # ALTER ATTRIBUTE statement and is out of scope for this + # additive diff. + for src, tgt in e.pairs: + if existing.has_edge_pair(e.name, src, tgt): + continue + stmts.append( + f"ALTER EDGE {e.name} ADD PAIR (FROM {src}, TO {tgt})" + ) + + return stmts + + +def emit_structural_link_alters( + proposal: SchemaProposal, + existing: Optional[ExistingSchema] = None, +) -> List[str]: + """For every domain vertex in *proposal*, emit ``ALTER EDGE … ADD + PAIR`` statements that connect it to the GraphRAG core via the + structural edges: + + * ``CONTAINS_ENTITY`` — ``Document`` / ``DocumentChunk`` → domain vertex + * ``IN_COMMUNITY`` — domain vertex → ``Community`` (so the + post-Louvain mirror step can attach domain-VT instances to the + community their twin Entity belongs to, and retrievers walking + domain VTs can reach community memberships directly) + + The typed-relationship pattern (``IS_HEAD_OF`` / ``HAS_TAIL``) lives + at the meta-schema layer (``EntityType`` ↔ ``RelationshipType``) and + does NOT need per-domain-vertex pairs. The original schema + declaration covers the only pairs we ever traverse. + + Pairs already on the graph (per *existing*) are skipped. The + statements are returned in a deterministic order so the schema + diff is reproducible. + """ + if existing is None: + existing = ExistingSchema() + + # Skip the structural-link emit entirely when the GraphRAG core + # types aren't on the graph — without them the ALTER would + # reference an undeclared endpoint and fail. In production these + # are always present by the time apply_proposal runs (init_supportai + # creates the structural schema first), but unit tests and + # bare-graph fixtures may not have them. + has_doc = existing.has_vertex("Document") + has_chunk = existing.has_vertex("DocumentChunk") + has_community = existing.has_vertex("Community") + + stmts: List[str] = [] + for v in proposal.vertices: + # CONTAINS_ENTITY: Document / DocumentChunk → + if has_doc and not existing.has_edge_pair("CONTAINS_ENTITY", "Document", v.name): + stmts.append( + f"ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM Document, TO {v.name})" + ) + if has_chunk and not existing.has_edge_pair("CONTAINS_ENTITY", "DocumentChunk", v.name): + stmts.append( + f"ALTER EDGE CONTAINS_ENTITY ADD PAIR (FROM DocumentChunk, TO {v.name})" + ) + # IN_COMMUNITY: → Community + if has_community and not existing.has_edge_pair("IN_COMMUNITY", v.name, "Community"): + stmts.append( + f"ALTER EDGE IN_COMMUNITY ADD PAIR (FROM {v.name}, TO Community)" + ) + return stmts + + +def emit_preview_gsql(proposal: SchemaProposal) -> str: + """Render *proposal* as a self-contained GSQL block suitable for the + UI's "Preview as GSQL" tab. Comments above each declaration carry + the description, when set. + """ + lines: List[str] = [] + if proposal.domain_label: + lines.append(f"// Domain: {proposal.domain_label}") + lines.append("") + + for v in proposal.vertices: + if v.description: + lines.append(f"// {v.description}") + attrs_part = "" + if v.attributes: + attrs_part = ", " + ", ".join( + f"{a.name} {a.type}" for a in v.attributes + ) + lines.append( + f'ADD VERTEX {v.name} (PRIMARY_ID id STRING{attrs_part}) ' + f'WITH PRIMARY_ID_AS_ATTRIBUTE="true";' + ) + lines.append("") + + for e in proposal.edges: + if not e.pairs: + continue + if e.description: + lines.append(f"// {e.description}") + pairs_str = " | ".join(f"FROM {src}, TO {tgt}" for src, tgt in e.pairs) + attrs_part = "" + if e.attributes: + attrs_part = ", " + ", ".join( + f"{a.name} {a.type}" for a in e.attributes + ) + edge_kw = "DIRECTED EDGE" if e.directed else "UNDIRECTED EDGE" + with_clause = ( + f' WITH REVERSE_EDGE="reverse_{e.name}"' if e.directed else "" + ) + lines.append( + f'ADD {edge_kw} {e.name} ({pairs_str}{attrs_part}){with_clause};' + ) + lines.append("") + + return "\n".join(lines).rstrip() + "\n" + + +# ----------------------------------------------------------------------------- +# TigerGraph-side schema reader +# ----------------------------------------------------------------------------- + + +def read_existing_schema(conn) -> ExistingSchema: + """Read the current vertex / edge schema from a TigerGraph + connection and return an :class:`ExistingSchema` snapshot suitable + for :func:`emit_add_statements`. + + Works with both ``pyTigerGraph.TigerGraphConnection`` and our + ``TigerGraphConnectionProxy`` wrapper. Only the synchronous + ``getVertexTypes`` / ``getEdgeTypes`` / ``getEdgeType`` API is used. + + Edge pairs are extracted from the edge-type metadata returned by + pyTigerGraph. For single-pair edges the metadata exposes + ``FromVertexTypeName`` / ``ToVertexTypeName`` directly. For + multi-pair edges (where those fields are ``"*"``) the metadata + contains an ``EdgePairs`` list of ``{"From": ..., "To": ...}`` + dicts. We accept both shapes. + + Errors during schema introspection are not swallowed — the caller + needs to know if the snapshot is incomplete before diffing. If the + graph hasn't been initialized at all (no vertex types yet), + pyTigerGraph returns an empty list, which produces an + ``ExistingSchema`` with empty ``vertex_types`` / ``edge_pairs`` + (the diff emitter then emits a full ``ADD`` for everything in the + proposal — which is the desired behavior on a fresh graph). + """ + snapshot = ExistingSchema() + + vertex_types = conn.getVertexTypes() or [] + snapshot.vertex_types = set(vertex_types) + + for et_name in conn.getEdgeTypes() or []: + meta = conn.getEdgeType(et_name) or {} + pairs: Set[Tuple[str, str]] = set() + + from_v = meta.get("FromVertexTypeName") + to_v = meta.get("ToVertexTypeName") + if from_v and to_v and from_v != "*" and to_v != "*": + pairs.add((from_v, to_v)) + + # Multi-pair edges: an EdgePairs list either always (some TG + # versions) or only when From/To are "*" (other versions). + for ep in meta.get("EdgePairs", []) or []: + f = ep.get("From") + t = ep.get("To") + if f and t: + pairs.add((f, t)) + + if pairs: + snapshot.edge_pairs[et_name] = pairs + if meta.get("IsDirected"): + snapshot.directed_edges.add(et_name) + + return snapshot + + +# ----------------------------------------------------------------------------- +# Atomic apply +# ----------------------------------------------------------------------------- + + +def build_schema_change_job( + graphname: str, + statements: Sequence[str], + job_name: Optional[str] = None, +) -> Tuple[str, str]: + """Wrap *statements* into a single ``CREATE SCHEMA_CHANGE JOB`` / + ``RUN`` / ``DROP`` GSQL block for *graphname*. + + Returns ``(gsql_block, job_name)``. The job name is generated with a + short uuid suffix so re-runs against the same graph don't collide + with a previously-created (but never dropped) job. + + The returned block is intended to be passed verbatim to + ``conn.gsql(...)``; running every ``ADD`` / ``ALTER`` inside one job + is what makes the application atomic. + """ + if not statements: + raise ValueError("build_schema_change_job: statements is empty") + if not _GSQL_IDENT_RE.fullmatch(graphname): + raise ValueError(f"Invalid graph name: {graphname!r}") + if job_name is None: + job_name = f"add_domain_schema_{uuid.uuid4().hex[:8]}" + elif not _GSQL_IDENT_RE.fullmatch(job_name): + raise ValueError(f"Invalid job name: {job_name!r}") + + body = ";\n ".join(s.rstrip(";") for s in statements) + ";" + block = ( + f"USE GRAPH {graphname}\n" + f"CREATE SCHEMA_CHANGE JOB {job_name} FOR GRAPH {graphname} {{\n" + f" {body}\n" + f"}}\n" + f"RUN SCHEMA_CHANGE JOB {job_name}\n" + f"DROP JOB {job_name}" + ) + return block, job_name + + +def read_type_metadata(conn) -> Tuple[dict, dict]: + """Read every ``EntityType`` / ``RelationshipType`` vertex from + *conn* and return two dicts: + + ( + {entity_type_id: description}, + {relationship_type_id: definition}, + ) + + Empty / missing values are dropped so callers can ``.get(name, "")`` + without distinguishing "no row" from "row with empty description". + Errors propagate — callers needing best-effort behavior should wrap + in their own try/except. + """ + entity_descs: dict = {} + rel_defs: dict = {} + + try: + rows = conn.getVertices("EntityType") or [] + except Exception: + rows = [] + for row in rows: + attrs = row.get("attributes", row) + v_id = row.get("v_id") or attrs.get("id") + desc = (attrs.get("description") or "").strip() + if v_id and desc: + entity_descs[v_id] = desc + + try: + rows = conn.getVertices("RelationshipType") or [] + except Exception: + rows = [] + for row in rows: + attrs = row.get("attributes", row) + v_id = row.get("v_id") or attrs.get("id") + defn = (attrs.get("definition") or "").strip() + if v_id and defn: + rel_defs[v_id] = defn + + return entity_descs, rel_defs + + +async def read_existing_schema_async(conn) -> "ExistingSchema": + """Async counterpart to :func:`read_existing_schema` — used by the + ECC pipeline where ``conn`` is an ``AsyncTigerGraphConnection``. + + Returns a raw :class:`ExistingSchema` snapshot. Callers that want + to filter structural types (e.g. for domain-only consumers like + the extractor builder) should use :func:`is_structural_type` on + the returned vertex / edge names — this helper deliberately does + not couple the live-schema read to the proposal-time concept of + "domain vs structural", so live-schema consumers stay independent + of the proposal lifecycle. + """ + snapshot = ExistingSchema() + snapshot.vertex_types = set(await conn.getVertexTypes() or []) + for et_name in await conn.getEdgeTypes() or []: + meta = await conn.getEdgeType(et_name) or {} + pairs: Set[Tuple[str, str]] = set() + from_v = meta.get("FromVertexTypeName") + to_v = meta.get("ToVertexTypeName") + if from_v and to_v and from_v != "*" and to_v != "*": + pairs.add((from_v, to_v)) + for ep in meta.get("EdgePairs", []) or []: + f = ep.get("From") + t = ep.get("To") + if f and t: + pairs.add((f, t)) + if pairs: + snapshot.edge_pairs[et_name] = pairs + if meta.get("IsDirected"): + snapshot.directed_edges.add(et_name) + return snapshot + + +def _assemble_schema_rep( + *, + graphname: str, + schema_ver: Optional[int], + vertex_blocks: List[str], + edge_blocks: List[str], + exclude_structural: bool, + domain_verts: List[str], + domain_edge_types: List[str], + vertex_attributes: dict, + edge_attributes: dict, + entity_descs: dict, + rel_defs: dict, + edge_endpoints: dict, +) -> AllowedSchema: + """Bundle pre-computed blocks into an ``AllowedSchema``. Shared by + the sync and async builders so both paths produce identical output. + """ + if exclude_structural and not domain_verts and not domain_edge_types: + return AllowedSchema(schema_version=schema_ver) + graph_label = f" {graphname}" if graphname else "" + qualifier = "domain " if exclude_structural else "" + text = ( + f"The {qualifier}schema of the graph{graph_label} is as follows:\n" + f"Vertex Types:\n{chr(10).join(vertex_blocks) if vertex_blocks else '(none)'}" + f"\n\nEdge Types:\n{chr(10).join(edge_blocks) if edge_blocks else '(none)'}\n" + ) + domain_entity_defs = {v: entity_descs[v] for v in domain_verts if entity_descs.get(v)} + domain_rel_defs = {e: rel_defs[e] for e in domain_edge_types if rel_defs.get(e)} + return AllowedSchema( + schema_rep=text, + schema_version=schema_ver, + vertex_types=domain_verts, + edge_types=domain_edge_types, + vertex_attributes=vertex_attributes, + edge_attributes=edge_attributes, + vertex_definitions=domain_entity_defs, + edge_definitions=domain_rel_defs, + edge_endpoints=edge_endpoints, + ) + + +def render_schema_rep(conn, exclude_structural: bool = False) -> AllowedSchema: + """Read the live schema and return a full :class:`AllowedSchema` + bundle (rendered text + structured maps + version). + + Used by both query-side tools (``generate_cypher`` / ``generate_gsql`` + / ``map_question_to_schema``) and the ECC entity extractor. Pass + ``exclude_structural=True`` to drop GraphRAG structural types + (Entity, Document, Community, structural edges, etc.) — the + extractor uses this mode; query-side tools use the default so the + LLM sees the full graph including bookkeeping types. + + Returns an :class:`AllowedSchema` with at least ``schema_version`` + populated; when the graph has no types yet, the other fields stay + empty. + """ + from common.db.connections import get_schema_ver as _get_schema_ver + schema_ver = _get_schema_ver(conn) + + try: + entity_descs, rel_defs = read_type_metadata(conn) + except Exception: + # Older / unmigrated graphs may lack the EntityType / + # RelationshipType meta-schema; render without definitions + # rather than failing. + entity_descs, rel_defs = {}, {} + + try: + all_verts = conn.getVertexTypes() or [] + except Exception: + all_verts = [] + domain_verts = ( + [v for v in all_verts if not is_structural_type(v)] + if exclude_structural else list(all_verts) + ) + + vertex_attributes: dict = {} + vertex_blocks: List[str] = [] + for vert in sorted(domain_verts): + try: + vinfo = conn.getVertexType(vert) or {} + except Exception: + continue + primary_id_name = (vinfo.get("PrimaryId") or {}).get("AttributeName", "") + attrs_map, attr_lines = _collect_attrs(vinfo.get("Attributes"), primary_id_name) + vertex_attributes[vert] = attrs_map + defn_line = ( + f"\n\tDefinition: {entity_descs[vert]}" if entity_descs.get(vert) else "" + ) + attrs_block = "\n\t\t".join(attr_lines) or "No attributes" + vertex_blocks.append( + f"{vert}{defn_line}\n\tPrimary Id Attribute: {primary_id_name}" + f"\n\tAttributes: \n\t\t{attrs_block}" + ) + + try: + all_edges = conn.getEdgeTypes() or [] + except Exception: + all_edges = [] + edge_attributes: dict = {} + edge_endpoints: dict = {} + edge_blocks: List[str] = [] + domain_edge_types: List[str] = [] + for edge in sorted(all_edges): + if exclude_structural and is_structural_type(edge): + continue + try: + einfo = conn.getEdgeType(edge) or {} + except Exception: + continue + pairs = _collect_edge_pairs(einfo, exclude_structural) + if exclude_structural and not pairs: + continue + domain_edge_types.append(edge) + edge_endpoints[edge] = pairs + attrs_map, attr_lines = _collect_attrs(einfo.get("Attributes"), "") + edge_attributes[edge] = attrs_map + direction = "Directed" if einfo.get("IsDirected") else "Undirected" + defn_line = ( + f"\n\tDefinition: {rel_defs[edge]}" if rel_defs.get(edge) else "" + ) + attrs_block = "\n\t\t".join(attr_lines) or "No attributes" + # Emit one block per (FROM, TO) pair — keeps the rendered + # text single-pair-per-block. + for src, tgt in pairs: + pair_info = f"From Vertex: {src}\n\tTo Vertex: {tgt}" + edge_blocks.append( + f"{edge}{defn_line}\n\t{pair_info}" + f"\n\tEdge direction: {direction}" + f"\n\tAttributes: \n\t\t{attrs_block}" + ) + + return _assemble_schema_rep( + graphname=getattr(conn, "graphname", "") or "", + schema_ver=schema_ver, + vertex_blocks=vertex_blocks, + edge_blocks=edge_blocks, + exclude_structural=exclude_structural, + domain_verts=sorted(domain_verts) if exclude_structural else list(all_verts), + domain_edge_types=domain_edge_types, + vertex_attributes=vertex_attributes, + edge_attributes=edge_attributes, + entity_descs=entity_descs, + rel_defs=rel_defs, + edge_endpoints=edge_endpoints, + ) + + +def _collect_attrs(attr_list, skip_name: str) -> Tuple[dict, List[str]]: + """Walk an ``Attributes`` array from ``getVertexType`` / + ``getEdgeType`` and return ``({attr_name: tg_type}, ["name of type + type", ...])``. ``skip_name`` is the primary-id attribute that + shouldn't appear in the user-facing schema rep. + """ + attrs_map: dict = {} + lines: List[str] = [] + for a in attr_list or []: + a_name = a.get("AttributeName") + a_type = ((a.get("AttributeType") or {}).get("Name")) or "STRING" + if not a_name or a_name == skip_name: + continue + attrs_map[a_name] = a_type + lines.append(f"{a_name} of type {a_type}") + return attrs_map, lines + + +def _collect_edge_pairs(einfo: dict, exclude_structural: bool) -> List[Tuple[str, str]]: + """Build the (FROM, TO) pair list for an edge, filtering out pairs + whose endpoint is a structural type when ``exclude_structural`` is + set. Used by both schema-rep paths. + """ + pairs: List[Tuple[str, str]] = [] + from_v = einfo.get("FromVertexTypeName") + to_v = einfo.get("ToVertexTypeName") + if from_v and to_v and from_v != "*" and to_v != "*": + if not (exclude_structural and (is_structural_type(from_v) or is_structural_type(to_v))): + pairs.append((from_v, to_v)) + for ep in einfo.get("EdgePairs", []) or []: + f, t = ep.get("From"), ep.get("To") + if not (f and t): + continue + if exclude_structural and (is_structural_type(f) or is_structural_type(t)): + continue + pairs.append((f, t)) + return pairs + + +# Backwards-compatible alias for callers that still want the old name. +# ``render_schema_rep(conn, exclude_structural=True)`` is the canonical +# spelling; keep this until call sites migrate. +def build_allowed_schema(conn) -> AllowedSchema: + """Back-compat alias for ``render_schema_rep(conn, exclude_structural=True)``.""" + return render_schema_rep(conn, exclude_structural=True) + + +async def render_schema_rep_async( + conn, exclude_structural: bool = False, +) -> AllowedSchema: + """Async counterpart to :func:`render_schema_rep`. Used by the ECC + pipeline where ``conn`` is an ``AsyncTigerGraphConnection`` (whose + ``getVertexType`` / ``getEdgeType`` are coroutines). + + Same semantics as the sync version — see :func:`render_schema_rep`. + """ + from common.db.connections import get_schema_ver as _get_schema_ver + + try: + schema_ver = _get_schema_ver(conn) + except Exception: + schema_ver = None + + try: + entity_descs, rel_defs = await read_type_metadata_async(conn) + except Exception: + entity_descs, rel_defs = {}, {} + + try: + all_verts = await conn.getVertexTypes() or [] + except Exception: + all_verts = [] + domain_verts = ( + [v for v in all_verts if not is_structural_type(v)] + if exclude_structural else list(all_verts) + ) + + vertex_attributes: dict = {} + vertex_blocks: List[str] = [] + for vert in sorted(domain_verts): + try: + vinfo = await conn.getVertexType(vert) or {} + except Exception: + continue + primary_id_name = (vinfo.get("PrimaryId") or {}).get("AttributeName", "") + attrs_map, attr_lines = _collect_attrs(vinfo.get("Attributes"), primary_id_name) + vertex_attributes[vert] = attrs_map + defn_line = ( + f"\n\tDefinition: {entity_descs[vert]}" if entity_descs.get(vert) else "" + ) + attrs_block = "\n\t\t".join(attr_lines) or "No attributes" + vertex_blocks.append( + f"{vert}{defn_line}\n\tPrimary Id Attribute: {primary_id_name}" + f"\n\tAttributes: \n\t\t{attrs_block}" + ) + + try: + all_edges = await conn.getEdgeTypes() or [] + except Exception: + all_edges = [] + edge_attributes: dict = {} + edge_endpoints: dict = {} + edge_blocks: List[str] = [] + domain_edge_types: List[str] = [] + for edge in sorted(all_edges): + if exclude_structural and is_structural_type(edge): + continue + try: + einfo = await conn.getEdgeType(edge) or {} + except Exception: + continue + pairs = _collect_edge_pairs(einfo, exclude_structural) + if exclude_structural and not pairs: + continue + domain_edge_types.append(edge) + edge_endpoints[edge] = pairs + attrs_map, attr_lines = _collect_attrs(einfo.get("Attributes"), "") + edge_attributes[edge] = attrs_map + direction = "Directed" if einfo.get("IsDirected") else "Undirected" + defn_line = ( + f"\n\tDefinition: {rel_defs[edge]}" if rel_defs.get(edge) else "" + ) + attrs_block = "\n\t\t".join(attr_lines) or "No attributes" + for src, tgt in pairs: + pair_info = f"From Vertex: {src}\n\tTo Vertex: {tgt}" + edge_blocks.append( + f"{edge}{defn_line}\n\t{pair_info}" + f"\n\tEdge direction: {direction}" + f"\n\tAttributes: \n\t\t{attrs_block}" + ) + + return _assemble_schema_rep( + graphname=getattr(conn, "graphname", "") or "", + schema_ver=schema_ver, + vertex_blocks=vertex_blocks, + edge_blocks=edge_blocks, + exclude_structural=exclude_structural, + domain_verts=sorted(domain_verts) if exclude_structural else list(all_verts), + domain_edge_types=domain_edge_types, + vertex_attributes=vertex_attributes, + edge_attributes=edge_attributes, + entity_descs=entity_descs, + rel_defs=rel_defs, + edge_endpoints=edge_endpoints, + ) + + +# Back-compat alias for the ECC pipeline. +async def build_allowed_schema_async(conn) -> AllowedSchema: + """Back-compat alias for ``render_schema_rep_async(conn, exclude_structural=True)``.""" + return await render_schema_rep_async(conn, exclude_structural=True) + + +async def read_type_metadata_async(conn) -> Tuple[dict, dict]: + """Async counterpart to :func:`read_type_metadata` — used by the + ECC pipeline where the available connection is + ``pyTigerGraph.AsyncTigerGraphConnection``. + + Same return shape: ``({entity_id: description}, {rel_id: definition})``. + Errors propagate to the caller. + """ + entity_descs: dict = {} + rel_defs: dict = {} + + try: + rows = await conn.getVertices("EntityType") or [] + except Exception: + rows = [] + for row in rows: + attrs = row.get("attributes", row) + v_id = row.get("v_id") or attrs.get("id") + desc = (attrs.get("description") or "").strip() + if v_id and desc: + entity_descs[v_id] = desc + + try: + rows = await conn.getVertices("RelationshipType") or [] + except Exception: + rows = [] + for row in rows: + attrs = row.get("attributes", row) + v_id = row.get("v_id") or attrs.get("id") + defn = (attrs.get("definition") or "").strip() + if v_id and defn: + rel_defs[v_id] = defn + + return entity_descs, rel_defs + + +def _short_name(name: str) -> str: + """Lowercase, underscore-separated form of *name* — used as the + ``short_name`` attribute on ``RelationshipType`` vertices for display. + Trims to at most ~32 characters (the column has no length but display + is friendlier when short). + """ + folded = re.sub(r"[^A-Za-z0-9]+", "_", name).strip("_").lower() + return folded[:32] + + +def upsert_type_metadata( + conn, + proposal: SchemaProposal, +) -> dict: + """Upsert ``EntityType`` / ``RelationshipType`` vertices with the + descriptions from *proposal*. Does not touch existing rows whose + description / definition is already non-empty unless the proposal + carries a non-empty value of its own (callers may opt to override + by passing a description; we always pass through what the proposal + has). + + Returns ``{"entity_types": [...], "relationship_types": [...]}`` + listing the ids upserted. + """ + now = int(time.time()) + entity_ids: List[str] = [] + relationship_ids: List[str] = [] + + for v in proposal.vertices: + # EntityType schema: (id STRING, description STRING, epoch_added UINT) + attrs = {"epoch_added": now} + if v.description: + attrs["description"] = v.description + conn.upsertVertex("EntityType", v.name, attributes=attrs) + entity_ids.append(v.name) + + for e in proposal.edges: + # RelationshipType schema: + # (id STRING, definition STRING, short_name STRING, + # epoch_added UINT, epoch_processing UINT, epoch_processed UINT) + attrs = { + "epoch_added": now, + "short_name": _short_name(e.name), + } + if e.description: + attrs["definition"] = e.description + conn.upsertVertex("RelationshipType", e.name, attributes=attrs) + relationship_ids.append(e.name) + + return { + "entity_types": entity_ids, + "relationship_types": relationship_ids, + } + + +def apply_proposal( + conn, + graphname: str, + proposal: SchemaProposal, + job_name: Optional[str] = None, + progress: Optional[Callable[[str], None]] = None, +) -> dict: + """Diff *proposal* against the current schema on *conn* and apply the + additive delta as a single atomic ``SCHEMA_CHANGE JOB``. + + Returns a result dict:: + + { + "status": "applied" | "no-op", + "statements": [...], # ADD/ALTER statements that were emitted + "job_name": "", + "gsql_output": "", + "summary": {...}, # summarize(proposal) + } + + *progress* is an optional callback invoked at each sub-phase with a + short status string (e.g. ``"Creating new vertex/edge types"``, + ``"Installing retriever queries"``); the router uses it to drive + the init-dialog status line. + + Schema introspection errors propagate; the caller decides whether the + overall init flow should be marked as failed. The structural GraphRAG + schema must already exist on the graph (so the diff sees structural + types and only emits domain-side ADDs). + """ + def _report(msg: str) -> None: + if progress is None: + return + try: + progress(msg) + except Exception: + pass + + existing = read_existing_schema(conn) + domain_stmts = emit_add_statements(proposal, existing) + # Run the structural-link emitter against an *augmented* snapshot + # so vertices we're about to ADD are treated as present — otherwise + # has_edge_pair would always say "missing" and we'd over-emit. + augmented = ExistingSchema( + vertex_types=set(existing.vertex_types) | {v.name for v in proposal.vertices}, + edge_pairs=dict(existing.edge_pairs), + ) + structural_stmts = emit_structural_link_alters(proposal, augmented) + statements = domain_stmts + structural_stmts + summary = summarize(proposal) + + if not statements: + # Even on no-op, refresh metadata so descriptions edited in the + # review panel land on EntityType / RelationshipType vertices. + # The upsert is fast (<5s) so we don't surface it as its own + # status — the previous phase's message lingers through it. + metadata = upsert_type_metadata(conn, proposal) + retrievers = _install_retrievers_after_apply( + conn, graphname, + proposal=proposal, pre_apply_existing=existing, + progress=progress, + ) + return { + "status": "no-op", + "statements": [], + "job_name": None, + "job_names": [], + "gsql_output": "", + "summary": summary, + "metadata": metadata, + "retrievers": retrievers, + } + + # Split into two phases so TG's job-validator never sees an ALTER + # referencing a vertex/edge type created elsewhere in the same + # job. The ADD phase runs first; the ALTER phase (e.g. ADD PAIR + # on existing edges) runs only after the ADD phase commits. + add_stmts = [s for s in statements if s.lstrip().upper().startswith("ADD ")] + alter_stmts = [s for s in statements if s.lstrip().upper().startswith("ALTER ")] + + def _run_phase(phase_stmts: List[str], phase_job: Optional[str]) -> Tuple[str, str]: + block, name = build_schema_change_job(graphname, phase_stmts, phase_job) + try: + out = conn.gsql(block) + except Exception: + try: + conn.gsql(f"USE GRAPH {graphname}\nDROP JOB {name}") + except Exception: + pass + raise + return out, name + + # The two-phase split (ADD then ALTER) is internal mechanics; the + # user just sees a single "Applying domain schema" message that + # spans both phases plus the brief metadata upsert. The wording + # matches both schema-source paths (sample extraction and pasted + # GSQL) — "extracted" would be misleading for the paste mode. + _report("Applying domain schema") + + phase_outputs: List[str] = [] + phase_jobs: List[str] = [] + first_job_name: Optional[str] = None + for phase_stmts in (add_stmts, alter_stmts): + if not phase_stmts: + continue + # Only honor the caller-supplied job_name on the first phase that + # actually runs; subsequent phases get auto-generated names so + # they don't collide. + phase_job = job_name if first_job_name is None else None + output, ran_name = _run_phase(phase_stmts, phase_job) + phase_outputs.append(output) + phase_jobs.append(ran_name) + if first_job_name is None: + first_job_name = ran_name + err = gsql_output_error(output) + if err: + try: + conn.gsql(f"USE GRAPH {graphname}\nDROP JOB {ran_name}") + except Exception: + pass + return { + "status": "error", + "statements": statements, + "job_name": first_job_name, + "job_names": phase_jobs, + "gsql_output": "\n".join(phase_outputs), + "error": err, + "summary": summary, + "metadata": {"entity_types": [], "relationship_types": []}, + "retrievers": {"status": "skipped", "reason": "schema apply failed"}, + } + + # Metadata upsert is fast (<5s); no separate status message. + metadata = upsert_type_metadata(conn, proposal) + retrievers = _install_retrievers_after_apply( + conn, graphname, + proposal=proposal, pre_apply_existing=existing, + progress=progress, + ) + return { + "status": "applied", + "statements": statements, + "job_name": first_job_name, + "job_names": phase_jobs, + "gsql_output": "\n".join(phase_outputs), + "summary": summary, + "metadata": metadata, + "retrievers": retrievers, + } + + +def _detect_transitional_state( + conn, + proposal: SchemaProposal, + pre_apply_existing: ExistingSchema, +) -> Optional[dict]: + """Return a payload when a domain schema is being added to a graph + that already has Entity-layer data; ``None`` otherwise. + """ + new_vts = [ + v.name for v in proposal.vertices if not pre_apply_existing.has_vertex(v.name) + ] + if not new_vts: + return None + try: + entity_count = conn.getVertexCount("Entity") or 0 + except Exception: + entity_count = 0 + if entity_count <= 0: + return None + return { + "entity_count": int(entity_count), + "new_domain_vts": sorted(new_vts), + "recommendation": ( + "Existing Entity-layer data won't be auto-promoted to the " + "newly declared domain types. Retrievers will keep walking " + "the Entity layer (retrieval_include_entity forced to " + "True) so chat answers stay grounded. For full typed " + "retrieval, clear derived data (Entity / RELATIONSHIP / " + "Community) and re-run ECC against the existing chunks." + ), + } + + +def _install_retrievers_after_apply( + conn, + graphname: str, + proposal: Optional[SchemaProposal] = None, + pre_apply_existing: Optional[ExistingSchema] = None, + progress: Optional[Callable[[str], None]] = None, +) -> dict: + """Re-render and install the templated retrievers against the live + domain schema. No-op when no domain types are on the graph. + """ + try: + snapshot = read_existing_schema(conn) + except Exception as exc: + return {"status": "error", "error": f"read live schema: {exc}"} + + # Union the live-schema view with the proposal so a stale cache + # right after SCHEMA_CHANGE JOB doesn't miss new types. + domain_vt_set: Set[str] = { + v for v in snapshot.vertex_types if not is_structural_type(v) + } + domain_edge_set: Set[str] = { + e for e in snapshot.edge_pairs + if not is_structural_type(e) and e in snapshot.directed_edges + } + if proposal is not None: + for v in proposal.vertices: + if not is_structural_type(v.name): + domain_vt_set.add(v.name) + for e in proposal.edges: + if not is_structural_type(e.name) and e.directed: + domain_edge_set.add(e.name) + domain_vts = sorted(domain_vt_set) + domain_edges = sorted(domain_edge_set) + + import logging as _logging + _logger = _logging.getLogger(__name__) + _logger.info( + f"_install_retrievers_after_apply: graph={graphname} " + f"domain_vts={len(domain_vts)} directed_domain_edges={len(domain_edges)} " + f"snapshot_edge_pairs={len(snapshot.edge_pairs)}" + ) + + if not domain_vts and not domain_edges: + return {"status": "skipped", "reason": "no domain types on graph"} + + transitional: Optional[dict] = None + if proposal is not None and pre_apply_existing is not None: + transitional = _detect_transitional_state( + conn, proposal, pre_apply_existing + ) + + try: + from common.db.retriever_render import ( + install_retrievers, + resolve_include_entity, + ) + except Exception as exc: + return {"status": "error", "error": f"import renderer: {exc}"} + + try: + from common.config import graphrag_config + include_entity = resolve_include_entity( + graphrag_config.get, + has_domain_schema=bool(domain_vts), + ) + except Exception: + include_entity = False if domain_vts else True + + if transitional: + include_entity = True + + result: dict = { + "status": "installed", + "include_entity": include_entity, + "results": install_retrievers( + conn, + graphname, + domain_vts=domain_vts, + domain_edges=domain_edges, + include_entity=include_entity, + progress=progress, + ), + } + if transitional: + result["transitional"] = transitional + return result + + +# ----------------------------------------------------------------------------- +# Validation summary (informational, never blocking) +# ----------------------------------------------------------------------------- + + +def summarize(proposal: SchemaProposal) -> dict: + """Return a small descriptive payload for logging / API responses + (counts and lists of names). Never raises. + """ + return { + "vertex_count": len(proposal.vertices), + "edge_count": len(proposal.edges), + "vertex_names": [v.name for v in proposal.vertices], + "edge_names": [e.name for e in proposal.edges], + "edge_pair_count": sum(len(e.pairs) for e in proposal.edges), + "domain_label": proposal.domain_label, + } diff --git a/common/embeddings/tigergraph_embedding_store.py b/common/embeddings/tigergraph_embedding_store.py index 748f166..12d3caf 100644 --- a/common/embeddings/tigergraph_embedding_store.py +++ b/common/embeddings/tigergraph_embedding_store.py @@ -69,11 +69,7 @@ def __init__( tg_version = self.conn.getVer() ver = tg_version.split(".") if int(ver[0]) >= 4 and int(ver[1]) >= 2: - logger.info(f"Installing GDS library") - q_res = self.conn.gsql( - """USE GLOBAL\nimport package gds\ninstall function gds.**""" - ) - logger.info(f"Done installing GDS library with status {q_res}") + self._ensure_gds_installed() if self.conn.graphname and not self.conn.graphname == "MyGraph": current_schema = self.conn.gsql(f"USE GRAPH {self.conn.graphname}\n ls") if "(Dimension=" in current_schema: @@ -82,6 +78,47 @@ def __init__( else: raise Exception(f"Current TigerGraph version {ver} does not support vector feature!") + def _ensure_gds_installed(self) -> None: + """Install the gds package only if the gds.vector sub-package + isn't already present. + + Probes via ``SHOW PACKAGE gds`` whose output looks like + ``Packages "gds":\\n - Sub-Packages:\\n - vector\\n`` when + the sub-package is installed (verified empirically). Checking + the sub-package (rather than just the top-level ``gds``) also + catches a partial-install state where ``gds`` is present but + ``gds.vector`` isn't. + + The probe is a fast catalog read (~60ms) compared to + ``install function gds.**`` which takes a global catalog + write lock for the duration of the install scan (~3 minutes + against a remote TG). Skipping the install when the package + is present avoids that lock on every container restart. + + Falls through to the install on any probe failure — better to + occasionally re-install than to skip an install that's + actually missing. + """ + try: + sub_packages = self.conn.gsql("SHOW PACKAGE gds") + except Exception as exc: # noqa: BLE001 — defensive + logger.warning( + f"GDS-presence probe failed: {exc}. Falling through to install." + ) + sub_packages = "" + # The expected installed output is: + # 'Packages "gds":\n - Sub-Packages:\n - vector\n' + # When gds isn't installed at all, ``SHOW PACKAGE gds`` returns + # an error message instead, so this check fails closed. + if "- vector" in sub_packages: + logger.info("GDS library already installed; skipping install.") + return + logger.info("Installing GDS library") + q_res = self.conn.gsql( + """USE GLOBAL\nimport package gds\ninstall function gds.**""" + ) + logger.info(f"Done installing GDS library with status {q_res}") + def install_vector_queries(self): logger.info(f"Installing vector queries") vector_queries = [ @@ -121,6 +158,12 @@ def set_graphname(self, graphname): self.vector_attr_cache = {} if self.conn.apiToken or self.conn.jwtToken: self.conn.getToken() + # Re-verify GDS presence on every graphname switch. Cheap when + # the package is already installed (one LS call) and recovers + # from a mid-flight DROP ALL or admin-side wipe before the + # per-graph vector-query install below tries to reference + # missing gds.vector.* UDFs. + self._ensure_gds_installed() if self.conn.graphname and not self.conn.graphname == "MyGraph": current_schema = self.conn.gsql(f"USE GRAPH {self.conn.graphname}\n ls") if "(Dimension=" in current_schema: @@ -382,7 +425,8 @@ def has_embeddings( "vertex_id": v_id, } ) - logger.info(f"Return result {res} for has_embeddings({v_ids})") + # v_ids carry user-content-derived identifiers; demote. + logger.debug(f"Return result {res} for has_embeddings({v_ids})") found = False if "results" in res[0]: for v in res[0]["results"]: diff --git a/common/extractors/LLMEntityRelationshipExtractor.py b/common/extractors/LLMEntityRelationshipExtractor.py index dec1753..43fdb67 100644 --- a/common/extractors/LLMEntityRelationshipExtractor.py +++ b/common/extractors/LLMEntityRelationshipExtractor.py @@ -29,15 +29,169 @@ class LLMEntityRelationshipExtractor(BaseExtractor): def __init__( self, llm_service: LLM_Model, - allowed_entity_types: List[str] = None, - allowed_relationship_types: List[str] = None, + allowed_schema=None, strict_mode: bool = False, ): + """Build an LLM-driven entity/relationship extractor. + + ``allowed_schema`` is the consolidated description of the + domain schema the extractor must respect. It carries the + LLM-facing text rendering plus the structured maps the worker + layer uses for coercion and endpoint validation. Pass ``None`` + for "no schema — extract anything" mode. + + ``strict_mode`` (default ``False``) — when ``True`` the parser + drops nodes / relationships whose type isn't in the schema + AND the prompt tells the LLM to stay within it. Read from + ``graphrag_config.strict_mode`` by the ECC builder. + """ + from common.db.schema_utils import AllowedSchema self.llm_service = llm_service - self.allowed_vertex_types = allowed_entity_types - self.allowed_edge_types = allowed_relationship_types + self.allowed_schema = allowed_schema or AllowedSchema() self.strict_mode = strict_mode + # Thin @property accessors so the worker can read schema fields + # directly off the extractor without unpacking ``allowed_schema``. + + @property + def allowed_vertex_types(self): + return self.allowed_schema.vertex_types or None + + @property + def allowed_edge_types(self): + return self.allowed_schema.edge_types or None + + @property + def entity_type_definitions(self): + return self.allowed_schema.vertex_definitions + + @property + def relationship_type_definitions(self): + return self.allowed_schema.edge_definitions + + @property + def domain_edge_endpoints(self): + return self.allowed_schema.edge_endpoints + + @property + def entity_type_attributes(self): + return self.allowed_schema.vertex_attributes + + @property + def relationship_type_attributes(self): + return self.allowed_schema.edge_attributes + + def _format_definitions(self, defs: dict) -> str: + """Render a ``{type_name: definition}`` dict as one + ``- : `` line per type, sorted by name. Used + when assembling the schema-aware extraction prompt. + """ + if not defs: + return "" + return "\n".join( + f"- {name}: {definition}" + for name, definition in sorted(defs.items()) + if definition + ) + + def _format_edge_endpoints(self) -> str: + """Render ``{edge_name: [(from, to), ...]}`` as + ``- : -> [, -> ]`` lines, sorted + by edge name. Empty when no endpoints are configured. + """ + if not self.domain_edge_endpoints: + return "" + lines = [] + for name, pairs in sorted(self.domain_edge_endpoints.items()): + pair_strs = ", ".join(f"{f} -> {t}" for f, t in pairs) or "" + defn = self.relationship_type_definitions.get(name, "") + tail = f" — {defn}" if defn else "" + lines.append(f"- {name}: {pair_strs}{tail}") + return "\n".join(lines) + + @staticmethod + def _rel_props(rels: dict) -> dict: + """Pull a ``properties`` / ``attributes`` dict off an LLM- + emitted relationship object. Empty dict when neither key is + present or the value isn't a dict. + """ + p = rels.get("properties") or rels.get("attributes") or {} + return p if isinstance(p, dict) else {} + + def _format_type_attributes(self, type_attrs: dict) -> str: + """Render ``{type_name: {attr_name: tg_type}}`` as a nested + block the LLM can read:: + + - Filing + - filed_at (DATETIME) + - amount (DOUBLE) + - jurisdiction (STRING) + - Company + - founded_year (INT) + - industry (STRING) + + Empty when no types carry attributes. + """ + if not type_attrs: + return "" + lines = [] + for name in sorted(type_attrs.keys()): + attrs = type_attrs.get(name) or {} + if not attrs: + continue + lines.append(f"- {name}") + for attr_name in sorted(attrs.keys()): + lines.append(f" - {attr_name} ({attrs[attr_name]})") + return "\n".join(lines) + + def _build_schema_prompt_messages(self) -> list: + """Return the human-message tuples that describe the domain + schema to the LLM. Used by both sync and async extraction paths. + Empty list when no schema is configured. + """ + msgs = [] + schema_rep = (self.allowed_schema.schema_rep or "").strip() + if not schema_rep: + return msgs + + if self.strict_mode: + msgs.append(( + "human", + "STRICT SCHEMA MODE: only emit entities whose entity_type " + "matches one of the vertex types in the schema below, and " + "only emit relationships whose relation_type matches an " + "edge type AND whose source / target match a declared " + "(FROM, TO) endpoint pair. Drop any entity or relationship " + "that doesn't fit. Do NOT invent new types.", + )) + else: + msgs.append(( + "human", + "When choosing the entity_type / relationship_type for an " + "extraction, strongly prefer the schema types listed below " + "and use their definitions to disambiguate similar types. " + "Ignore page-structure / chart / layout artifacts (axes, " + "segments, percentages, page numbers, sections, navigation " + "menus, captions). Prefer concrete real-world entities over " + "abstract categorical groupings. Only invent a new type " + "when nothing in the schema fits.", + )) + msgs.append(("human", schema_rep)) + msgs.append(( + "human", + "For every node and relationship, populate a `properties` map " + "with values you find in the text for the attributes shown in " + "the schema. Use the exact attribute names listed. Match the " + "declared type: INT / UINT as integers, DOUBLE / FLOAT as " + "numbers, BOOL as true/false, DATETIME as an ISO-8601 string " + "(e.g. \"2024-01-15\" or \"2024-01-15T09:30:00\"). Omit " + "attributes you can't find values for — partial coverage is " + "fine. Do NOT invent attribute names beyond those in the " + "schema. The `id` / primary-id attribute lives on the node's " + "`id` field — do NOT also put it in `properties`.", + )) + return msgs + def _parse_json_output(self, content: str) -> dict: """Parse JSON from LLM output with multiple fallback strategies. @@ -80,61 +234,8 @@ async def _aextract_kg_from_doc(self, doc, chain, parser) -> list[GraphDocument] try: json_out = self._parse_json_output(out.content) - formatted_rels = [] - for rels in json_out["rels"]: - if isinstance(rels["source"], str) and isinstance(rels["target"], str): - formatted_rels.append( - { - "source": rels["source"], - "target": rels["target"], - "type": rels["relation_type"].replace(" ", "_").upper(), - "definition": rels["definition"], - } - ) - elif isinstance(rels["source"], dict) and isinstance( - rels["target"], str - ): - formatted_rels.append( - { - "source": rels["source"]["id"], - "target": rels["target"], - "type": rels["relation_type"].replace(" ", "_").upper(), - "definition": rels["definition"], - } - ) - elif isinstance(rels["source"], str) and isinstance( - rels["target"], dict - ): - formatted_rels.append( - { - "source": rels["source"], - "target": rels["target"]["id"], - "type": rels["relation_type"].replace(" ", "_").upper(), - "definition": rels["definition"], - } - ) - elif isinstance(rels["source"], dict) and isinstance( - rels["target"], dict - ): - formatted_rels.append( - { - "source": rels["source"]["id"], - "target": rels["target"]["id"], - "type": rels["relation_type"].replace(" ", "_").upper(), - "definition": rels["definition"], - } - ) - else: - raise Exception("Relationship parsing error") - formatted_nodes = [] - for node in json_out["nodes"]: - formatted_nodes.append( - { - "id": node["id"], - "type": node["node_type"].replace(" ", "_").capitalize(), - "definition": node["definition"], - } - ) + formatted_rels = self._format_rels(json_out["rels"]) + formatted_nodes = self._format_nodes(json_out["nodes"]) # filter relationships and nodes based on allowed types if self.strict_mode: @@ -151,19 +252,11 @@ async def _aextract_kg_from_doc(self, doc, chain, parser) -> list[GraphDocument] if rel["type"] in self.allowed_edge_types ] - nodes = [] - for node in formatted_nodes: - nodes.append(Node(id=node["id"], - type=node["type"], - properties={"description": node["definition"]})) - relationships = [] - for rel in formatted_rels: - relationships.append(Relationship(source=Node(id=rel["source"], type=rel["source"], - properties={"description": rel["definition"]}), - target=Node(id=rel["target"], type=rel["target"], - properties={"description": rel["definition"]}), type=rel["type"])) - - return [GraphDocument(nodes=nodes, relationships=relationships, source=Document(page_content=doc))] + return [GraphDocument( + nodes=self._build_nodes(formatted_nodes), + relationships=self._build_rels(formatted_rels), + source=Document(page_content=doc), + )] except: return [GraphDocument(nodes=[], relationships=[], source=Document(page_content=doc))] @@ -178,61 +271,8 @@ def _extract_kg_from_doc(self, doc, chain, parser) -> list[GraphDocument]: try: json_out = self._parse_json_output(out.content) - formatted_rels = [] - for rels in json_out["rels"]: - if isinstance(rels["source"], str) and isinstance(rels["target"], str): - formatted_rels.append( - { - "source": rels["source"], - "target": rels["target"], - "type": rels["relation_type"].replace(" ", "_").upper(), - "definition": rels["definition"], - } - ) - elif isinstance(rels["source"], dict) and isinstance( - rels["target"], str - ): - formatted_rels.append( - { - "source": rels["source"]["id"], - "target": rels["target"], - "type": rels["relation_type"].replace(" ", "_").upper(), - "definition": rels["definition"], - } - ) - elif isinstance(rels["source"], str) and isinstance( - rels["target"], dict - ): - formatted_rels.append( - { - "source": rels["source"], - "target": rels["target"]["id"], - "type": rels["relation_type"].replace(" ", "_").upper(), - "definition": rels["definition"], - } - ) - elif isinstance(rels["source"], dict) and isinstance( - rels["target"], dict - ): - formatted_rels.append( - { - "source": rels["source"]["id"], - "target": rels["target"]["id"], - "type": rels["relation_type"].replace(" ", "_").upper(), - "definition": rels["definition"], - } - ) - else: - raise Exception("Relationship parsing error") - formatted_nodes = [] - for node in json_out["nodes"]: - formatted_nodes.append( - { - "id": node["id"], - "type": node["node_type"].replace(" ", "_").capitalize(), - "definition": node["definition"], - } - ) + formatted_rels = self._format_rels(json_out["rels"]) + formatted_nodes = self._format_nodes(json_out["nodes"]) # filter relationships and nodes based on allowed types if self.strict_mode: @@ -248,23 +288,124 @@ def _extract_kg_from_doc(self, doc, chain, parser) -> list[GraphDocument]: for rel in formatted_rels if rel["type"] in self.allowed_edge_types ] - - nodes = [] - for node in formatted_nodes: - nodes.append(Node(id=node["id"], - type=node["type"], - properties={"description": node["definition"]})) - relationships = [] - for rel in formatted_rels: - relationships.append(Relationship(source=Node(id=rel["source"], type=rel["source"], - properties={"description": rel["definition"]}), - target=Node(id=rel["target"], type=rel["target"], - properties={"description": rel["definition"]}), type=rel["type"])) - - return [GraphDocument(nodes=nodes, relationships=relationships, source=Document(page_content=doc))] + + return [GraphDocument( + nodes=self._build_nodes(formatted_nodes), + relationships=self._build_rels(formatted_rels), + source=Document(page_content=doc), + )] except: return [GraphDocument(nodes=[], relationships=[], source=Document(page_content=doc))] + + # --- LLM-output normalization helpers (shared by sync + async) ---- + + @staticmethod + def _resolve_id_and_props(value): + """Source / target in the LLM's ``rels`` list may come as a + bare id string or as a dict with ``id`` + optional + ``node_type`` + optional ``properties``. Return + ``(id_str, node_type_str, props_dict)``. ``node_type`` is the + empty string when the LLM didn't carry one on this endpoint; + callers should fall back to the entity's own node entry to + recover the type in that case. + """ + if isinstance(value, dict): + props = value.get("properties") or value.get("attributes") or {} + node_type = value.get("node_type") or value.get("type") or "" + return ( + str(value.get("id", "")), + str(node_type), + props if isinstance(props, dict) else {}, + ) + return str(value), "", {} + + def _format_rels(self, rels_in: list) -> list: + formatted = [] + for rels in rels_in or []: + try: + src_id, src_type, src_props = self._resolve_id_and_props(rels["source"]) + tgt_id, tgt_type, tgt_props = self._resolve_id_and_props(rels["target"]) + if not (src_id and tgt_id): + continue + # Edge-level properties (typed attrs the LLM extracted + # for this edge type, e.g. ``MONEY_TRANSFER.amount``) + # live directly under the rel object. Source/target + # vertex attrs are kept separately so the worker can + # apply each to the right row. + rel_props = self._rel_props(rels) + formatted.append({ + "source": src_id, + "target": tgt_id, + "source_type": src_type.replace(" ", "_").capitalize() if src_type else "", + "target_type": tgt_type.replace(" ", "_").capitalize() if tgt_type else "", + "source_props": src_props, + "target_props": tgt_props, + "type": rels["relation_type"].replace(" ", "_").upper(), + "definition": rels.get("definition", ""), + "properties": rel_props, + }) + except (KeyError, TypeError): + continue + return formatted + + def _format_nodes(self, nodes_in: list) -> list: + formatted = [] + for node in nodes_in or []: + try: + # ``properties`` (or ``attributes``) is optional — the + # LLM may omit it when nothing in the text fits the + # typed attribute schema we sent. + props = node.get("properties") or node.get("attributes") or {} + formatted.append({ + "id": node["id"], + "type": node["node_type"].replace(" ", "_").capitalize(), + "definition": node.get("definition", ""), + "properties": props if isinstance(props, dict) else {}, + }) + except (KeyError, TypeError): + continue + return formatted + + def _build_nodes(self, formatted_nodes: list) -> list: + nodes = [] + for node in formatted_nodes: + # Forward LLM-emitted typed attributes alongside the + # description text. The worker splits ``description`` + # (-> Entity row) from typed attributes (-> domain VT row) + # and coerces / filters the latter to the live schema. + node_props = {**(node.get("properties") or {}), + "description": node["definition"]} + nodes.append(Node(id=node["id"], + type=node["type"], + properties=node_props)) + return nodes + + def _build_rels(self, formatted_rels: list) -> list: + relationships = [] + for rel in formatted_rels: + src_props = {**(rel.get("source_props") or {}), + "description": rel["definition"]} + tgt_props = {**(rel.get("target_props") or {}), + "description": rel["definition"]} + edge_props = {**(rel.get("properties") or {}), + "description": rel["definition"]} + # Use the canonical entity types when the LLM provided them + # on the relationship endpoints; fall back to the id so the + # field is never empty. Downstream endpoint-pair validation + # in the worker relies on these values matching the live + # schema's declared edge endpoints. + src_type = rel.get("source_type") or rel["source"] + tgt_type = rel.get("target_type") or rel["target"] + relationships.append(Relationship( + source=Node(id=rel["source"], type=src_type, + properties=src_props), + target=Node(id=rel["target"], type=tgt_type, + properties=tgt_props), + type=rel["type"], + properties=edge_props, + )) + return relationships async def adocument_er_extraction(self, document): from langchain.prompts import ChatPromptTemplate @@ -298,6 +439,7 @@ async def adocument_er_extraction(self, document): prompt.append(("human", f"Allowed Node Types: {self.allowed_vertex_types}")) if self.allowed_edge_types: prompt.append(("human", f"Allowed Edge Types: {self.allowed_edge_types}")) + prompt.extend(self._build_schema_prompt_messages()) prompt = ChatPromptTemplate.from_messages(prompt) chain = prompt | self.llm_service.llm # | parser er = await self._aextract_kg_from_doc(document, chain, parser) @@ -336,6 +478,7 @@ def document_er_extraction(self, document): prompt.append(("human", f"Allowed Node Types: {self.allowed_vertex_types}")) if self.allowed_edge_types: prompt.append(("human", f"Allowed Edge Types: {self.allowed_edge_types}")) + prompt.extend(self._build_schema_prompt_messages()) prompt = ChatPromptTemplate.from_messages(prompt) chain = prompt | self.llm_service.llm # | parser er = self._extract_kg_from_doc(document, chain, parser) diff --git a/common/gsql/graphrag/graphrag_delete_all_communities.gsql b/common/gsql/graphrag/graphrag_delete_all_communities.gsql new file mode 100644 index 0000000..ceb9f80 --- /dev/null +++ b/common/gsql/graphrag/graphrag_delete_all_communities.gsql @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024-2026 TigerGraph, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 +*/ + +CREATE OR REPLACE DISTRIBUTED QUERY graphrag_delete_all_communities() { + SumAccum @@deleted; + + comms = {Community.*}; + res = SELECT c FROM comms:c + ACCUM + DELETE(c), + @@deleted += 1; + + PRINT @@deleted AS deleted; +} diff --git a/common/gsql/graphrag/graphrag_stream_all_ids.gsql b/common/gsql/graphrag/graphrag_stream_all_ids.gsql new file mode 100644 index 0000000..8f16441 --- /dev/null +++ b/common/gsql/graphrag/graphrag_stream_all_ids.gsql @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024-2026 TigerGraph, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 +*/ + +CREATE OR REPLACE QUERY graphrag_stream_all_ids(STRING v_type) { + ListAccum @@ids; + Verts = {v_type}; + Verts = SELECT v FROM Verts:v + ACCUM @@ids += v.id; + PRINT @@ids; +} diff --git a/common/gsql/graphrag/graphrag_stream_entity_community_pairs.gsql b/common/gsql/graphrag/graphrag_stream_entity_community_pairs.gsql new file mode 100644 index 0000000..8d20117 --- /dev/null +++ b/common/gsql/graphrag/graphrag_stream_entity_community_pairs.gsql @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024-2026 TigerGraph, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 +*/ + +CREATE OR REPLACE DISTRIBUTED QUERY graphrag_stream_entity_community_pairs() { + TYPEDEF TUPLE Pair; + ListAccum @@pairs; + + ents = {Entity.*}; + res = SELECT e + FROM ents:e -(IN_COMMUNITY>:m)- Community:c + ACCUM @@pairs += Pair(e.id, c.id); + + PRINT @@pairs AS pairs; +} diff --git a/common/gsql/graphrag/louvain/graphrag_louvain_communities.gsql b/common/gsql/graphrag/louvain/graphrag_louvain_communities.gsql index 5e6dcda..91e6cf2 100644 --- a/common/gsql/graphrag/louvain/graphrag_louvain_communities.gsql +++ b/common/gsql/graphrag/louvain/graphrag_louvain_communities.gsql @@ -1,4 +1,5 @@ -CREATE OR REPLACE DISTRIBUTED QUERY graphrag_louvain_communities(UINT iteration=1, UINT max_hop = 10, UINT n_batches = 1) SYNTAX V2{ +// Non-distributed form intentional; see v1.5 plan to revisit. +CREATE OR REPLACE QUERY graphrag_louvain_communities(UINT iteration=1, UINT max_hop = 10, UINT n_batches = 1) SYNTAX V2{ /* * This is the same query as tg_louvain, just that Paper-related schema * are changed to Community-related schema diff --git a/common/gsql/graphrag/louvain/graphrag_louvain_init.gsql b/common/gsql/graphrag/louvain/graphrag_louvain_init.gsql index dd85d6d..f582dc3 100644 --- a/common/gsql/graphrag/louvain/graphrag_louvain_init.gsql +++ b/common/gsql/graphrag/louvain/graphrag_louvain_init.gsql @@ -1,4 +1,5 @@ -CREATE OR REPLACE DISTRIBUTED QUERY graphrag_louvain_init(UINT max_hop = 10, UINT n_batches = 1) { +// Non-distributed form intentional; see v1.5 plan to revisit. +CREATE OR REPLACE QUERY graphrag_louvain_init(UINT max_hop = 10, UINT n_batches = 1) { /* * Initialize GraphRAG's hierarchical communities. */ @@ -43,9 +44,6 @@ CREATE OR REPLACE DISTRIBUTED QUERY graphrag_louvain_init(UINT max_hop = 10, UIN ACCUM s.@k += wt, @@m += 1; - PRINT z.size(); - PRINT z; - // Local moving INT hop = 0; Candidates = AllNodes; @@ -183,7 +181,4 @@ CREATE OR REPLACE DISTRIBUTED QUERY graphrag_louvain_init(UINT max_hop = 10, UIN ACCUM DOUBLE w = @@source_target_k_in_map.get(s.@community_vid).get(t.@community_vid), INSERT INTO LINKS_TO VALUES (s.@community_vid+"_1", t.@community_vid+"_1", w); - - - PRINT @@source_target_k_in_map; } diff --git a/common/gsql/graphrag/louvain/modularity.gsql b/common/gsql/graphrag/louvain/modularity.gsql index cab6255..6f53c96 100644 --- a/common/gsql/graphrag/louvain/modularity.gsql +++ b/common/gsql/graphrag/louvain/modularity.gsql @@ -1,4 +1,5 @@ -CREATE OR REPLACE DISTRIBUTED QUERY modularity(UINT iteration=1) SYNTAX V2 { +// Non-distributed form intentional; see v1.5 plan to revisit. +CREATE OR REPLACE QUERY modularity(UINT iteration=1) SYNTAX V2 { SumAccum @@sum_weight; // the sum of the weights of all the links in the network MinAccum @community_id; // the community ID of the node MapAccum> @@community_total_weight_map; // community ID C -> the sum of the weights of the links incident to nodes in C diff --git a/common/gsql/graphrag/louvain/stream_community.gsql b/common/gsql/graphrag/louvain/stream_community.gsql index e04f7fc..d54fd19 100644 --- a/common/gsql/graphrag/louvain/stream_community.gsql +++ b/common/gsql/graphrag/louvain/stream_community.gsql @@ -1,4 +1,5 @@ -CREATE OR REPLACE DISTRIBUTED QUERY stream_community(UINT iter) { +// Non-distributed form intentional; see v1.5 plan to revisit. +CREATE OR REPLACE QUERY stream_community(UINT iter) { Comms = {Community.*}; Comms = SELECT s FROM Comms:s diff --git a/common/gsql/supportai/SupportAI_Schema.gsql b/common/gsql/supportai/SupportAI_Schema.gsql index c756fd3..79aa865 100644 --- a/common/gsql/supportai/SupportAI_Schema.gsql +++ b/common/gsql/supportai/SupportAI_Schema.gsql @@ -22,14 +22,13 @@ CREATE SCHEMA_CHANGE JOB add_supportai_schema { ADD VERTEX Content(PRIMARY_ID id STRING, ctype STRING, text STRING, epoch_added UINT) WITH STATS="OUTDEGREE_BY_EDGETYPE", PRIMARY_ID_AS_ATTRIBUTE="true"; ADD VERTEX EntityType(PRIMARY_ID id STRING, description STRING, epoch_added UINT) WITH STATS="OUTDEGREE_BY_EDGETYPE", PRIMARY_ID_AS_ATTRIBUTE="true"; ADD DIRECTED EDGE HAS_CONTENT(FROM Document, TO Content|FROM DocumentChunk, TO Content) WITH REVERSE_EDGE="reverse_HAS_CONTENT"; - ADD DIRECTED EDGE IS_HEAD_OF(FROM Entity, TO RelationshipType) WITH REVERSE_EDGE="reverse_IS_HEAD_OF"; - ADD DIRECTED EDGE HAS_TAIL(FROM RelationshipType, TO Entity) WITH REVERSE_EDGE="reverse_HAS_TAIL"; + ADD DIRECTED EDGE IS_HEAD_OF(FROM EntityType, TO RelationshipType) WITH REVERSE_EDGE="reverse_IS_HEAD_OF"; + ADD DIRECTED EDGE HAS_TAIL(FROM RelationshipType, TO EntityType) WITH REVERSE_EDGE="reverse_HAS_TAIL"; ADD DIRECTED EDGE CONTAINS_ENTITY(FROM DocumentChunk, TO Entity|FROM Document, TO Entity) WITH REVERSE_EDGE="reverse_CONTAINS_ENTITY"; ADD DIRECTED EDGE MENTIONS_RELATIONSHIP(FROM DocumentChunk, TO RelationshipType|FROM Document, TO RelationshipType) WITH REVERSE_EDGE="reverse_MENTIONS_RELATIONSHIP"; ADD DIRECTED EDGE IS_AFTER(FROM DocumentChunk, TO DocumentChunk) WITH REVERSE_EDGE="reverse_IS_AFTER"; ADD DIRECTED EDGE HAS_CHILD(FROM Document, TO DocumentChunk) WITH REVERSE_EDGE="reverse_HAS_CHILD"; ADD DIRECTED EDGE ENTITY_HAS_TYPE(FROM Entity, TO EntityType) WITH REVERSE_EDGE="reverse_ENTITY_HAS_TYPE"; - ADD DIRECTED EDGE RELATIONSHIP_TYPE(FROM EntityType, TO EntityType, DISCRIMINATOR(relation_type STRING), frequency INT) WITH REVERSE_EDGE="reverse_RELATIONSHIP_TYPE"; // GraphRAG ADD VERTEX Community (PRIMARY_ID id STRING, iteration UINT, description STRING) WITH STATS="OUTDEGREE_BY_EDGETYPE", PRIMARY_ID_AS_ATTRIBUTE="true"; diff --git a/common/gsql/supportai/create_entity_type_relationships.gsql b/common/gsql/supportai/create_entity_type_relationships.gsql deleted file mode 100644 index 860f55c..0000000 --- a/common/gsql/supportai/create_entity_type_relationships.gsql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE OR REPLACE DISTRIBUTED QUERY create_entity_type_relationships(/* Parameters here */) SYNTAX v2{ - MapAccum>> @rel_type_count; // entity type, relationship type for entity type, frequency - SumAccum @@rels_inserted; - ents = {Entity.*}; - accum_types = SELECT et FROM ents:e -(RELATIONSHIP>:r)- Entity:e2 -(ENTITY_HAS_TYPE>:eht)- EntityType:et - WHERE r.relation_type != "DOC_CHUNK_COOCCURRENCE" - ACCUM - e.@rel_type_count += (et.id -> (r.relation_type -> 1)); - - ets = SELECT et FROM ents:e -(ENTITY_HAS_TYPE>:eht)- EntityType:et - ACCUM - FOREACH (entity_type, rel_type_freq) IN e.@rel_type_count DO - FOREACH (rel_type, freq) IN e.@rel_type_count.get(entity_type) DO - INSERT INTO RELATIONSHIP_TYPE VALUES (et.id, entity_type, rel_type, freq), - @@rels_inserted += 1 - END - END; - - PRINT @@rels_inserted as relationships_inserted; -} \ No newline at end of file diff --git a/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql b/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql index ed917e0..2801164 100644 --- a/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql +++ b/common/gsql/supportai/retrievers/Content_Similarity_Search.gsql @@ -40,15 +40,15 @@ CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Search(STRING json_list_v @@final_retrieval += (s.id -> tgt.text) END POST-ACCUM - IF s.type == "RelationshipType" OR s.type == "Entity" THEN + IF s.type == "Entity" THEN @@final_retrieval += (s.id -> s.definition) ELSE IF s.type == "Community" THEN @@final_retrieval += (s.id -> s.description) END; - + @@verbose_info += ("start_set" -> @@start_set_type); - PRINT @@final_retrieval as final_retrieval; + PRINT @@final_retrieval as final_retrieval; IF verbose THEN PRINT @@verbose_info as verbose; diff --git a/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql b/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql index 24648d9..e711208 100644 --- a/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql +++ b/common/gsql/supportai/retrievers/Content_Similarity_Vector_Search.gsql @@ -14,7 +14,7 @@ * limitations under the License. */ -CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Vector_Search(STRING v_type, LIST query_vector, INT top_k=5, BOOL verbose = False) { +CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Vector_Search(STRING v_type, LIST query_vector, INT top_k=5, BOOL verbose = False) { TYPEDEF tuple Similarity_Results; TYPEDEF TUPLE VertexTypes; SetAccum @@start_set_type; @@ -22,31 +22,31 @@ CREATE OR REPLACE DISTRIBUTED QUERY Content_Similarity_Vector_Search(STRING v_ty HeapAccum(top_k, score DESC) @@topk_set; SetAccum @@start_set; MapAccum @@final_retrieval; - + vset = {v_type}; - result = SELECT v FROM vset:v WHERE v.embedding.size() > 0 POST-ACCUM @@topk_set += Similarity_Results(v, 1 - gds.vector.distance(query_vector, v.embedding, "COSINE")); + result = SELECT v FROM vset:v POST-ACCUM @@topk_set += Similarity_Results(v, 1 - gds.vector.distance(query_vector, v.embedding, "COSINE")); FOREACH item IN @@topk_set DO @@start_set += item.v; END; - + start = {@@start_set}; - + res = SELECT s FROM start:s -(:e)- :tgt WHERE s.type == v_type ACCUM @@start_set_type += VertexTypes(s, s.type), IF (s.type == "DocumentChunk" OR s.type == "Document") AND tgt.type == "Content" THEN @@final_retrieval += (s.id -> tgt.text) END POST-ACCUM - IF s.type == "RelationshipType" OR s.type == "Entity" THEN + IF s.type == "Entity" THEN @@final_retrieval += (s.id -> s.definition) ELSE IF s.type == "Community" THEN @@final_retrieval += (s.id -> s.description) END; - + @@verbose_info += ("start_set" -> @@start_set_type); - PRINT @@final_retrieval as final_retrieval; + PRINT @@final_retrieval as final_retrieval; IF verbose THEN PRINT @@verbose_info as verbose; diff --git a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search.gsql b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search.gsql index 9d68c00..8a49e75 100644 --- a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search.gsql +++ b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search.gsql @@ -49,9 +49,7 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Search(STRING json_list_vts start = SELECT t FROM start:s -((RELATIONSHIP>| CONTAINS_ENTITY>| reverse_CONTAINS_ENTITY>| - IS_AFTER>| - IS_HEAD_OF>| - HAS_TAIL>):e)- :t + IS_AFTER>):e)- :t WHERE s.@visited < 1 AND t NOT IN s.@parents ACCUM s.@visited += 1, t.@num_times_seen += 1, t.@parents += s, t.@parents += s.@parents, t.@paths += s.@paths, t.@paths += e POST-ACCUM(t) @@tmp_set += t; @@ -76,6 +74,14 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Search(STRING json_list_vts s.@context += tmp_dsc ELSE IF s.type == "DocumentChunk" THEN @@to_retrieve_content += s + ELSE + // Domain vertex type instance — surface its type label + // and id so the LLM sees "Company: acme corp" instead of + // an unlabeled identifier. Domain VTs mirror Entity + // instances by id, so the description data is already + // captured via the Entity branch above; this branch adds + // the type-aware grounding the schema-aware path provides. + s.@context += s.type + ": " + replace(s.id, "_", " ") END POST-ACCUM(s) IF NOT (chunk_only OR doc_only) OR (chunk_only OR doc_only) AND s.type == "DocumentChunk" THEN diff --git a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search_Display.gsql b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search_Display.gsql index 6a6f9b9..a5dcae5 100644 --- a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search_Display.gsql +++ b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Search_Display.gsql @@ -48,9 +48,7 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Search_Display(STRING json_l start = SELECT t FROM start:s -((RELATIONSHIP>| CONTAINS_ENTITY>| reverse_CONTAINS_ENTITY>| - IS_AFTER>| - IS_HEAD_OF>| - HAS_TAIL>):e)- :t + IS_AFTER>):e)- :t WHERE s.@visited < 1 AND t NOT IN s.@parents ACCUM s.@visited += 1, t.@num_times_seen += 1, t.@parents += s, t.@parents += s.@parents, t.@paths += s.@paths, t.@paths += e POST-ACCUM(t) @@tmp_set += t, t.@seen_in_hop += to_string(hierachy), @@ -80,6 +78,10 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Search_Display(STRING json_l s.@context += tmp_dsc ELSE IF s.type == "DocumentChunk" THEN @@to_retrieve_content += s + ELSE + // Domain vertex type — surface ": " so the + // LLM sees the schema-aware label. + s.@context += s.type + ": " + replace(s.id, "_", " ") END POST-ACCUM(s) IF NOT chunk_only OR chunk_only AND s.type == "DocumentChunk" THEN diff --git a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Vector_Search.gsql b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Vector_Search.gsql index 2816156..d9fc9b4 100644 --- a/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Vector_Search.gsql +++ b/common/gsql/supportai/retrievers/GraphRAG_Hybrid_Vector_Search.gsql @@ -55,9 +55,7 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Vector_Search(Set v_ start = SELECT t FROM start:s -((RELATIONSHIP>| CONTAINS_ENTITY>| reverse_CONTAINS_ENTITY>| - IS_AFTER>| - IS_HEAD_OF>| - HAS_TAIL>):e)- :t + IS_AFTER>):e)- :t WHERE s.@visited < 1 AND t NOT IN s.@parents ACCUM s.@visited += 1, t.@num_times_seen += 1, t.@parents += s, t.@parents += s.@parents, t.@paths += s.@paths, t.@paths += e POST-ACCUM(t) @@tmp_set += t; @@ -82,6 +80,10 @@ CREATE OR REPLACE DISTRIBUTED QUERY GraphRAG_Hybrid_Vector_Search(Set v_ s.@context += tmp_dsc ELSE IF s.type == "DocumentChunk" THEN @@to_retrieve_content += s + ELSE + // Domain vertex type — surface ": " so the + // LLM sees the schema-aware label. + s.@context += s.type + ": " + replace(s.id, "_", " ") END POST-ACCUM(s) IF NOT (chunk_only OR doc_only) OR (chunk_only OR doc_only) AND s.type == "DocumentChunk" THEN diff --git a/common/llm_services/aws_bedrock_service.py b/common/llm_services/aws_bedrock_service.py index de6143a..ce6056c 100644 --- a/common/llm_services/aws_bedrock_service.py +++ b/common/llm_services/aws_bedrock_service.py @@ -22,6 +22,70 @@ logger = logging.getLogger(__name__) +#: Per-model-family ``max_tokens`` caps for Bedrock-hosted models. +#: Keys are case-insensitive prefixes / substrings of the model id; the +#: longest matching prefix wins. Models not matched fall back to the +#: generic default (see :data:`_BEDROCK_MAX_TOKENS_DEFAULT`). +#: +#: References (cap = max output tokens supported by the model on Bedrock): +#: - Anthropic Claude 3 / 3.5 Haiku / 3 Opus: 4096 +#: - Anthropic Claude 3.5 / 3.7 Sonnet: 8192 +#: - Anthropic Claude Sonnet 4 / 4.5: 64000 (we cap at 8192 for safety) +#: - Amazon Titan Text: 4096 +#: - Amazon Nova: 5120 +#: - Cohere Command: 4000 +#: - Meta Llama 2: 2048; Llama 3: 4096 +#: - AI21 Jurassic / Jamba: 4096 +#: - Mistral: 8192 +_BEDROCK_MAX_TOKENS_BY_MODEL: tuple = ( + # Anthropic Claude 3.x family — explicit capped at 4096 + ("anthropic.claude-3-5-haiku", 4096), + ("anthropic.claude-3-haiku", 4096), + ("anthropic.claude-3-opus", 4096), + ("anthropic.claude-3-sonnet", 4096), + ("anthropic.claude-instant", 4096), + # Anthropic Claude 3.5 / 3.7 Sonnet — 8192 + ("anthropic.claude-3-5-sonnet", 8192), + ("anthropic.claude-3-7-sonnet", 8192), + # Amazon Titan Text models — capped at 4096 + ("amazon.titan-text", 4096), + ("amazon.titan-tg1", 4096), + # Cohere Command — 4000 + ("cohere.command", 4000), + # Meta Llama + ("meta.llama2", 2048), + ("meta.llama3", 4096), + # AI21 Jamba / Jurassic + ("ai21.", 4096), +) +_BEDROCK_MAX_TOKENS_DEFAULT = 8192 + + +def _bedrock_max_tokens_for_model(model_id: str) -> int: + """Return the recommended ``max_tokens`` for the given Bedrock model + id. Falls back to :data:`_BEDROCK_MAX_TOKENS_DEFAULT` when no + family-specific cap is registered. + """ + if not model_id: + return _BEDROCK_MAX_TOKENS_DEFAULT + mid = model_id.lower() + # Cross-region inference profiles are prefixed with the region + # short code (``us.``, ``eu.``, ``apac.``, ``us-gov.``); strip so + # ``us.anthropic.claude-3-haiku-...`` matches the same family. + for prefix in ("us.", "eu.", "apac.", "us-gov."): + if mid.startswith(prefix): + mid = mid[len(prefix):] + break + # Walk the table in order; longest-prefix match wins. The table is + # already sorted with more-specific entries first + # (``claude-3-5-haiku`` before ``claude-3-haiku``). + best: tuple = ("", _BEDROCK_MAX_TOKENS_DEFAULT) + for prefix, cap in _BEDROCK_MAX_TOKENS_BY_MODEL: + if mid.startswith(prefix) and len(prefix) > len(best[0]): + best = (prefix, cap) + return best[1] + + class AWSBedrock(LLM_Model): def __init__(self, config): super().__init__(config) @@ -45,11 +109,25 @@ def __init__(self, config): "AWS_SECRET_ACCESS_KEY" ], ) + # Resolve ``max_tokens`` so the langchain-aws built-in default + # of 1024 (Anthropic Claude on InvokeModel) doesn't truncate + # large prompts. Priority: + # 1. ``model_kwargs["max_tokens"]`` — explicit per-deployment override + # 2. ``token_limit`` config field — shared with retrieval-side context cap + # 3. Known model-family cap (Claude 3.x, Titan, Cohere, etc.) + # 4. Generic fallback: 8192 + merged_kwargs = dict(config.get("model_kwargs") or {"temperature": 0}) + if "max_tokens" not in merged_kwargs: + cfg_limit = config.get("token_limit") + if isinstance(cfg_limit, int) and cfg_limit > 0: + merged_kwargs["max_tokens"] = cfg_limit + else: + merged_kwargs["max_tokens"] = _bedrock_max_tokens_for_model(model_name) self.llm = ChatBedrock( client=client, model_id=model_name, region_name=config.get("region_name", "us-east-1"), - model_kwargs=config.get("model_kwargs", {"temperature": 0}), + model_kwargs=merged_kwargs, ) self.prompt_path = config["prompt_path"] diff --git a/common/llm_services/base_llm.py b/common/llm_services/base_llm.py index ba1c770..bf24588 100644 --- a/common/llm_services/base_llm.py +++ b/common/llm_services/base_llm.py @@ -23,6 +23,42 @@ logger = logging.getLogger(__name__) +# Per-request collector for LLM usage so callers (e.g. agent trace logs) can +# aggregate token usage without breaking the existing return signatures. +# It's a context-local list the agent resets before each node executes. +import contextvars as _contextvars + +_usage_collector: _contextvars.ContextVar = _contextvars.ContextVar( + "llm_usage_collector", default=None +) + + +def start_usage_collection(): + """Begin collecting LLM usage for the current context (per node).""" + _usage_collector.set([]) + + +def get_collected_usage(): + """Return the usage entries collected since the last start (or None).""" + return _usage_collector.get() + + +def reset_usage_collection(): + """Drop any accumulated usage and disable collection for this context. + + Must be called at the end of a request (success or failure) so stale + usage data doesn't bleed into the next request that runs on the same + thread (sync FastAPI handlers re-use worker threads from a pool). + """ + _usage_collector.set(None) + + +def _record_usage(caller_name: str, usage_data: dict): + bucket = _usage_collector.get() + if bucket is not None: + bucket.append({"caller_name": caller_name, **usage_data}) + + class LLM_Model: """Base LLM_Model Class @@ -95,6 +131,7 @@ def invoke_with_parser( usage_data["total_tokens"] = cb.total_tokens usage_data["cost"] = cb.total_cost logger.info(f"{caller_name} usage: {usage_data}") + _record_usage(caller_name, usage_data) raw_text = raw_output.content if hasattr(raw_output, "content") else str(raw_output) @@ -131,6 +168,7 @@ async def ainvoke_with_parser( usage_data["total_tokens"] = cb.total_tokens usage_data["cost"] = cb.total_cost logger.info(f"{caller_name} usage: {usage_data}") + _record_usage(caller_name, usage_data) raw_text = raw_output.content if hasattr(raw_output, "content") else str(raw_output) @@ -146,52 +184,185 @@ async def ainvoke_with_parser( @property def map_question_schema_prompt(self): """Property to get the prompt for the MapQuestionToSchema tool.""" - return self._read_prompt_file(self.prompt_path + "map_question_to_schema.txt") + result = self._read_prompt_file(self.prompt_path + "map_question_to_schema.txt") + if result is not None: + return result + return """# Map Question to Schema + +Replace each entity in the question with its corresponding **vertex type name**, and each relationship with its corresponding **edge type name**, using the canonical schema names in the Inputs section below. + +## Rules +- If an entity (e.g. "John Doe") is referred to by different names or pronouns ("Joe", "he"), use the most complete identifier ("John Doe") consistently. +- Choose the better mapping between a vertex type and one of its attributes. +- Ensure entities are either source or target vertices of the chosen relationships. +- If an entity maps to a vertex attribute, consider generating a `WHERE` clause. +- For synonyms, output the canonical form from the schema choices. +- Generate the **complete** rewritten question. Keep the case of schema elements unchanged. +- Do NOT generate `target_vertex_ids` unless the term `id` is explicitly mentioned in the question. + +{query_guidance} + +## Inputs +- **Vertices**: {vertices} +- **Vertex attributes**: {verticesAttrs} +- **Edges**: {edges} +- **Edge source/target**: {edgesInfo} +- **Question**: {question} +- **Conversation**: {conversation} + +{format_instructions} +""" @property def generate_function_prompt(self): """Property to get the prompt for the GenerateFunction tool.""" - return self._read_prompt_file(self.prompt_path + "generate_function.txt") + result = self._read_prompt_file(self.prompt_path + "generate_function.txt") + if result is not None: + return result + return """# pyTigerGraph Function Selection + +Use the schema below to write the pyTigerGraph function call that answers the question via a `pyTigerGraph` connection. + +## Selection Rules +- For "how many", counts, totals, or graph-DB statistics, always pick a function whose name contains `Count` (e.g. `getVertexCount`, `getEdgeCount`). +- Never pick a function not described in the docstrings below. +- If entities map to vertex attributes, consider a `WHERE` clause. +- When constructing `WHERE`, quote string attribute values properly. Example: `('Person', where='name="William Torres"')` — applies to every string attribute (name, email, address, etc.). +- Do NOT generate `target_vertex_ids` unless the term `id` is explicitly mentioned in the question. +- Pick exactly **one** function to execute. + +{query_guidance} + +## Schema +- **Vertex Types**: {vertex_types} +- **Vertex Attributes**: {vertex_attributes} +- **Vertex IDs**: {vertex_ids} +- **Edge Types**: {edge_types} +- **Edge Attributes**: {edge_attributes} + +## Question +{question} + +## Reference Docstrings +1. {doc1} +2. {doc2} +3. {doc3} +4. {doc4} +5. {doc5} +6. {doc6} +7. {doc7} +8. {doc8} + +## Output +- If the function output answers the user's question, return that answer immediately. +- Output **valid JSON only** — no extra text would render the response invalid. + +{format_instructions} +""" @property def entity_relationship_extraction_prompt(self): """Property to get the prompt for the EntityRelationshipExtraction tool.""" - return self._read_prompt_file( + result = self._read_prompt_file( self.prompt_path + "entity_relationship_extraction.txt" ) - - @property - def generate_cypher_prompt(self): - """Property to get the prompt for the GenerateCypher tool.""" - result = self._read_prompt_file(self.prompt_path + "generate_cypher.txt") if result is not None: return result - return """You're an expert in OpenCypher programming. Given the following schema and history, what is the OpenCypher query that retrieves the {question} - Only include attributes that are found in the schema. Never include any attributes that are not found in the schema. - Use attributes instead of primary id if attribute name is closer to the keyword type in the question. - Use as less vertex type, edge type and attributes as possible. If an attribute is not found in the schema, please exclude it from the query. - Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex. - Never use directed edge pattern in the OpenCypher query. Always use and create query using undirected pattern. - Always use double quotes for strings instead of single quotes. + return """# Knowledge Graph Extraction + +You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph. + +## Faithfulness — Most Important Rule +- Only emit entities, relationships, definitions, and attribute values that are **explicitly stated in the input text**. +- Do NOT include information from your general knowledge, training data, or background context about well-known entities. +- If a fact is not in the text, leave the corresponding field empty or omit the attribute — never guess, infer, or fill from outside knowledge. +- A short, faithful description is always better than a long description that adds plausible-sounding facts. + +## Goals +- **Nodes** represent entities, concepts, and properties of entities. +- Aim for simplicity and clarity so the graph is accessible to a vast audience. + +## Node Labeling +- **Consistency**: use basic or elementary types. Label a person as `person`, not `mathematician` / `scientist`. +- **Node IDs**: never use integers. Use names or human-readable identifiers found in the text. - Avoid generating invalid OpenCypher queries based on the errors from history below. +## Numerical Data and Dates +- Incorporate as **attributes / properties** of the respective nodes. +- Do NOT create separate nodes for dates or numerical values. +- Properties are key-value. Use properties only for dates and numbers; string properties become new nodes. +- Only include numerical or date values that are **explicitly written in the input text** — do NOT compute, estimate, or recall from memory. +- Never use escaped single or double quotes within property values. +- Use `camelCase` for property keys (e.g. `birthDate`). - Schema: {schema} - History: {history} +## Coreference Resolution +- Maintain entity consistency: if "John Doe" is referred to as "Joe" or "he", always use the most complete identifier (`John Doe`) throughout. - You cannot use the following clauses: - OPTIONAL MATCH - CREATE - MERGE - REMOVE - UNION - UNION ALL - UNWIND - SET +## Strict Compliance +- Follow these rules strictly. Non-compliance, including poor formatting, results in termination. - Make sure to have correct attribute names in the OpenCypher query and not to name result aliases that are vertex or edge types. +## No-Relationship Nodes +- Include nodes that have no relationships. Add the node and leave the relationships section empty.""" - ONLY write the OpenCypher query in the response. Do not include any other information in the response.""" + @property + def generate_cypher_prompt(self): + """Property to get the prompt for the GenerateCypher tool.""" + result = self._read_prompt_file(self.prompt_path + "generate_cypher.txt") + if result is not None: + return result + return """# OpenCypher Query Generation + +You are an expert in OpenCypher. Generate the best query that retrieves the answer to: **{question}**. + +## Schema and History +- **Schema**: {schema} +- **History**: {history} + +## Construction Rules +- Distinguish entity **value** from entity **type** carefully. +- Remove duplicate words with the same meaning in the question. +- Only use attributes that exist in the schema. Pick the closest matching attribute name when multiple candidates exist. +- Prefer attributes over primary IDs when an attribute name is more similar to the keyword in the question. +- Keep the query minimal — fewest vertex types, edge types, and attributes possible. +- Do NOT return attributes that aren't explicitly mentioned in the question. If only a vertex is mentioned, return only the vertex. +- Always include the entity from the `WHERE` clause in the final `RETURN`. Use vertex name over ID when available. +- Always use **undirected** edge patterns. Ensure edges connect correct vertex types per schema. +- Use **double quotes** for strings. +- For string comparisons in `WHERE`, convert with `toLower()`. +- Use multi-word, underscore-joined aliases for `ORDER BY`. Aliases / attributes used in `ORDER BY` must be in `RETURN`. Always specify `ASC` / `DESC` based on data type. +- For "summarize" / "write a summary" questions, fetch all neighbour nodes and edges. +- Avoid invalid queries based on errors in the history above. + +{query_guidance} + +## Supported +- **Clauses**: `MATCH`, `OPTIONAL MATCH`, `MANDATORY MATCH`, `WHERE`, `RETURN`, `WITH`, `ORDER BY`, `SKIP`, `LIMIT`, `DELETE`, `DETACH DELETE` +- **Operators**: + - Math: `+`, `-`, `*`, `/`, `%`, `^` + - Comparison: `=`, `<`, `<=`, `>`, `>=`, `<>`, `IS NULL`, `IS NOT NULL` + - Boolean: `AND`, `OR`, `NOT`, `XOR` + - String / list: `CONTAINS`, `STARTS WITH`, `ENDS WITH`, `IN`, `DISTINCT`, `[ ]`, `.` +- **Functions**: + - Aggregation: `count`, `sum`, `avg`, `min`, `max`, `stDev`, `stDevP` + - Math: `abs`, `sqrt`, `log`, `exp`, `sin`, `cos`, `tan`, `radians`, `degrees` + - String: `left`, `right`, `substring`, `replace`, `trim`, `toLower`, `toUpper`, `split` + - List: `head`, `last`, `size`, `range`, `coalesce`, `tail` + - Other: `id`, `elementId`, `labels`, `properties`, `timestamp` +- **Expressions**: `CASE` + +## Unsupported +- **Clauses**: `CALL`, `CREATE`, `MERGE`, `REMOVE`, `SET`, `UNION`, `UNION ALL`, `UNWIND` +- **Functions**: `collect`, `exists`, `keys`, `nodes`, `relationships`, `length`, `percentileCont`, `percentileDisc`, `startNode`, `endNode`, `reverse` (list form) +- **Syntax limits**: + - `WITH` must group by exactly one vertex variable. + - Path variables (`p = (...)`) not supported. + - `MATCH` must reference variables from prior `WITH`. + - Disconnected `MATCH` fragments not supported. + +## Output +- The query must return both the entity from the question AND the requested data. +- Validate syntax before responding. +- Aliases must NOT match vertex / edge types, operator / function names, or reserved keywords. Use multi-word underscore identifiers. +- Output ONLY the OpenCypher query — no explanation.""" @property def generate_gsql_prompt(self): @@ -199,64 +370,100 @@ def generate_gsql_prompt(self): result = self._read_prompt_file(self.prompt_path + "generate_gsql.txt") if result is not None: return result - return """You're an expert in GSQL (Graph SQL) programming for TigerGraph. Given the following schema: {schema}, what is the GSQL query that retrieves the answer for question: {question} - Only include attributes that are found in the schema. Never include any attributes that are not found in the schema. - Use attributes instead of primary id if attribute name is more similar to the keyword type in the question. - Use as few vertex types, edge types and attributes as possible. If an attribute is not found in the schema, please exclude it from the query. - Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex. - Always use double quotes for strings instead of single quotes. - Use alias for ORDER BY if any, and make sure the alias or attributes used in ORDER BY is also in PRINT. Always add ASC or DESC for ORDER BY based on data type. - - Avoid generating invalid GSQL queries based on the errors from history below. - - Schema: {schema} - History: {history} - - Additionally, you cannot use the following clauses: - CREATE - DELETE - INSERT - UPDATE - UPSERT - - Here's some commonly used abbreviations: - dt -> date - pct -> percentage - qty -> quantity - lng -> longitude - cm -> Contract Manufacturer - - Always make the GSQL query returns the entity in the original question together with the data to be queried. - Make sure to have correct attribute names in the GSQL query and not to name result aliases that are vertex or edge types, operator or function names, and other reserved keywords, always construct alias with multiple words connected with underscore. - - ONLY write the GSQL query in the response. Do not include any other information in the response.""" + return """# GSQL Query Generation + +You are an expert in TigerGraph GSQL. Generate the GSQL query that retrieves the answer to: **{question}**. + +## Schema and History +- **Schema**: {schema} +- **History**: {history} + +## Construction Rules +- Only use attributes in the schema. Never invent attributes. +- Prefer attributes over primary IDs when the attribute name is more similar to a keyword in the question. +- Keep the query minimal — fewest vertex types, edge types, and attributes possible. +- Do NOT return attributes the question doesn't mention. If only a vertex is mentioned, return only the vertex. +- Always use **double quotes** for strings. +- Use aliases for `ORDER BY`. Aliases / attributes used in `ORDER BY` must also be in `PRINT`. Always specify `ASC` / `DESC` based on data type. +- Avoid invalid queries based on errors in the history above. + +{query_guidance} + +## Unsupported +- **Clauses**: `CREATE`, `DELETE`, `INSERT`, `UPDATE`, `UPSERT` + +## Output +- The query must return both the entity from the question AND the requested data. +- Aliases must NOT match vertex / edge types, operator / function names, or reserved keywords. Use multi-word underscore identifiers. +- Output ONLY the GSQL query — no explanation.""" @property def route_response_prompt(self): """Property to get the prompt for the RouteResponse tool.""" result = self._read_prompt_file(self.prompt_path + "route_response.txt") + if result is not None: + return result + return """# Route the Question + +Route the user question to one of: `functions`, `vectorstore`, or `history`. + +## Routing +- **`history`**: questions similar to previous ones, or that reference earlier answers / responses, or that refer to the same entities mentioned in a previous answer. +- **`vectorstore`**: questions best answered by text documents. +- **`functions`**: questions about structured data or operations on structured data. Available entities: {v_types}; relationships: {e_types}. Some "how many documents are there?" style questions can be answered here. + +## Mandatory `functions` Routing +Any question about graph database **statistics or metadata** MUST route to `functions`: +- Counts of vertices / nodes / edges (e.g. "how many edges in the graph"). +- Listing or describing vertex / edge types, schema, or graph structure. +- Aggregations, totals, or summaries of data in the graph database. +- Any question mentioning "graph", "graph db", "graph database", "vertices", "nodes", or "edges" in the context of statistics / counts. + +These are **database queries, not document lookups** — always route them to `functions`. + +Otherwise, route to `vectorstore`. + +## Output +Return JSON with a single key `datasource` (value: `functions`, `vectorstore`, or `history`). No preamble or explanation. + +## Inputs +- **Question**: {question} +- **Conversation history**: {conversation} + +{format_instructions}""" + + @property + def select_retriever_prompt(self): + """Property to get the prompt for the auto-select retriever (RetrieverSelector Stage B). + + Returns the user-facing prompt template; the parser injects format_instructions. + """ + result = self._read_prompt_file(self.prompt_path + "select_retriever.txt") if result is not None: return result return """\ -You are an expert at routing a user question to a vectorstore, function calls, or conversation history. -Use the conversation history for questions that are similar to previous ones or that reference earlier answers or responses. -Use the vectorstore for questions that would be best suited by text documents. -Use the function calls for questions that ask about structured data, or operations on structured data. -Questions referring to same entities in a previous, earlier, or above answer or response should be routed to the conversation history. -Keep in mind that some questions about documents such as "how many documents are there?" can be answered by function calls. -The function calls can be used to answer questions about these entities: {v_types} and relationships: {e_types}. -IMPORTANT: Questions about graph database statistics or metadata MUST be routed to function calls. This includes: -- Counting vertices/nodes/edges (e.g. "how many vertices are there", "how many edges in the graph") -- Listing or describing vertex/edge types, schema, or graph structure -- Aggregations, totals, or summaries of data stored in the graph database -- Any question mentioning "graph", "graph db", "graph database", "vertices", "nodes", or "edges" in the context of statistics or counts -These are database queries, NOT document lookups — always route them to function calls. -Otherwise, use vectorstore. Choose one of 'functions', 'vectorstore', or 'history' based on the question and conversation history. -Return a JSON with a single key 'datasource' and no preamble or explanation. -Question to route: {question} -Conversation history: {conversation} -Format: {format_instructions}\ -""" +You are choosing the best retrieval strategy for a knowledge-graph question. +Pick exactly one of: similarity, contextual, hybrid, community. + +Methods: +- similarity: a single fact / definition / quote; the answer lives in one passage. Cheapest. Pick this for short factoid questions about a single entity. +- contextual: needs surrounding narrative (a process, a sequence, cause-and-effect). Returns matching chunks plus their lookback/lookahead siblings. +- hybrid: needs relationships between named entities or multi-hop reasoning. Returns matching chunks plus graph-expansion to nearby entities. +- community: global, thematic, or aggregate questions over the whole corpus ("main themes", "what topics are covered", "summarize the documents"). Returns community summaries instead of chunks. + +Important constraints: +- similarity returns a strict subset of contextual and hybrid (same vector hits, no expansion). Do NOT pick similarity if the question needs context or relationships — pick contextual or hybrid instead. +- community is the only method that operates on community summaries. Pick it ONLY for global/thematic questions; do not pick it for questions about specific named entities. + +Schema context — the knowledge graph contains these entity types: {v_types} +And these relationship types: {e_types} + +Question: {question} +Conversation history (last 2 turns, may be empty): {conversation} + +Return JSON: {{"method": "", "reason": "<≤20 words explaining the pick>"}} + +Format: {format_instructions}""" @property def hyde_prompt(self): @@ -264,8 +471,13 @@ def hyde_prompt(self): result = self._read_prompt_file(self.prompt_path + "hyde.txt") if result is not None: return result - return """You are a helpful agent that is writing an example of a document that might answer this question: {question} - Answer:""" + return """# Hypothetical Document + +Write an example of a document that might answer this question. + +**Question**: {question} + +**Answer**:""" @property def chatbot_response_prompt(self): @@ -273,13 +485,27 @@ def chatbot_response_prompt(self): result = self._read_prompt_file(self.prompt_path + "chatbot_response.txt") if result is not None: return result - return """Given the answer context in JSON format, rephrase it to answer the question. \n - Use only the provided information in context without adding any reasoning or additional logic. \n - Make sure all information in the answer are covered in the generated answer.\n - - Question: {question} \n - Answer: {context} \n - Format: {format_instructions}""" + return """# AI-Powered Knowledge Graph Assistant + +You are a highly efficient, empathetic, and professional AI assistant. Use the provided contexts to answer the user's question. + +## Rules +- The contexts arrive as JSON key-context pairs. **Combine and rephrase** them to answer the question. +- **Score** each context for relevance and use only the high-scoring ones — do not invent additional logic. +- **Cover** the relevant information, especially image references that carry critical visual information. +- **Preserve** image links exactly as `![description](url)` in the final answer when used. Do NOT modify or omit them. +- **Format** the answer in Markdown — titles, paragraphs, bulleted / numbered lists, images, and tables. Place images and tables below the related text section. +- **Tables**: every row, including the header, starts on a new line. +- **Output as JSON** — escape characters as needed so the response is valid JSON. Include every field required by the format instructions; set unknown fields to empty. +- Treat context keys as citations only when asked; otherwise do NOT include citations in the final answer. + +## Inputs +- **Question**: {question} +- **Contexts**: {context} +- **Query**: {query} + +{format_instructions} +""" @property def keyword_extraction_prompt(self): @@ -287,7 +513,20 @@ def keyword_extraction_prompt(self): result = self._read_prompt_file(self.prompt_path + "keyword_extraction.txt") if result is not None: return result - return """You are a helpful assistant responsible for extracting key terms (glossary) from all the questions below to represent their original meaning as much as possible. Each term should only contain a couple of words. Include a quality score for the each extracted glossary, based on how important and frequent it's in the given questions. The quality score should range from 0 (poor) to 100 (excellent), with higher scores indicating terms that are both significant and frequent in the context of the questions.\nThe output should only contain the extracted terms and their quality scores using the required format.\n\nQuestion: {question}\n\n{format_instructions}\n""" + return """# Keyword Extraction + +Extract key terms (glossary) from the question(s) below to represent their original meaning as faithfully as possible. + +## Rules +- Each term should contain only a couple of words. +- Score each extracted term **0 (poor)** to **100 (excellent)** based on how important and frequent it is in the question(s). Higher scores indicate terms that are both significant and frequent. +- Output ONLY the extracted terms with their quality scores in the required format. + +## Question +{question} + +{format_instructions} +""" @property def question_expansion_prompt(self): @@ -295,7 +534,18 @@ def question_expansion_prompt(self): result = self._read_prompt_file(self.prompt_path + "question_expansion.txt") if result is not None: return result - return """You are a helpful assistant responsible for generating 10 new questions similar to the original question below to represent its meaning in a more clear way.\nInclude a quality score for the answer, based on how well it represents the meaning of the original question. The quality score should be between 0 (poor) and 100 (excellent).\n\nQuestion: {question}\n\n{format_instructions}\n""" + return """# Question Expansion + +Generate **10 new questions** similar to the original question below to express its meaning more clearly. + +## Scoring +Include a quality score per generated question, **0 (poor)** to **100 (excellent)**, based on how well it represents the meaning of the original question. + +## Question +{question} + +{format_instructions} +""" @property def graphrag_scoring_prompt(self): @@ -303,7 +553,19 @@ def graphrag_scoring_prompt(self): result = self._read_prompt_file(self.prompt_path + "graphrag_scoring.txt") if result is not None: return result - return """You are a helpful assistant responsible for generating an answer to the question below using the data provided.\nInclude a quality score for the answer, based on how well it answers the question. The quality score should be between 0 (poor) and 100 (excellent).\n\nQuestion: {question}\nContext: {context}\n\n{format_instructions}\n""" + return """# Quality-Scored Answer + +Generate an answer to the question below using the provided data, and include a quality score. + +## Scoring +The quality score is between **0 (poor)** and **100 (excellent)**, based on how well the answer addresses the question. + +## Inputs +- **Question**: {question} +- **Context**: {context} + +{format_instructions} +""" @property def community_summarize_prompt(self): @@ -311,9 +573,93 @@ def community_summarize_prompt(self): result = self._read_prompt_file(self.prompt_path + "community_summarization.txt") if result is not None: return result - raise FileNotFoundError( - f"Community summarization prompt file not found in {self.prompt_path}. " - "Please ensure community_summarization.txt exists in the configured prompt path." + return """# Community Summary + +Generate a comprehensive summary of the data below. + +## Rules +- Concatenate the descriptions into a single, comprehensive summary that includes information from **all** descriptions. +- Resolve contradictions; do NOT add information that is not in the descriptions. +- Write in **third person** and include the entity name(s) for full context. + +## Data +- **Community Title**: {entity_name} +- **Description List**: {description_list} +""" + + @property + def schema_extraction_prompt(self): + """Property to get the prompt for sample-doc schema extraction.""" + result = self._read_prompt_file(self.prompt_path + "schema_extraction.txt") + if result is not None: + return result + return """# Schema Extraction + +You are a knowledge-graph schema architect. From the sample documents provided in the Inputs section below, produce a domain schema as TigerGraph GSQL `VERTEX` / `DIRECTED EDGE` / `UNDIRECTED EDGE` declarations (no leading `ADD`). Return GSQL only — no fences, no commentary, no JSON. + +## Rules + +1. **Vertex inclusion**: a vertex type's instances must be individuated in the source (each instance has its own identity), appear **2+ times**, and have at least one natural attribute beyond `name`. Concrete or conceptual is fine. Skip categorical wrappers — names ending in `_record`, `_management`, `_context`, `_grouping`, or labels of classes-of-classes. +2. **Skip layout**: do NOT produce types for axes, page numbers, captions, table cells, or other document-rendering artifacts. +3. **Edge naming**: use a specific action verb. Include an edge type ONLY IF the source documents contain **2+ concrete instances** of that relationship between named entities — do NOT propose merely-plausible edges. Avoid generic edges (`RELATED_TO`, `CONNECTED_TO`, `ASSOCIATED_WITH`, `HAS`, `BELONGS_TO`). Use `DIRECTED EDGE` for asymmetric verbs and `UNDIRECTED EDGE` only for genuinely symmetric peer relationships. +4. **Reserved names**: do NOT use a name (case-insensitive) matching any of the reserved structural types or GSQL keywords listed in the Inputs section. Pick a synonym or qualifier (e.g. `KeywordRecord`). +5. **Attributes**: each `VERTEX` has **1–10** attributes; each `EDGE` has **0–5**. Primitive types only: `STRING`, `INT`, `UINT`, `DOUBLE`, `FLOAT`, `BOOL`, `DATETIME`. Do NOT include any id / primary-key field. +6. **Comments**: every `VERTEX` and `EDGE` MUST be preceded by exactly one `// ` line. +7. **Size**: produce at least 8 vertex types. Emit every edge type that rule 3 supports — no upper bound on edge count, but every edge must earn its place via 2+ concrete instances in the source documents. + +## Example Output (illustrative — pick names that fit YOUR documents) + + // A natural person referenced in the documents. + VERTEX Person(name STRING, role STRING); + + // An organization or institutional body. + VERTEX Organization(name STRING, founded_at DATETIME); + + // A person works for an organization in a given role. + DIRECTED EDGE WORKS_FOR(FROM Person, TO Organization, role STRING); + + // Two people are colleagues — symmetric peer relationship. + UNDIRECTED EDGE COLLEAGUE_OF(FROM Person, TO Person); + +## Inputs +- **Reserved structural types** (case-insensitive): {structural_types} +- **Reserved GSQL keywords** (case-insensitive): {tg_keywords} +- **Sample documents**: + +{samples} +""" + + @property + def query_guidance_prompt(self): + """User-editable Query Guidance partial. Domain-specific + instructions / few-shot examples the user provides on the + Customize Prompts page. Injected into the four query-related + templates (map_question_to_schema, generate_function, + generate_cypher, generate_gsql) *after* their hard rules so + the LLM treats the guidance as advisory. + + Default is the empty string — the four templates render + unchanged from their pre-Query-Guidance form when no override + is configured. + """ + result = self._read_prompt_file(self.prompt_path + "query_guidance.txt") + return (result or "").strip() + + @property + def query_guidance_block(self): + """Wrap ``query_guidance_prompt`` in a markdown section so it + drops cleanly into a downstream template. Returns an empty + string when no guidance is configured — keeps the surrounding + prompts identical to today's behavior on the empty path. + """ + text = self.query_guidance_prompt + if not text: + return "" + return ( + "## Domain Hints\n" + "Use the following hints only when they do not conflict with the " + "rules above:\n\n" + f"{text}\n" ) @property @@ -325,13 +671,18 @@ def contextualize_question_prompt(self): ) if result is not None: return result - return ( - "Given the following conversation history and a follow-up " - "question, rewrite the follow-up question into a standalone, " - "self-contained question suitable for searching a knowledge " - "graph. Do NOT answer the question; only rewrite it.\n\n" - "Conversation history:\n{history}\n\n" - "Follow-up question: {question}\n\n" - "Standalone question:" - ) + return """# Standalone Question Rewrite + +Given the conversation history and a follow-up question, rewrite the follow-up into a **standalone, self-contained** question suitable for searching a knowledge graph. + +Do **NOT** answer the question — only rewrite it. + +## Conversation History +{history} + +## Follow-up Question +{question} + +## Standalone Question +""" diff --git a/common/metrics/prometheus_metrics.py b/common/metrics/prometheus_metrics.py index 0662872..ee671be 100644 --- a/common/metrics/prometheus_metrics.py +++ b/common/metrics/prometheus_metrics.py @@ -72,6 +72,11 @@ def __init__(self): "Number of LLM responses that yielded an error result", ["llm_model"], ) + self.llm_method_selection_total = Counter( + "llm_method_selection_total", + "Number of times each retrieval method was selected (auto + manual)", + ["selected_method", "selection_source"], + ) # collect metrics for TigerGraph self.tigergraph_active_connections = Gauge( diff --git a/common/prompts/aws_bedrock_claude3haiku/chatbot_response.txt b/common/prompts/aws_bedrock_claude3haiku/chatbot_response.txt deleted file mode 100644 index 6acdaf5..0000000 --- a/common/prompts/aws_bedrock_claude3haiku/chatbot_response.txt +++ /dev/null @@ -1,17 +0,0 @@ -You are a highly efficient and empathetic AI-powered knowledge graph assistant. Your goal is to provide accurate, helpful, and friendly response while maintaining professionalism. - -Follow these guidelines: -- Give the contexts in JSON format contains key-context pairs, combine and rephrase it to answer the question. -- Score the contexts for their relevance to the question and use only the information of the high-scoring contexts without adding extra logic. -- Make sure most relevant information in the provided contexts are covered in the generated answer, especially image references providing critical visual information. -- Make sure to preserve the image links in markdown syntax "![description](url)" with its orignal format in the final answer if the context contains the links are used in the response. Do NOT modify or omit these image references. -- Use markdown syntax to geneate the answer, including title, paragraphs, bulleted or numbered list, images and tables if any, and place images or tables below the related text section. -- Ensure that each row of every table, including the header row, starts on a new line. -- Generate the answer in JSON format, make sure to escape necessary characters in order to return a valid JSON response only. -- Make sure all the fields required by the format instructions are included, set a field to empty if you don't have that information. -- Use the keys of the contexts used as citations if asked, DO NOT include citations in the final answer - -Question: {question} -Contexts: {context} -Query: {query} -Format: {format_instructions} diff --git a/common/prompts/aws_bedrock_claude3haiku/community_summarization.txt b/common/prompts/aws_bedrock_claude3haiku/community_summarization.txt deleted file mode 100644 index 50e4619..0000000 --- a/common/prompts/aws_bedrock_claude3haiku/community_summarization.txt +++ /dev/null @@ -1,11 +0,0 @@ -You are a helpful assistant responsible for generating a comprehensive summary of the data provided below. -Given one or two entities, and a list of descriptions, all related to the same entity or group of entities. -Please concatenate all of these into a single, comprehensive description. Make sure to include information collected from all the descriptions. -If the provided descriptions are contradictory, please resolve the contradictions and provide a single, coherent summary, but do not add any information that is not in the description. -Make sure it is written in third person, and include the entity names so we the have full context. - -####### --Data- -Commuinty Title: {entity_name} -Description List: {description_list} - diff --git a/common/prompts/aws_bedrock_claude3haiku/entity_relationship_extraction.txt b/common/prompts/aws_bedrock_claude3haiku/entity_relationship_extraction.txt deleted file mode 100644 index 852dded..0000000 --- a/common/prompts/aws_bedrock_claude3haiku/entity_relationship_extraction.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Knowledge Graph Instructions for GPT-4 -## 1. Overview -You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph. -- **Nodes** represent entities, concepts, and properties of entities. -- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience. -## 2. Labeling Nodes -- **Consistency**: Ensure you use basic or elementary types for node labels. -- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist". -- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text. -## 3. Handling Numerical Data and Dates -- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes. -- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes. -- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes. -- **Quotation Marks**: Never use escaped single or double quotes within property values. -- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`. -## 4. Coreference Resolution -- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency. -If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID. -Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial. -## 5. Strict Compliance -Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting. -## 6. Handling Instances with No Relationships -If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty. \ No newline at end of file diff --git a/common/prompts/aws_bedrock_claude3haiku/generate_cypher.txt b/common/prompts/aws_bedrock_claude3haiku/generate_cypher.txt deleted file mode 100644 index 732ed49..0000000 --- a/common/prompts/aws_bedrock_claude3haiku/generate_cypher.txt +++ /dev/null @@ -1,85 +0,0 @@ -You're an expert in OpenCypher programming. Given the following schema, find the best OpenCypher query that retrieves the answer for question {question}. -If there're multiple words in the question having same meaning then remove the duplication. -Always carefully distinguish entity value from entity type. For example, "MAC LOB" is referring to a LOB named "MAC" because there is a vertex type Lob matching the word "LOB". -Only include attributes that are found in the schema. Never include any attributes that are not found in the schema. -Use attributes instead of primary id if attribute name is more similar to the keyword type in the question. Always use the closest attribute name when there're multiple candidates. -Use as less vertex type, edge type and attributes as possible. If an attribute is not found in the schema, please exclude it from the query. -Always make sure the attributes used exist in the vertex type or edge type referenced, DO NOT use an attribute that does not exist in the vertex or edge from the schema. -Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex. -Always include the entity from the WHERE clause to the final RETURN result. Use vertex name instead of ID whenever available. -Never use directed edge pattern in the OpenCypher query. Always use and create query using undirected pattern. Always ensure the edge used starts from and ends with correct vertex types matching the schema. -Always use double quotes for strings instead of single quotes. -Always convert strings to lower case using toLower() function for string comparision in WHERE clause. -Use alias for ORDER BY if any, avoid using short alias names especially single letter alias, always use meaningful words connected by underscore. -Always make sure the alias or attributes used in ORDER BY is the same type in RETURN. Always add ASC or DESC for ORDER BY based on data type. -For questions like "summarize" or "write a summary" about something, fetch all information on its neighbour nodes and edges. - -Avoid to generate invalid OpenCypher queries based on the errors from history below. - -Schema: {schema} -History: {history} - -Only use the Supported Clauses, Operators, Functions and Expressions below but do not use any of the Unsupported Features, Functions or Syntax Limitations below: - -Supported Clauses: -MATCH / OPTIONAL MATCH / MANDATORY MATCH: Match patterns in the graph. -WHERE: Filter results. -RETURN / WITH: Project query results, alias fields, chain query parts. -ORDER BY / SKIP / LIMIT: Control output order, offset, and size. -DELETE / DETACH DELETE: Delete nodes/edges. - -Supported Operators: -Mathematical: +, -, *, /, %, ^ (exponent) -Comparison: =, <, <=, >, >=, <>, IS NULL, IS NOT NULL -Boolean: AND, OR, NOT, XOR -String/List: CONTAINS, STARTS WITH, ENDS WITH, IN, DISTINCT, [ ] (subscript), . (property access) - -Supported Functions: -Aggregation: count(), sum(), avg(), min(), max(), stDev(), stDevP() -Math: abs(), sqrt(), log(), exp(), sin(), cos(), tan(), radians(), degrees() -String: left(), right(), substring(), replace(), trim(), toLower(), toUpper(), split() -List: head(), last(), size(), range(), coalesce(), tail() -Others: id(), elementId(), labels(), properties(), timestamp() - -Supported Expressions: -CASE: Conditional logic. - -Supported Operators: -Comparison: IS NULL, IS NOT NULL - -Unsupported Features: -Clauses Not Yet Supported -CALL, CREATE, MERGE, REMOVE, SET, UNION, UNION ALL, UNWIND - -Unsupported Functions: -collect(), exists(), keys(), nodes(), relationships(), length(), percentileCont(), percentileDisc(), startNode(), endNode(), reverse() (list form) - -Syntax Limitations: -WITH clause must group by exactly one vertex variable. -Path variables (e.g. p = (...)) not supported. -MATCH must reference variables from prior WITH. -Disconnected MATCH fragments not supported. - -Additionally, you cannot use the following clauses: -CREATE -MERGE -REMOVE -UNION -UNION ALL -UNWIND -SET - -Here's some commonly used abbreviations: -dt -> date -wk -> week -yr -> year -pct -> percentage -qty -> quantity -lng -> longitude -cm -> Contract Manufacturer - -Always make the cypher query returns the entity in the original question together with the data to be queried. -Make sure to have correct attribute names in the OpenCypher query and not to name result aliases that are vertex or edge types, operator or function names, and other reserved keywords, always construct alias with multiple words connected with underscore. -Always validate the syntax for the generated OpenCypher query before writing to response. - -ONLY write the OpenCypher query in the response. Do not include any other information in the response. diff --git a/common/prompts/aws_bedrock_claude3haiku/generate_function.txt b/common/prompts/aws_bedrock_claude3haiku/generate_function.txt deleted file mode 100644 index 359b46c..0000000 --- a/common/prompts/aws_bedrock_claude3haiku/generate_function.txt +++ /dev/null @@ -1,27 +0,0 @@ -Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection. -When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted. -For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on. -Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. -Vertex Types: {vertex_types} -Vertex Attributes: {vertex_attributes} -Vertex IDs: {vertex_ids} -Edge Types: {edge_types} -Edge Attributes: {edge_attributes} -Question: {question} -First Docstring: {doc1} -Second Docstring: {doc2} -Third Docstring: {doc3} -Fourth Docstring: {doc4} -Fifth Docstring: {doc5} -Sixth Docstring: {doc6} -Seventh Docstring: {doc7} -Eighth Docstring: {doc8} - -If the output of this function answers the user's question, immediately return that answer. - -Follow the output directions below on how to structure your response -Only include valid JSON do not include any other texts which would render the response invalid JSON. -{format_instructions} diff --git a/common/prompts/aws_bedrock_claude3haiku/graphrag_scoring.txt b/common/prompts/aws_bedrock_claude3haiku/graphrag_scoring.txt deleted file mode 100644 index 38ef643..0000000 --- a/common/prompts/aws_bedrock_claude3haiku/graphrag_scoring.txt +++ /dev/null @@ -1,7 +0,0 @@ -You are a helpful assistant responsible for generating an answer to the question below using the data provided. -Include a quality score for the answer, based on how well it answers the question. The quality score should be between 0 (poor) and 100 (excellent). - -Question: {question} -Context: {context} - -{format_instructions} diff --git a/common/prompts/aws_bedrock_claude3haiku/map_question_to_schema.txt b/common/prompts/aws_bedrock_claude3haiku/map_question_to_schema.txt deleted file mode 100644 index 8e4cf05..0000000 --- a/common/prompts/aws_bedrock_claude3haiku/map_question_to_schema.txt +++ /dev/null @@ -1,14 +0,0 @@ -Replace the entites mentioned in the question to one of these choices: {vertices}. -If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity. Choose a better mapping between vertex type or its attributes: {verticesAttrs}. -Replace the relationships mentioned in the question to one of these choices: {edges}. -Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above. -Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. - -Respond in JSON (and only JSON). Follow the format instructions below: -{format_instructions} -question: {question} -conversation: {conversation} diff --git a/common/prompts/aws_bedrock_titan/generate_function.txt b/common/prompts/aws_bedrock_titan/generate_function.txt deleted file mode 100644 index b0be05c..0000000 --- a/common/prompts/aws_bedrock_titan/generate_function.txt +++ /dev/null @@ -1,14 +0,0 @@ -Use the vertex types, edge types, and their attributes and IDs to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection. -When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted. -For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on. -Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. -Vertex Types: {vertices} -Edge Types: {edges} -Question: {question} -First Docstring: {doc1} -Second Docstring: {doc2} -Third Docstring: {doc3} -Python Call: conn. \ No newline at end of file diff --git a/common/prompts/aws_bedrock_titan/map_question_to_schema.txt b/common/prompts/aws_bedrock_titan/map_question_to_schema.txt deleted file mode 100644 index d9fb173..0000000 --- a/common/prompts/aws_bedrock_titan/map_question_to_schema.txt +++ /dev/null @@ -1,19 +0,0 @@ -Replace the entites mentioned in the question to one of these choices: {vertices}. -If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity. -Choose a better mapping between vertex type or its attributes: {verticesAttrs}. -Replace the relationships mentioned in the question to one of these choices: {edges}. -Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. - -Example: How many universities are there? -Response: How many vertices are University Vetexes? -Example: What is the schema? -Response: What is the schema? -Example: How many transactions are there? -Response: How many TRANSACTION Edges are there? -{format_instructions} -question: {question} -conversation: {conversation} diff --git a/common/prompts/azure_open_ai_gpt35_turbo_instruct/entity_relationship_extraction.txt b/common/prompts/azure_open_ai_gpt35_turbo_instruct/entity_relationship_extraction.txt deleted file mode 100644 index 852dded..0000000 --- a/common/prompts/azure_open_ai_gpt35_turbo_instruct/entity_relationship_extraction.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Knowledge Graph Instructions for GPT-4 -## 1. Overview -You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph. -- **Nodes** represent entities, concepts, and properties of entities. -- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience. -## 2. Labeling Nodes -- **Consistency**: Ensure you use basic or elementary types for node labels. -- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist". -- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text. -## 3. Handling Numerical Data and Dates -- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes. -- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes. -- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes. -- **Quotation Marks**: Never use escaped single or double quotes within property values. -- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`. -## 4. Coreference Resolution -- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency. -If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID. -Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial. -## 5. Strict Compliance -Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting. -## 6. Handling Instances with No Relationships -If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty. \ No newline at end of file diff --git a/common/prompts/azure_open_ai_gpt35_turbo_instruct/generate_function.txt b/common/prompts/azure_open_ai_gpt35_turbo_instruct/generate_function.txt deleted file mode 100644 index e0a83d0..0000000 --- a/common/prompts/azure_open_ai_gpt35_turbo_instruct/generate_function.txt +++ /dev/null @@ -1,29 +0,0 @@ -Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection. -When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted. -For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on. -Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. - -Never add more than one function call in the response, and only use the functions provided below. Do not chain function calls together. -For example, if the correct function is `getVertexCount()` do not use `getVertexCount().limit()`. -If the correct function is `getEdges()` do not use `getEdges().count()`. - -Vertex Types: {vertex_types} -Vertex Attributes: {vertex_attributes} -Vertex IDs: {vertex_ids} -Edge Types: {edge_types} -Edge Attributes: {edge_attributes} -Question: {question} -First Docstring: {doc1} -Second Docstring: {doc2} -Third Docstring: {doc3} -Fourth Docstring: {doc4} -Fifth Docstring: {doc5} -Sixth Docstring: {doc6} -Seventh Docstring: {doc7} -Eighth Docstring: {doc8} - -Follow the output directions below on how to structure your response, make sure to exactly match the output format. -{format_instructions} diff --git a/common/prompts/azure_open_ai_gpt35_turbo_instruct/map_question_to_schema.txt b/common/prompts/azure_open_ai_gpt35_turbo_instruct/map_question_to_schema.txt deleted file mode 100644 index d72726e..0000000 --- a/common/prompts/azure_open_ai_gpt35_turbo_instruct/map_question_to_schema.txt +++ /dev/null @@ -1,17 +0,0 @@ -You are mapping a question from a user to entities represented in a graph database. -The question is: {question} -Replace the entites mentioned in the question to one of these choices: {vertices}. -If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity. -Choose a better mapping between vertex type or its attributes: {verticesAttrs}. -Replace the relationships mentioned in the question to one of these choices: {edges}. -Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above. -Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same. If some entites are mapped to attributes, may consider to generate a where clause. -Format your response following the directions below. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. - -{format_instructions} -question: {question} -conversation: {conversation} \ No newline at end of file diff --git a/common/prompts/custom/aml/chatbot_response.txt b/common/prompts/custom/aml/chatbot_response.txt deleted file mode 100644 index 05532c4..0000000 --- a/common/prompts/custom/aml/chatbot_response.txt +++ /dev/null @@ -1,29 +0,0 @@ -You are a highly efficient and empathetic AI-powered assistant in JSON parsing and generating. -Given the following context in JSON format, rephrase it to answer the question. -Use only the provided information in context without adding any reasoning or additional logic. -Make sure all information in the context are covered in the generated answer. -Make sure to extract and include the image links in markdown syntax in the generated answer when their summaries are referenced, and preserve the link URLs in their original format. -Use compact markdown syntax to geneate the answer, including title, bulleted or numbered list, images and tables if any, and place images or tables below the related text section. -Ensure that each row of every table, including the header row, starts on a new line. -Always only return a JSON contains the answer and otheh required fields after validation. Assign an empty value to the field if you cannot determine it. - -For questions related to financial graph or transaction graph, create a suspicious activity report. -- The narrative should be clear, comprehensive, and avoid institution-specific acronyms. -- The narrative should include paragraphs for Summary of **Investigation**, **Suspicious Activity Overview**, **Details of Suspicious Activities**, **Investigation Conducted**, and **Conclusion** -- The narrative must provide information about the subject to include phone numbers, email addresses, addresses and government IDs. -- The narrative must include any suspicious transactions and the start of the suspicious transactions. -- The narrative must specify the suspicious activity observed (types of transactions, amount, frequency). -- The narrative must tell the complete story. A reviewer should understand the full picture without needing additional context. - -Narrative Writing Guidelines: -- Write in clear, complete sentences -- Spell out all acronyms (no institution-specific jargon) -- Include specific dates, amounts, and account numbers -- Describe the investigation conducted -- Note any customer explanations received and why they were insufficient -- Include any supporting documentation references - -Question: {question} -Context: {context} -Query: {query} -Format: {format_instructions} diff --git a/common/prompts/custom/aml/community_summarization.txt b/common/prompts/custom/aml/community_summarization.txt deleted file mode 100644 index 50e4619..0000000 --- a/common/prompts/custom/aml/community_summarization.txt +++ /dev/null @@ -1,11 +0,0 @@ -You are a helpful assistant responsible for generating a comprehensive summary of the data provided below. -Given one or two entities, and a list of descriptions, all related to the same entity or group of entities. -Please concatenate all of these into a single, comprehensive description. Make sure to include information collected from all the descriptions. -If the provided descriptions are contradictory, please resolve the contradictions and provide a single, coherent summary, but do not add any information that is not in the description. -Make sure it is written in third person, and include the entity names so we the have full context. - -####### --Data- -Commuinty Title: {entity_name} -Description List: {description_list} - diff --git a/common/prompts/custom/aml/entity_relationship_extraction.txt b/common/prompts/custom/aml/entity_relationship_extraction.txt deleted file mode 100644 index 852dded..0000000 --- a/common/prompts/custom/aml/entity_relationship_extraction.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Knowledge Graph Instructions for GPT-4 -## 1. Overview -You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph. -- **Nodes** represent entities, concepts, and properties of entities. -- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience. -## 2. Labeling Nodes -- **Consistency**: Ensure you use basic or elementary types for node labels. -- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist". -- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text. -## 3. Handling Numerical Data and Dates -- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes. -- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes. -- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes. -- **Quotation Marks**: Never use escaped single or double quotes within property values. -- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`. -## 4. Coreference Resolution -- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency. -If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID. -Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial. -## 5. Strict Compliance -Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting. -## 6. Handling Instances with No Relationships -If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty. \ No newline at end of file diff --git a/common/prompts/custom/aml/generate_cypher.txt b/common/prompts/custom/aml/generate_cypher.txt deleted file mode 100644 index 732ed49..0000000 --- a/common/prompts/custom/aml/generate_cypher.txt +++ /dev/null @@ -1,85 +0,0 @@ -You're an expert in OpenCypher programming. Given the following schema, find the best OpenCypher query that retrieves the answer for question {question}. -If there're multiple words in the question having same meaning then remove the duplication. -Always carefully distinguish entity value from entity type. For example, "MAC LOB" is referring to a LOB named "MAC" because there is a vertex type Lob matching the word "LOB". -Only include attributes that are found in the schema. Never include any attributes that are not found in the schema. -Use attributes instead of primary id if attribute name is more similar to the keyword type in the question. Always use the closest attribute name when there're multiple candidates. -Use as less vertex type, edge type and attributes as possible. If an attribute is not found in the schema, please exclude it from the query. -Always make sure the attributes used exist in the vertex type or edge type referenced, DO NOT use an attribute that does not exist in the vertex or edge from the schema. -Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex. -Always include the entity from the WHERE clause to the final RETURN result. Use vertex name instead of ID whenever available. -Never use directed edge pattern in the OpenCypher query. Always use and create query using undirected pattern. Always ensure the edge used starts from and ends with correct vertex types matching the schema. -Always use double quotes for strings instead of single quotes. -Always convert strings to lower case using toLower() function for string comparision in WHERE clause. -Use alias for ORDER BY if any, avoid using short alias names especially single letter alias, always use meaningful words connected by underscore. -Always make sure the alias or attributes used in ORDER BY is the same type in RETURN. Always add ASC or DESC for ORDER BY based on data type. -For questions like "summarize" or "write a summary" about something, fetch all information on its neighbour nodes and edges. - -Avoid to generate invalid OpenCypher queries based on the errors from history below. - -Schema: {schema} -History: {history} - -Only use the Supported Clauses, Operators, Functions and Expressions below but do not use any of the Unsupported Features, Functions or Syntax Limitations below: - -Supported Clauses: -MATCH / OPTIONAL MATCH / MANDATORY MATCH: Match patterns in the graph. -WHERE: Filter results. -RETURN / WITH: Project query results, alias fields, chain query parts. -ORDER BY / SKIP / LIMIT: Control output order, offset, and size. -DELETE / DETACH DELETE: Delete nodes/edges. - -Supported Operators: -Mathematical: +, -, *, /, %, ^ (exponent) -Comparison: =, <, <=, >, >=, <>, IS NULL, IS NOT NULL -Boolean: AND, OR, NOT, XOR -String/List: CONTAINS, STARTS WITH, ENDS WITH, IN, DISTINCT, [ ] (subscript), . (property access) - -Supported Functions: -Aggregation: count(), sum(), avg(), min(), max(), stDev(), stDevP() -Math: abs(), sqrt(), log(), exp(), sin(), cos(), tan(), radians(), degrees() -String: left(), right(), substring(), replace(), trim(), toLower(), toUpper(), split() -List: head(), last(), size(), range(), coalesce(), tail() -Others: id(), elementId(), labels(), properties(), timestamp() - -Supported Expressions: -CASE: Conditional logic. - -Supported Operators: -Comparison: IS NULL, IS NOT NULL - -Unsupported Features: -Clauses Not Yet Supported -CALL, CREATE, MERGE, REMOVE, SET, UNION, UNION ALL, UNWIND - -Unsupported Functions: -collect(), exists(), keys(), nodes(), relationships(), length(), percentileCont(), percentileDisc(), startNode(), endNode(), reverse() (list form) - -Syntax Limitations: -WITH clause must group by exactly one vertex variable. -Path variables (e.g. p = (...)) not supported. -MATCH must reference variables from prior WITH. -Disconnected MATCH fragments not supported. - -Additionally, you cannot use the following clauses: -CREATE -MERGE -REMOVE -UNION -UNION ALL -UNWIND -SET - -Here's some commonly used abbreviations: -dt -> date -wk -> week -yr -> year -pct -> percentage -qty -> quantity -lng -> longitude -cm -> Contract Manufacturer - -Always make the cypher query returns the entity in the original question together with the data to be queried. -Make sure to have correct attribute names in the OpenCypher query and not to name result aliases that are vertex or edge types, operator or function names, and other reserved keywords, always construct alias with multiple words connected with underscore. -Always validate the syntax for the generated OpenCypher query before writing to response. - -ONLY write the OpenCypher query in the response. Do not include any other information in the response. diff --git a/common/prompts/custom/aml/generate_function.txt b/common/prompts/custom/aml/generate_function.txt deleted file mode 100644 index 359b46c..0000000 --- a/common/prompts/custom/aml/generate_function.txt +++ /dev/null @@ -1,27 +0,0 @@ -Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection. -When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted. -For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on. -Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. -Vertex Types: {vertex_types} -Vertex Attributes: {vertex_attributes} -Vertex IDs: {vertex_ids} -Edge Types: {edge_types} -Edge Attributes: {edge_attributes} -Question: {question} -First Docstring: {doc1} -Second Docstring: {doc2} -Third Docstring: {doc3} -Fourth Docstring: {doc4} -Fifth Docstring: {doc5} -Sixth Docstring: {doc6} -Seventh Docstring: {doc7} -Eighth Docstring: {doc8} - -If the output of this function answers the user's question, immediately return that answer. - -Follow the output directions below on how to structure your response -Only include valid JSON do not include any other texts which would render the response invalid JSON. -{format_instructions} diff --git a/common/prompts/custom/aml/graphrag_scoring.txt b/common/prompts/custom/aml/graphrag_scoring.txt deleted file mode 100644 index 38ef643..0000000 --- a/common/prompts/custom/aml/graphrag_scoring.txt +++ /dev/null @@ -1,7 +0,0 @@ -You are a helpful assistant responsible for generating an answer to the question below using the data provided. -Include a quality score for the answer, based on how well it answers the question. The quality score should be between 0 (poor) and 100 (excellent). - -Question: {question} -Context: {context} - -{format_instructions} diff --git a/common/prompts/custom/aml/map_question_to_schema.txt b/common/prompts/custom/aml/map_question_to_schema.txt deleted file mode 100644 index 8e4cf05..0000000 --- a/common/prompts/custom/aml/map_question_to_schema.txt +++ /dev/null @@ -1,14 +0,0 @@ -Replace the entites mentioned in the question to one of these choices: {vertices}. -If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity. Choose a better mapping between vertex type or its attributes: {verticesAttrs}. -Replace the relationships mentioned in the question to one of these choices: {edges}. -Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above. -Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. - -Respond in JSON (and only JSON). Follow the format instructions below: -{format_instructions} -question: {question} -conversation: {conversation} diff --git a/common/prompts/gcp_vertexai_palm/community_summarization.txt b/common/prompts/gcp_vertexai_palm/community_summarization.txt deleted file mode 100644 index 50e4619..0000000 --- a/common/prompts/gcp_vertexai_palm/community_summarization.txt +++ /dev/null @@ -1,11 +0,0 @@ -You are a helpful assistant responsible for generating a comprehensive summary of the data provided below. -Given one or two entities, and a list of descriptions, all related to the same entity or group of entities. -Please concatenate all of these into a single, comprehensive description. Make sure to include information collected from all the descriptions. -If the provided descriptions are contradictory, please resolve the contradictions and provide a single, coherent summary, but do not add any information that is not in the description. -Make sure it is written in third person, and include the entity names so we the have full context. - -####### --Data- -Commuinty Title: {entity_name} -Description List: {description_list} - diff --git a/common/prompts/gcp_vertexai_palm/entity_relationship_extraction.txt b/common/prompts/gcp_vertexai_palm/entity_relationship_extraction.txt deleted file mode 100644 index 852dded..0000000 --- a/common/prompts/gcp_vertexai_palm/entity_relationship_extraction.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Knowledge Graph Instructions for GPT-4 -## 1. Overview -You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph. -- **Nodes** represent entities, concepts, and properties of entities. -- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience. -## 2. Labeling Nodes -- **Consistency**: Ensure you use basic or elementary types for node labels. -- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist". -- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text. -## 3. Handling Numerical Data and Dates -- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes. -- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes. -- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes. -- **Quotation Marks**: Never use escaped single or double quotes within property values. -- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`. -## 4. Coreference Resolution -- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency. -If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID. -Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial. -## 5. Strict Compliance -Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting. -## 6. Handling Instances with No Relationships -If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty. \ No newline at end of file diff --git a/common/prompts/gcp_vertexai_palm/generate_function.txt b/common/prompts/gcp_vertexai_palm/generate_function.txt deleted file mode 100644 index fe7d3cc..0000000 --- a/common/prompts/gcp_vertexai_palm/generate_function.txt +++ /dev/null @@ -1,33 +0,0 @@ -Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection. -When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted. -For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on. -Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. - -Never add more than one function call in the response, and only use the functions provided below. Do not chain function calls together. -For example, if the correct function is `getVertexCount()` do not use `getVertexCount().limit()`. -If the correct function is `getEdges()` do not use `getEdges().count()`. - -Vertex Types: {vertex_types} -Vertex Attributes: {vertex_attributes} -Vertex IDs: {vertex_ids} -Edge Types: {edge_types} -Edge Attributes: {edge_attributes} -Question: {question} -First Docstring: {doc1} -Second Docstring: {doc2} -Third Docstring: {doc3} -Fourth Docstring: {doc4} -Fifth Docstring: {doc5} -Sixth Docstring: {doc6} -Seventh Docstring: {doc7} -Eighth Docstring: {doc8} - -Make sure to carefully read the question and the docstrings to determine the correct function to call. -Choose the simplest function that will answer the question. -Only choose one function to call, and do not change the syntax of the function call. - -Follow the output directions below on how to structure your response: -{format_instructions} diff --git a/common/prompts/gcp_vertexai_palm/map_question_to_schema.txt b/common/prompts/gcp_vertexai_palm/map_question_to_schema.txt deleted file mode 100644 index b9051bc..0000000 --- a/common/prompts/gcp_vertexai_palm/map_question_to_schema.txt +++ /dev/null @@ -1,14 +0,0 @@ -Replace the entites mentioned in the question to one of these choices: {vertices}. -If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity. -Choose a better mapping between vertex type or its attributes: {verticesAttrs}. -Replace the relationships mentioned in the question to one of these choices: {edges}. -Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above. -Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. - -{format_instructions} -question: {question} -conversation: {conversation} diff --git a/common/prompts/google_gemini/chatbot_response.txt b/common/prompts/google_gemini/chatbot_response.txt deleted file mode 100644 index 6acdaf5..0000000 --- a/common/prompts/google_gemini/chatbot_response.txt +++ /dev/null @@ -1,17 +0,0 @@ -You are a highly efficient and empathetic AI-powered knowledge graph assistant. Your goal is to provide accurate, helpful, and friendly response while maintaining professionalism. - -Follow these guidelines: -- Give the contexts in JSON format contains key-context pairs, combine and rephrase it to answer the question. -- Score the contexts for their relevance to the question and use only the information of the high-scoring contexts without adding extra logic. -- Make sure most relevant information in the provided contexts are covered in the generated answer, especially image references providing critical visual information. -- Make sure to preserve the image links in markdown syntax "![description](url)" with its orignal format in the final answer if the context contains the links are used in the response. Do NOT modify or omit these image references. -- Use markdown syntax to geneate the answer, including title, paragraphs, bulleted or numbered list, images and tables if any, and place images or tables below the related text section. -- Ensure that each row of every table, including the header row, starts on a new line. -- Generate the answer in JSON format, make sure to escape necessary characters in order to return a valid JSON response only. -- Make sure all the fields required by the format instructions are included, set a field to empty if you don't have that information. -- Use the keys of the contexts used as citations if asked, DO NOT include citations in the final answer - -Question: {question} -Contexts: {context} -Query: {query} -Format: {format_instructions} diff --git a/common/prompts/google_gemini/community_summarization.txt b/common/prompts/google_gemini/community_summarization.txt deleted file mode 100644 index 50e4619..0000000 --- a/common/prompts/google_gemini/community_summarization.txt +++ /dev/null @@ -1,11 +0,0 @@ -You are a helpful assistant responsible for generating a comprehensive summary of the data provided below. -Given one or two entities, and a list of descriptions, all related to the same entity or group of entities. -Please concatenate all of these into a single, comprehensive description. Make sure to include information collected from all the descriptions. -If the provided descriptions are contradictory, please resolve the contradictions and provide a single, coherent summary, but do not add any information that is not in the description. -Make sure it is written in third person, and include the entity names so we the have full context. - -####### --Data- -Commuinty Title: {entity_name} -Description List: {description_list} - diff --git a/common/prompts/google_gemini/entity_relationship_extraction.txt b/common/prompts/google_gemini/entity_relationship_extraction.txt deleted file mode 100644 index 852dded..0000000 --- a/common/prompts/google_gemini/entity_relationship_extraction.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Knowledge Graph Instructions for GPT-4 -## 1. Overview -You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph. -- **Nodes** represent entities, concepts, and properties of entities. -- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience. -## 2. Labeling Nodes -- **Consistency**: Ensure you use basic or elementary types for node labels. -- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist". -- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text. -## 3. Handling Numerical Data and Dates -- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes. -- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes. -- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes. -- **Quotation Marks**: Never use escaped single or double quotes within property values. -- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`. -## 4. Coreference Resolution -- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency. -If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID. -Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial. -## 5. Strict Compliance -Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting. -## 6. Handling Instances with No Relationships -If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty. \ No newline at end of file diff --git a/common/prompts/google_gemini/generate_cypher.txt b/common/prompts/google_gemini/generate_cypher.txt deleted file mode 100644 index f347194..0000000 --- a/common/prompts/google_gemini/generate_cypher.txt +++ /dev/null @@ -1,84 +0,0 @@ -You're an expert in OpenCypher programming. Given the following schema, find the best OpenCypher query that retrieves the answer for question {question}. -If there're multiple words in the question having same meaning then remove the duplication. -Always carefully distinguish entity value from entity type. For example, "MAC LOB" is referring to a LOB named "MAC" because there is a vertex type Lob matching the word "LOB". -Only include attributes that are found in the schema. Never include any attributes that are not found in the schema. -Use attributes instead of primary id if attribute name is more similar to the keyword type in the question. Always use the closest attribute name when there're multiple candidates. -Use as less vertex type, edge type and attributes as possible. If an attribute is not found in the schema, please exclude it from the query. -Always make sure the attributes used exist in the vertex type or edge type referenced, DO NOT use an attribute that does not exist in the vertex or edge from the schema. -Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex. -Always return the entity from the WHERE clause together with the final result in the RETURN statement. Use vertex name instead of ID whenever available. -Never use directed edge pattern in the OpenCypher query. Always use and create query using undirected pattern. Always ensure the edge used starts from and ends with correct vertex types matching the schema. -Always use double quotes for strings instead of single quotes. -Always convert strings to lower case using toLower() function for string comparision in WHERE clause. -Use alias for ORDER BY if any, avoid using short alias names especially single letter alias, always use meaningful words connected by underscore. -Always make sure the alias or attributes used in ORDER BY is the same type in RETURN. Always add ASC or DESC for ORDER BY based on data type. - -Avoid to generate invalid OpenCypher queries based on the errors from history below. - -Schema: {schema} -History: {history} - -Only use the Supported Clauses, Operators, Functions and Expressions below but do not use any of the Unsupported Features, Functions or Syntax Limitations below: - -Supported Clauses: -MATCH / OPTIONAL MATCH / MANDATORY MATCH: Match patterns in the graph. -WHERE: Filter results. -RETURN / WITH: Project query results, alias fields, chain query parts. -ORDER BY / SKIP / LIMIT: Control output order, offset, and size. -DELETE / DETACH DELETE: Delete nodes/edges. - -Supported Operators: -Mathematical: +, -, *, /, %, ^ (exponent) -Comparison: =, <, <=, >, >=, <>, IS NULL, IS NOT NULL -Boolean: AND, OR, NOT, XOR -String/List: CONTAINS, STARTS WITH, ENDS WITH, IN, DISTINCT, [ ] (subscript), . (property access) - -Supported Functions: -Aggregation: count(), sum(), avg(), min(), max(), stDev(), stDevP() -Math: abs(), sqrt(), log(), exp(), sin(), cos(), tan(), radians(), degrees() -String: left(), right(), substring(), replace(), trim(), toLower(), toUpper(), split() -List: head(), last(), size(), range(), coalesce(), tail() -Others: id(), elementId(), labels(), properties(), timestamp() - -Supported Expressions: -CASE: Conditional logic. - -Supported Operators: -Comparison: IS NULL, IS NOT NULL - -Unsupported Features: -Clauses Not Yet Supported -CALL, CREATE, MERGE, REMOVE, SET, UNION, UNION ALL, UNWIND - -Unsupported Functions: -collect(), exists(), keys(), nodes(), relationships(), length(), percentileCont(), percentileDisc(), startNode(), endNode(), reverse() (list form) - -Syntax Limitations: -WITH clause must group by exactly one vertex variable. -Path variables (e.g. p = (...)) not supported. -MATCH must reference variables from prior WITH. -Disconnected MATCH fragments not supported. - -Additionally, you cannot use the following clauses: -CREATE -MERGE -REMOVE -UNION -UNION ALL -UNWIND -SET - -Here's some commonly used abbreviations: -dt -> date -wk -> week -yr -> year -pct -> percentage -qty -> quantity -lng -> longitude -cm -> Contract Manufacturer - -Always make the cypher query returns the entity in the original question together with the data to be queried. -Make sure to have correct attribute names in the OpenCypher query and not to name result aliases that are vertex or edge types, operator or function names, and other reserved keywords, always construct alias with multiple words connected with underscore. -Always validate the syntax for the generated OpenCypher query before writing to response. - -ONLY write the OpenCypher query in the response. Do not include any other information in the response. diff --git a/common/prompts/google_gemini/generate_function.txt b/common/prompts/google_gemini/generate_function.txt deleted file mode 100644 index a7e4ee0..0000000 --- a/common/prompts/google_gemini/generate_function.txt +++ /dev/null @@ -1,24 +0,0 @@ -Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection. -When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted. -For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on. -Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. -Vertex Types: {vertex_types} -Vertex Attributes: {vertex_attributes} -Vertex IDs: {vertex_ids} -Edge Types: {edge_types} -Edge Attributes: {edge_attributes} -Question: {question} -First Docstring: {doc1} -Second Docstring: {doc2} -Third Docstring: {doc3} -Fourth Docstring: {doc4} -Fifth Docstring: {doc5} -Sixth Docstring: {doc6} -Seventh Docstring: {doc7} -Eighth Docstring: {doc8} - -Follow the output directions below on how to structure your response: -{format_instructions} diff --git a/common/prompts/google_gemini/graphrag_scoring.txt b/common/prompts/google_gemini/graphrag_scoring.txt deleted file mode 100644 index 38ef643..0000000 --- a/common/prompts/google_gemini/graphrag_scoring.txt +++ /dev/null @@ -1,7 +0,0 @@ -You are a helpful assistant responsible for generating an answer to the question below using the data provided. -Include a quality score for the answer, based on how well it answers the question. The quality score should be between 0 (poor) and 100 (excellent). - -Question: {question} -Context: {context} - -{format_instructions} diff --git a/common/prompts/google_gemini/map_question_to_schema.txt b/common/prompts/google_gemini/map_question_to_schema.txt deleted file mode 100644 index 81ed53d..0000000 --- a/common/prompts/google_gemini/map_question_to_schema.txt +++ /dev/null @@ -1,15 +0,0 @@ -Replace the entites mentioned in the question to one of these choices: {vertices}. -If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity. -Choose a better mapping between vertex type or its attributes: {verticesAttrs}. -Replace the relationships mentioned in the question to one of these choices: {edges}. -Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above. -Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. - -{format_instructions} -question: {question} -conversation: {conversation} - diff --git a/common/prompts/google_gemini/question_expansion.txt b/common/prompts/google_gemini/question_expansion.txt deleted file mode 100644 index b04aed8..0000000 --- a/common/prompts/google_gemini/question_expansion.txt +++ /dev/null @@ -1,6 +0,0 @@ -You are a helpful assistant responsible for generating 10 new questions similar to the original question below to represent its meaning in a more clear way. -Include a quality score for the answer, based on how well it represents the meaning of the original question. The quality score should be between 0 (poor) and 100 (excellent). - -Question: {question} - -{format_instructions} diff --git a/common/prompts/llama_70b/generate_function.txt b/common/prompts/llama_70b/generate_function.txt deleted file mode 100644 index a7e4ee0..0000000 --- a/common/prompts/llama_70b/generate_function.txt +++ /dev/null @@ -1,24 +0,0 @@ -Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection. -When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted. -For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on. -Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. -Vertex Types: {vertex_types} -Vertex Attributes: {vertex_attributes} -Vertex IDs: {vertex_ids} -Edge Types: {edge_types} -Edge Attributes: {edge_attributes} -Question: {question} -First Docstring: {doc1} -Second Docstring: {doc2} -Third Docstring: {doc3} -Fourth Docstring: {doc4} -Fifth Docstring: {doc5} -Sixth Docstring: {doc6} -Seventh Docstring: {doc7} -Eighth Docstring: {doc8} - -Follow the output directions below on how to structure your response: -{format_instructions} diff --git a/common/prompts/llama_70b/map_question_to_schema.txt b/common/prompts/llama_70b/map_question_to_schema.txt deleted file mode 100644 index 81ed53d..0000000 --- a/common/prompts/llama_70b/map_question_to_schema.txt +++ /dev/null @@ -1,15 +0,0 @@ -Replace the entites mentioned in the question to one of these choices: {vertices}. -If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity. -Choose a better mapping between vertex type or its attributes: {verticesAttrs}. -Replace the relationships mentioned in the question to one of these choices: {edges}. -Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above. -Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. - -{format_instructions} -question: {question} -conversation: {conversation} - diff --git a/common/prompts/openai_gpt4/chatbot_response.txt b/common/prompts/openai_gpt4/chatbot_response.txt deleted file mode 100644 index 6acdaf5..0000000 --- a/common/prompts/openai_gpt4/chatbot_response.txt +++ /dev/null @@ -1,17 +0,0 @@ -You are a highly efficient and empathetic AI-powered knowledge graph assistant. Your goal is to provide accurate, helpful, and friendly response while maintaining professionalism. - -Follow these guidelines: -- Give the contexts in JSON format contains key-context pairs, combine and rephrase it to answer the question. -- Score the contexts for their relevance to the question and use only the information of the high-scoring contexts without adding extra logic. -- Make sure most relevant information in the provided contexts are covered in the generated answer, especially image references providing critical visual information. -- Make sure to preserve the image links in markdown syntax "![description](url)" with its orignal format in the final answer if the context contains the links are used in the response. Do NOT modify or omit these image references. -- Use markdown syntax to geneate the answer, including title, paragraphs, bulleted or numbered list, images and tables if any, and place images or tables below the related text section. -- Ensure that each row of every table, including the header row, starts on a new line. -- Generate the answer in JSON format, make sure to escape necessary characters in order to return a valid JSON response only. -- Make sure all the fields required by the format instructions are included, set a field to empty if you don't have that information. -- Use the keys of the contexts used as citations if asked, DO NOT include citations in the final answer - -Question: {question} -Contexts: {context} -Query: {query} -Format: {format_instructions} diff --git a/common/prompts/openai_gpt4/community_summarization.txt b/common/prompts/openai_gpt4/community_summarization.txt deleted file mode 100644 index 50e4619..0000000 --- a/common/prompts/openai_gpt4/community_summarization.txt +++ /dev/null @@ -1,11 +0,0 @@ -You are a helpful assistant responsible for generating a comprehensive summary of the data provided below. -Given one or two entities, and a list of descriptions, all related to the same entity or group of entities. -Please concatenate all of these into a single, comprehensive description. Make sure to include information collected from all the descriptions. -If the provided descriptions are contradictory, please resolve the contradictions and provide a single, coherent summary, but do not add any information that is not in the description. -Make sure it is written in third person, and include the entity names so we the have full context. - -####### --Data- -Commuinty Title: {entity_name} -Description List: {description_list} - diff --git a/common/prompts/openai_gpt4/entity_relationship_extraction.txt b/common/prompts/openai_gpt4/entity_relationship_extraction.txt deleted file mode 100644 index 852dded..0000000 --- a/common/prompts/openai_gpt4/entity_relationship_extraction.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Knowledge Graph Instructions for GPT-4 -## 1. Overview -You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph. -- **Nodes** represent entities, concepts, and properties of entities. -- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience. -## 2. Labeling Nodes -- **Consistency**: Ensure you use basic or elementary types for node labels. -- For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist". -- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text. -## 3. Handling Numerical Data and Dates -- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes. -- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes. -- **Property Format**: Properties must be in a key-value format. Only use properties for dates and numbers, string properties should be new nodes. -- **Quotation Marks**: Never use escaped single or double quotes within property values. -- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`. -## 4. Coreference Resolution -- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency. -If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID. -Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial. -## 5. Strict Compliance -Adhere to the rules strictly. Non-compliance will result in termination, including poor formatting. -## 6. Handling Instances with No Relationships -If a node has no relationships, it should still be included in the knowledge graph. Simply add the node and leave the relationships section empty. \ No newline at end of file diff --git a/common/prompts/openai_gpt4/generate_cypher.txt b/common/prompts/openai_gpt4/generate_cypher.txt deleted file mode 100644 index 732ed49..0000000 --- a/common/prompts/openai_gpt4/generate_cypher.txt +++ /dev/null @@ -1,85 +0,0 @@ -You're an expert in OpenCypher programming. Given the following schema, find the best OpenCypher query that retrieves the answer for question {question}. -If there're multiple words in the question having same meaning then remove the duplication. -Always carefully distinguish entity value from entity type. For example, "MAC LOB" is referring to a LOB named "MAC" because there is a vertex type Lob matching the word "LOB". -Only include attributes that are found in the schema. Never include any attributes that are not found in the schema. -Use attributes instead of primary id if attribute name is more similar to the keyword type in the question. Always use the closest attribute name when there're multiple candidates. -Use as less vertex type, edge type and attributes as possible. If an attribute is not found in the schema, please exclude it from the query. -Always make sure the attributes used exist in the vertex type or edge type referenced, DO NOT use an attribute that does not exist in the vertex or edge from the schema. -Do not return attributes that are not explicitly mentioned in the question. If a vertex type is mentioned in the question, only return the vertex. -Always include the entity from the WHERE clause to the final RETURN result. Use vertex name instead of ID whenever available. -Never use directed edge pattern in the OpenCypher query. Always use and create query using undirected pattern. Always ensure the edge used starts from and ends with correct vertex types matching the schema. -Always use double quotes for strings instead of single quotes. -Always convert strings to lower case using toLower() function for string comparision in WHERE clause. -Use alias for ORDER BY if any, avoid using short alias names especially single letter alias, always use meaningful words connected by underscore. -Always make sure the alias or attributes used in ORDER BY is the same type in RETURN. Always add ASC or DESC for ORDER BY based on data type. -For questions like "summarize" or "write a summary" about something, fetch all information on its neighbour nodes and edges. - -Avoid to generate invalid OpenCypher queries based on the errors from history below. - -Schema: {schema} -History: {history} - -Only use the Supported Clauses, Operators, Functions and Expressions below but do not use any of the Unsupported Features, Functions or Syntax Limitations below: - -Supported Clauses: -MATCH / OPTIONAL MATCH / MANDATORY MATCH: Match patterns in the graph. -WHERE: Filter results. -RETURN / WITH: Project query results, alias fields, chain query parts. -ORDER BY / SKIP / LIMIT: Control output order, offset, and size. -DELETE / DETACH DELETE: Delete nodes/edges. - -Supported Operators: -Mathematical: +, -, *, /, %, ^ (exponent) -Comparison: =, <, <=, >, >=, <>, IS NULL, IS NOT NULL -Boolean: AND, OR, NOT, XOR -String/List: CONTAINS, STARTS WITH, ENDS WITH, IN, DISTINCT, [ ] (subscript), . (property access) - -Supported Functions: -Aggregation: count(), sum(), avg(), min(), max(), stDev(), stDevP() -Math: abs(), sqrt(), log(), exp(), sin(), cos(), tan(), radians(), degrees() -String: left(), right(), substring(), replace(), trim(), toLower(), toUpper(), split() -List: head(), last(), size(), range(), coalesce(), tail() -Others: id(), elementId(), labels(), properties(), timestamp() - -Supported Expressions: -CASE: Conditional logic. - -Supported Operators: -Comparison: IS NULL, IS NOT NULL - -Unsupported Features: -Clauses Not Yet Supported -CALL, CREATE, MERGE, REMOVE, SET, UNION, UNION ALL, UNWIND - -Unsupported Functions: -collect(), exists(), keys(), nodes(), relationships(), length(), percentileCont(), percentileDisc(), startNode(), endNode(), reverse() (list form) - -Syntax Limitations: -WITH clause must group by exactly one vertex variable. -Path variables (e.g. p = (...)) not supported. -MATCH must reference variables from prior WITH. -Disconnected MATCH fragments not supported. - -Additionally, you cannot use the following clauses: -CREATE -MERGE -REMOVE -UNION -UNION ALL -UNWIND -SET - -Here's some commonly used abbreviations: -dt -> date -wk -> week -yr -> year -pct -> percentage -qty -> quantity -lng -> longitude -cm -> Contract Manufacturer - -Always make the cypher query returns the entity in the original question together with the data to be queried. -Make sure to have correct attribute names in the OpenCypher query and not to name result aliases that are vertex or edge types, operator or function names, and other reserved keywords, always construct alias with multiple words connected with underscore. -Always validate the syntax for the generated OpenCypher query before writing to response. - -ONLY write the OpenCypher query in the response. Do not include any other information in the response. diff --git a/common/prompts/openai_gpt4/generate_function.txt b/common/prompts/openai_gpt4/generate_function.txt deleted file mode 100644 index 359b46c..0000000 --- a/common/prompts/openai_gpt4/generate_function.txt +++ /dev/null @@ -1,27 +0,0 @@ -Use the vertex types, edge types, and their attributes and IDs below to write the pyTigerGraph function call to answer the question using a pyTigerGraph connection. -When the question asks for "How many", counts, totals, or statistics about vertices/nodes/edges in the graph or graph database, make sure to always select a function that contains "Count" in the description/function call. For example, questions like "how many vertices are there in the graph" or "how many vertices are there in the graph db" should use getVertexCount or getEdgeCount. Make sure never to generate a function that is not listed below. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If a WHERE clause is generated, please follow the instruction with proper quoting. To construct a WHERE clause string. Ensure that string attribute values are properly quoted. -For example, if the generated function contains "('Person', where='name=William Torres')", Expected Output: "('Person', where='name="William Torres"')", This rule applies to all types of attributes. e.g., name, email, address and so on. -Documentation contains helpful Python docstrings for the various functions. Use this knowledge to construct the proper function call. Choose one function to execute. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. -Vertex Types: {vertex_types} -Vertex Attributes: {vertex_attributes} -Vertex IDs: {vertex_ids} -Edge Types: {edge_types} -Edge Attributes: {edge_attributes} -Question: {question} -First Docstring: {doc1} -Second Docstring: {doc2} -Third Docstring: {doc3} -Fourth Docstring: {doc4} -Fifth Docstring: {doc5} -Sixth Docstring: {doc6} -Seventh Docstring: {doc7} -Eighth Docstring: {doc8} - -If the output of this function answers the user's question, immediately return that answer. - -Follow the output directions below on how to structure your response -Only include valid JSON do not include any other texts which would render the response invalid JSON. -{format_instructions} diff --git a/common/prompts/openai_gpt4/graphrag_scoring.txt b/common/prompts/openai_gpt4/graphrag_scoring.txt deleted file mode 100644 index 38ef643..0000000 --- a/common/prompts/openai_gpt4/graphrag_scoring.txt +++ /dev/null @@ -1,7 +0,0 @@ -You are a helpful assistant responsible for generating an answer to the question below using the data provided. -Include a quality score for the answer, based on how well it answers the question. The quality score should be between 0 (poor) and 100 (excellent). - -Question: {question} -Context: {context} - -{format_instructions} diff --git a/common/prompts/openai_gpt4/map_question_to_schema.txt b/common/prompts/openai_gpt4/map_question_to_schema.txt deleted file mode 100644 index 81ed53d..0000000 --- a/common/prompts/openai_gpt4/map_question_to_schema.txt +++ /dev/null @@ -1,15 +0,0 @@ -Replace the entites mentioned in the question to one of these choices: {vertices}. -If an entity, such as "John Doe", is mentioned multiple times in the conversation but is referred to by different names or pronouns (e.g., "Joe", "he"), -always use the most complete identifier for that entity throughout the question. In this example, use "John Doe" as the entity. -Choose a better mapping between vertex type or its attributes: {verticesAttrs}. -Replace the relationships mentioned in the question to one of these choices: {edges}. -Make sure the entities are either the source vertices or target vertices of the relationships: {edgesInfo}. -When certain entities are mapped to vertex attributes, may consider to generate a WHERE clause. -If there are words that are synonyms with the entities or relationships above, make sure to output the cannonical form found in the choices above. -Generate the complete question with the appropriate replacements. Keep the case of the schema elements the same. -Don't generate target_vertex_ids if there is no the term 'id' explicitly mentioned in the question. - -{format_instructions} -question: {question} -conversation: {conversation} - diff --git a/common/prompts/openai_gpt4/question_expansion.txt b/common/prompts/openai_gpt4/question_expansion.txt deleted file mode 100644 index b04aed8..0000000 --- a/common/prompts/openai_gpt4/question_expansion.txt +++ /dev/null @@ -1,6 +0,0 @@ -You are a helpful assistant responsible for generating 10 new questions similar to the original question below to represent its meaning in a more clear way. -Include a quality score for the answer, based on how well it represents the meaning of the original question. The quality score should be between 0 (poor) and 100 (excellent). - -Question: {question} - -{format_instructions} diff --git a/common/requirements.txt b/common/requirements.txt index 0a7c34f..f4d5ac6 100644 --- a/common/requirements.txt +++ b/common/requirements.txt @@ -105,6 +105,8 @@ nest-asyncio==1.6.0 nltk==3.9.1 numpy>=1, <2 openai==1.92.2 +openpyxl>=3.1.0 +xlrd>=2.0.1 ordered-set==4.1.0 orjson==3.10.18 packaging==24.2 diff --git a/common/utils/image_data_extractor.py b/common/utils/image_data_extractor.py index 711c562..575264a 100644 --- a/common/utils/image_data_extractor.py +++ b/common/utils/image_data_extractor.py @@ -38,7 +38,9 @@ def describe_image_with_llm(file_path): """ try: from PIL import Image as PILImage - + import os + import time + client = _get_client() if not client: return "Image: Failed to create multimodal LLM client" @@ -58,10 +60,24 @@ def describe_image_with_llm(file_path): { "type": "text", "text": ( - "Please describe what you see in this image and " - "if the image has scanned text then extract all the text. " - "If the image has any graph, chart, table, or other diagram, describe it. " - "If the image has any logo, identify and describe the logo." + "Describe the substantive CONTENT of this image so it " + "can be retrieved alongside the surrounding document. " + "Prioritize, in this order: (1) any text — copy it " + "verbatim, including headings, labels, axis ticks, " + "captions, and footnotes; (2) the data and structure of " + "any chart, graph, or table — name the chart type, the " + "axes / columns, and the values or trend the chart " + "actually shows; (3) the entities, relationships, or " + "process steps in any diagram or flowchart; (4) any logo " + "or branding mark, identified by name. Do NOT describe " + "layout, background color, decorative styling, slide " + "templates, or generic visual impressions — those add " + "no retrieval value. If the image is purely decorative " + "(no text, no data, no diagram), reply with just " + "\"decorative image\" and nothing else. Respond as a " + "SINGLE plain-text paragraph — no markdown headings, no " + "bullet lists, no blank lines. The reply is used " + "verbatim as the alt-text inside `![alt](url)`." ), }, _build_image_content_block(image_base64, "image/jpeg"), @@ -70,8 +86,31 @@ def describe_image_with_llm(file_path): ] langchain_client = client.llm + # Tag the upcoming chat completion as a multimodal image + # describe so it's distinguishable from text-only completions + # in the log stream (e.g. schema extraction, retriever LLM + # calls). Image-describe runs are typically dozens-to-hundreds + # per PDF, while text completions are one-shot. + image_basename = os.path.basename(str(file_path)) + model_name = ( + getattr(_multimodal_client, "config", {}).get("llm_model") + if _multimodal_client else None + ) or "?" + logger.info( + f"multimodal_describe: image={image_basename} " + f"model={model_name} provider={_multimodal_provider}" + ) + t0 = time.monotonic() response = langchain_client.invoke(messages) + elapsed = time.monotonic() - t0 + logger.info( + f"multimodal_describe done: image={image_basename} " + f"elapsed={elapsed:.2f}s" + ) return response.content if hasattr(response, "content") else str(response) except Exception as e: + error_str = str(e).lower() + if "throttl" in error_str or "rate" in error_str or "too many" in error_str: + raise # Let caller retry on rate limit logger.error(f"Failed to describe image with LLM: {str(e)}") return "Image: Error processing image description" diff --git a/common/utils/prompt_validation.py b/common/utils/prompt_validation.py new file mode 100644 index 0000000..8f4e8f5 --- /dev/null +++ b/common/utils/prompt_validation.py @@ -0,0 +1,142 @@ +# Copyright (c) 2024-2026 TigerGraph, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +"""Gatekeepers for user-customized prompt templates. + +When a user saves a customized prompt via the *Customize Prompts* UI, +two things must hold before the file is written: + +1. **Required placeholders are present.** Every prompt type has a fixed + set of ``{var}`` tokens the calling code substitutes at runtime + (e.g. ``community_summarization`` always interpolates + ``{entity_name}`` and ``{description_list}``). If the user removes + one of these, the corresponding feature breaks at the next call. + ``validate_and_escape_prompt`` returns the missing list so the API + can reject the save with a 400. + +2. **Stray brace tokens are escaped.** Users frequently include literal + ``{example}`` or ``{TODO}`` text in their prompts as documentation + or examples. ``str.format`` / ``PromptTemplate`` interpret those as + placeholders and either substitute the wrong thing or raise + ``KeyError``. ``validate_and_escape_prompt`` rewrites any + ``{ident}`` whose name isn't a recognized placeholder for the + prompt type into ``{{ident}}`` so the runtime treats it as literal. + +The placeholder sets are derived from ``input_variables=[…]`` at the +caller site (e.g. ``agent_generation.py``, ``community_summarizer.py``, +``map_question_to_schema.py``). Add a new entry here when a new +user-customizable prompt is wired up. +""" + +from __future__ import annotations + +import re +from typing import List, Set, Tuple + + +#: Variables every customized prompt of this type MUST contain. Derived +#: from the ``input_variables`` arguments passed to the +#: ``PromptTemplate`` / ``ChatPromptTemplate`` constructors at the call +#: sites that consume each prompt. +REQUIRED_VARS_BY_PROMPT_TYPE: dict = { + # Used by graphrag/app/agent/agent_generation.py and the supportai + # retrievers' final answer step. + "chatbot_response": {"question", "context"}, + # System message in LLMEntityRelationshipExtractor — input arrives + # via separate human messages, so the customizable prompt doesn't + # need any required placeholders of its own. + "entity_relationship": set(), + # ecc/app/graphrag/community_summarizer.py. + "community_summarization": {"entity_name", "description_list"}, + # graphrag/app/tools/map_question_to_schema.py. + "query_generation": { + "question", + "conversation", + "vertices", + "verticesAttrs", + "edges", + "edgesInfo", + }, + # common/db/schema_extraction.py. + "schema_extraction": {"samples", "structural_types", "tg_keywords"}, + # Free-form partial injected into the four query-related templates; + # no required placeholders — the user content IS the body. + "query_guidance": set(), +} + + +#: Variables the runtime supplies as ``partial_variables`` (or via a +#: separate prompt message) — they MAY appear in the user content but +#: aren't required. Listed so the escaper doesn't double-brace them. +ALLOWED_PARTIALS_BY_PROMPT_TYPE: dict = { + "chatbot_response": {"format_instructions", "query", "history"}, + "entity_relationship": {"format_instructions", "input"}, + "community_summarization": {"format_instructions"}, + # ``query_guidance`` is a partial the runtime supplies; allowing + # it here keeps a user-pasted ``{query_guidance}`` from being + # double-braced into a literal. + "query_generation": {"format_instructions", "query_guidance"}, + "schema_extraction": set(), + "query_guidance": set(), +} + + +# Match a single-brace placeholder like ``{ident}`` BUT NOT a +# double-brace ``{{ident}}`` (Python's str.format escape) and NOT +# ``{}`` / ``{123}`` (no leading letter or underscore). +# +# The negative lookbehind ``(? Tuple[str, List[str]]: + """Run both gatekeepers on *content* for *prompt_type*. + + Returns ``(escaped_content, missing_required)`` where: + + * ``escaped_content`` is *content* with every stray ``{ident}`` + rewritten to ``{{ident}}``. Tokens whose name is in the + required + partials set are left as-is. + * ``missing_required`` lists the required placeholder names the + user did NOT include. Caller should reject the save when this + list is non-empty. + + For unknown ``prompt_type`` (e.g. a future addition that this + module hasn't been updated for), returns ``(content, [])`` + unchanged so the save isn't blocked — better to ship a forward- + compatible passthrough than fail-closed on a name typo. + """ + if prompt_type not in REQUIRED_VARS_BY_PROMPT_TYPE: + return content, [] + + required: Set[str] = REQUIRED_VARS_BY_PROMPT_TYPE[prompt_type] + allowed_partials: Set[str] = ALLOWED_PARTIALS_BY_PROMPT_TYPE.get( + prompt_type, set() + ) + legal: Set[str] = required | allowed_partials + + found_idents: Set[str] = set() + + def _replace(m: re.Match) -> str: + ident = m.group(1) + found_idents.add(ident) + if ident in legal: + return m.group(0) + return "{{" + ident + "}}" + + escaped = _PLACEHOLDER_RE.sub(_replace, content) + missing = sorted(required - found_idents) + return escaped, missing diff --git a/common/utils/text_extractors.py b/common/utils/text_extractors.py index 449ace5..d8df543 100644 --- a/common/utils/text_extractors.py +++ b/common/utils/text_extractors.py @@ -87,7 +87,7 @@ def insert_description_by_id(md_text, image_id, description): """ Replace the description for an image whose basename == image_id. """ - safe_desc = description.replace("[", "(").replace("]", ")") + safe_desc = _sanitize_alt_text(description) def repl(m): old_path = m.group(2) @@ -100,6 +100,40 @@ def repl(m): return _md_pattern.sub(repl, md_text) +# Maximum characters retained from an LLM image description when +# rendered as markdown alt text. Long alt text bloats the chat +# rendering and offers no extra accessibility value beyond the first +# couple of sentences. +_ALT_TEXT_MAX_CHARS = 400 + + +def _sanitize_alt_text(description: str) -> str: + """Collapse an LLM image description into a single-line, markdown- + safe alt-text string. The LLM is free to respond with headings, + paragraph breaks and bracketed phrases; the markdown image syntax + ``![alt](url)`` doesn't tolerate any of that — a newline or + unescaped ``]`` terminates the construct and the renderer falls + back to printing the raw text (the bug this guards against). + """ + if not description: + return "" + text = str(description) + # Drop a leading markdown heading like ``# Image Description`` + # the LLM tends to emit as a preamble. + text = re.sub(r"^\s*#{1,6}\s*[^\n]*\n+", "", text, count=1) + # Drop a literal "Image Description:" / "Description:" prefix that + # the LLM occasionally writes in place of (or alongside) a heading. + text = re.sub(r"^\s*(image\s+description|description)\s*:\s*", "", text, count=1, flags=re.IGNORECASE) + # Replace every newline + run of whitespace with a single space. + text = re.sub(r"\s+", " ", text).strip() + # ``]`` would close the alt-text bracket; ``[`` can also confuse + # some renderers. Swap both for round parens. + text = text.replace("[", "(").replace("]", ")") + if len(text) > _ALT_TEXT_MAX_CHARS: + text = text[: _ALT_TEXT_MAX_CHARS - 1].rstrip() + "…" + return text + + def replace_path_with_tg_protocol(md_text, image_id, tg_reference): """ Replace the file path for an image whose basename == image_id with tg:// protocol reference. @@ -235,6 +269,7 @@ def safe_walk(path): files_to_process = [] jsonl_files_copied = [] + cached_jsonl_skipped = [] for file_path in safe_walk(folder_path_obj): if file_path.is_file(): if file_path.name.startswith(('.', '~', '$')) or 'BROMIUM' in file_path.name.upper(): @@ -252,9 +287,41 @@ def safe_walk(path): }) logger.info(f"Copied JSONL file directly: {file_path.name} ({num_lines} documents)") elif file_ext in self.supported_extensions: - files_to_process.append(file_path) - - logger.info(f"Found {len(files_to_process)} files to process, {len(jsonl_files_copied)} JSONL files copied directly") + # If a previous run (e.g. schema extraction) already + # produced a matching JSONL in *temp_folder*, reuse + # it instead of re-converting the source file. This + # saves the per-file PDF / image conversion cost + # when the user uploaded sample files via the + # Initialize Graph dialog and is now ingesting them. + cached_jsonl = os.path.join( + temp_folder, f"{file_path.stem}.jsonl" + ) + if os.path.exists(cached_jsonl): + try: + num_lines = sum( + 1 for _ in open(cached_jsonl, 'r', encoding='utf-8') + ) + except Exception: + num_lines = 0 + cached_jsonl_skipped.append({ + 'file_path': str(file_path), + 'num_documents': num_lines, + 'jsonl_file': os.path.basename(cached_jsonl), + 'status': 'success', + 'cached': True, + }) + logger.info( + f"Reusing cached JSONL for {file_path.name} " + f"({num_lines} documents) — skipping re-conversion" + ) + else: + files_to_process.append(file_path) + + logger.info( + f"Found {len(files_to_process)} files to process, " + f"{len(jsonl_files_copied)} JSONL files copied directly, " + f"{len(cached_jsonl_skipped)} skipped via cached JSONL" + ) semaphore = asyncio.Semaphore(max_concurrent) @@ -265,8 +332,10 @@ async def process_with_semaphore(file_path): tasks = [process_with_semaphore(fp) for fp in files_to_process] results = await asyncio.gather(*tasks, return_exceptions=True) - processed_files_info = list(jsonl_files_copied) - total_docs = sum(f['num_documents'] for f in jsonl_files_copied) + processed_files_info = list(jsonl_files_copied) + list(cached_jsonl_skipped) + total_docs = sum( + f['num_documents'] for f in jsonl_files_copied + cached_jsonl_skipped + ) for result in results: if isinstance(result, Exception): @@ -290,7 +359,7 @@ async def process_with_semaphore(file_path): 'error': result.get('error', 'Unknown error') }) - logger.info(f"Prepared {len(processed_files_info)} files ({len(jsonl_files_copied)} JSONL copied, {len(files_to_process)} converted), {total_docs} total documents") + logger.info(f"Processed {len(processed_files_info)} files, extracted {total_docs} total documents") logger.info(f"Created {len([f for f in processed_files_info if f.get('status') == 'success'])} JSONL files in {temp_folder}") return { @@ -457,54 +526,64 @@ def _extract_pdf_with_images_as_docs(file_path, base_doc_id, graphname=None): "content": markdown_content, "position": 0 }] - image_entries = [] - image_counter = 0 - for img_ref in image_refs: + # Phase 1 — describe + base64-encode every image in parallel. + # Each worker hits Bedrock for the description and reads the + # image off disk, so they're I/O-bound; a small thread pool + # cuts wall-clock proportionally for image-heavy PDFs. + # Markdown mutations stay in phase 2 (next loop) because + # insert_description_by_id / replace_path_with_tg_protocol + # mutate the same shared string and must run in deterministic + # order. Concurrency cap is intentionally small to stay below + # Bedrock's per-account throttle. + image_workers = int(os.environ.get("PDF_IMAGE_CONCURRENCY", "8")) + + def _describe_and_encode(img_ref: dict) -> dict: + """Run on a worker thread. Returns one of: + * ``{"ok": True, "img_ref", "description", "image_base64", + "width", "height"}`` + * ``{"ok": False, "img_ref", "error"}`` + Never raises. + """ try: - img_path = Path(img_ref["path"]) # convert to Path - image_id = img_ref["image_id"] - # Image description + img_path = Path(img_ref["path"]) description = describe_image_with_llm(str(img_path)) - markdown_content = insert_description_by_id( - markdown_content, - image_id, - description - ) - # Convert image to base64 pil_image = PILImage.open(img_path) - buffer = io.BytesIO() - if pil_image.mode != "RGB": pil_image = pil_image.convert("RGB") - + buffer = io.BytesIO() pil_image.save(buffer, format="JPEG", quality=95) image_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") - - image_counter += 1 - image_doc_id = f"{base_doc_id}_image_{image_counter}".lower() - - # Replace file path with tg:// protocol reference in markdown - markdown_content = replace_path_with_tg_protocol( - markdown_content, - image_id, - image_doc_id - ) - - image_entries.append({ - "doc_id": image_doc_id, - "doc_type": "image", - "image_description": description, - "image_data": image_base64, - "image_format": "jpg", - "parent_doc": base_doc_id, - "page_number": 0, + return { + "ok": True, + "img_ref": img_ref, + "description": description, + "image_base64": image_base64, "width": pil_image.width, "height": pil_image.height, - "position": image_counter - }) + } + except Exception as img_error: # noqa: BLE001 — keep going + return {"ok": False, "img_ref": img_ref, "error": img_error} + + if image_refs: + with ThreadPoolExecutor( + max_workers=max(1, min(image_workers, len(image_refs))) + ) as ex: + # executor.map preserves input ordering, which is what + # the markdown-mutation phase below relies on. + described = list(ex.map(_describe_and_encode, image_refs)) + else: + described = [] - except Exception as img_error: - logger.warning(f"Failed to process image {img_ref.get('path')}: {img_error}") + # Phase 2 — apply markdown mutations and build image_entries + # in deterministic order using the parallel results. + image_entries: list[dict] = [] + image_counter = 0 + for d in described: + img_ref = d["img_ref"] + if not d.get("ok"): + logger.warning( + f"Failed to process image {img_ref.get('path')}: {d.get('error')}" + ) failed_path = img_ref.get("path", "") if failed_path: markdown_content = re.sub( @@ -512,6 +591,30 @@ def _extract_pdf_with_images_as_docs(file_path, base_doc_id, graphname=None): "", markdown_content, ) + continue + + image_id = img_ref["image_id"] + markdown_content = insert_description_by_id( + markdown_content, image_id, d["description"] + ) + + image_counter += 1 + image_doc_id = f"{base_doc_id}_image_{image_counter}".lower() + markdown_content = replace_path_with_tg_protocol( + markdown_content, image_id, image_doc_id + ) + image_entries.append({ + "doc_id": image_doc_id, + "doc_type": "image", + "image_description": d["description"], + "image_data": d["image_base64"], + "image_format": "jpg", + "parent_doc": base_doc_id, + "page_number": 0, + "width": d["width"], + "height": d["height"], + "position": image_counter, + }) # FINAL CLEANUP — delete folder after processing everything if image_output_folder.exists() and image_output_folder.is_dir(): @@ -613,9 +716,22 @@ def extract_text_from_file(file_path, graphname=None): if extension in ['.txt', '.md']: with open(file_path, 'r', encoding='utf-8') as f: return f.read().strip() - elif extension in ['.html', '.htm', '.csv']: + elif extension in ['.html', '.htm']: with open(file_path, 'r', encoding='utf-8') as f: return f.read().strip() + elif extension == '.csv': + raw = file_path.read_bytes() + # utf-8-sig handles UTF-8 with BOM (common Excel CSV export) + try: + return raw.decode('utf-8-sig').strip() + except UnicodeDecodeError: + pass + # Fall back to chardet detection + import chardet + detected = chardet.detect(raw) + encoding = detected.get('encoding') if detected.get('confidence', 0) >= 0.5 else None + # latin-1 as final fallback — never raises DecodeError + return raw.decode(encoding or 'latin-1').strip() elif extension == '.json': with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) @@ -624,6 +740,37 @@ def extract_text_from_file(file_path, graphname=None): import docx doc = docx.Document(file_path) return "\n".join(p.text for p in doc.paragraphs if p.text.strip()) + elif extension in ['.xlsx', '.xls']: + import pandas as pd + engine = 'openpyxl' if extension == '.xlsx' else 'xlrd' + try: + xl = pd.ExcelFile(file_path, engine=engine) + except Exception: + xl = pd.ExcelFile(file_path) + sheet_texts = [] + for sheet_name in xl.sheet_names: + # Always read with header=None so no data row is silently + # consumed as column names for headerless spreadsheets. + df = xl.parse(sheet_name, header=None) + if df.empty: + continue + df = df.fillna('') + first_row = df.iloc[0] + first_row_values = [str(v).strip() for v in first_row] + looks_like_header = ( + len(df) > 1 + and all(first_row_values) + and len(set(first_row_values)) == len(first_row_values) + and not any(v.isdigit() for v in first_row_values) + ) + if looks_like_header: + df.columns = first_row_values + df = df.iloc[1:].reset_index(drop=True) + else: + df.columns = [f"Column {i + 1}" for i in range(len(df.columns))] + sheet_md = df.to_markdown(index=False) + sheet_texts.append(f"## Sheet: {sheet_name}\n\n{sheet_md}") + return "\n\n".join(sheet_texts) if sheet_texts else "[Excel file is empty or contains no data]" elif extension == '.xml': import xml.etree.ElementTree as ET tree = ET.parse(file_path) @@ -663,7 +810,7 @@ def get_doc_type_from_extension(extension): def get_supported_extensions(): """Get list of supported file extensions.""" - return {'.txt', '.md', '.html', '.htm', '.csv', '.json', '.pdf', '.docx', '.xml', '.jpeg', '.jpg', '.png', '.gif'} + return {'.txt', '.md', '.html', '.htm', '.csv', '.json', '.pdf', '.docx', '.doc', '.xml', '.jpeg', '.jpg', '.png', '.gif', '.xlsx', '.xls', '.jsonl'} def is_supported_file(file_path): """Check if a file is supported for text extraction.""" diff --git a/docker-compose.yml b/docker-compose.yml index 97a0952..1034ead 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: graphrag: - image: tigergraph/graphrag:latest + image: tigergraph/graphrag:latest platform: linux/amd64 container_name: graphrag build: @@ -51,12 +51,14 @@ services: - ./configs/:/configs graphrag-ui: - image: tigergraph/graphrag-ui:latest + image: tigergraph/graphrag-ui:latest platform: linux/amd64 - container_name: graphrag-ui + container_name: graphrag-ui build: context: graphrag-ui dockerfile: Dockerfile + additional_contexts: + repo: . ports: - 3000:3000 depends_on: diff --git a/docs/tutorials/configs/nginx.conf b/docs/tutorials/configs/nginx.conf index 975d8a0..121922c 100644 --- a/docs/tutorials/configs/nginx.conf +++ b/docs/tutorials/configs/nginx.conf @@ -29,6 +29,14 @@ server { proxy_pass http://graphrag-ui:3000/; } + location /trace { + proxy_pass http://graphrag-ui:3000; + } + + location /trace/ { + proxy_pass http://graphrag-ui:3000; + } + location ~^/ui/.*/chat$ { proxy_pass http://graphrag:8000; proxy_http_version 1.1; diff --git a/ecc/Dockerfile b/ecc/Dockerfile index 5b83e4a..9d9f063 100644 --- a/ecc/Dockerfile +++ b/ecc/Dockerfile @@ -2,13 +2,16 @@ FROM python:3.11.9-bullseye WORKDIR /code COPY common/requirements.txt requirements.txt - + RUN apt-get update && apt-get upgrade -y RUN pip install -r requirements.txt COPY ecc/app /code COPY common /code/common +COPY VERSION /code/VERSION +RUN date -u +%Y-%m-%dT%H:%M:%SZ > /code/BUILD_DATE + ENV SERVER_CONFIG="/server_config.json" ENV LOGLEVEL="INFO" diff --git a/ecc/app/ecc_util.py b/ecc/app/ecc_util.py index a28567a..7da80bc 100644 --- a/ecc/app/ecc_util.py +++ b/ecc/app/ecc_util.py @@ -1,5 +1,5 @@ from common.chunkers import character_chunker, regex_chunker, semantic_chunker, markdown_chunker, recursive_chunker, html_chunker, single_chunker -from common.config import get_graphrag_config, embedding_service +from common.config import get_graphrag_config, get_embedding_service def get_chunker(chunker_type: str = "", graphname: str = None): cfg = get_graphrag_config(graphname) @@ -8,7 +8,7 @@ def get_chunker(chunker_type: str = "", graphname: str = None): chunker_config = cfg.get("chunker_config", {}) if chunker_type == "semantic": chunker = semantic_chunker.SemanticChunker( - embedding_service, + get_embedding_service(), chunker_config.get("method", "percentile"), chunker_config.get("threshold", 0.95), ) diff --git a/ecc/app/eventual_consistency_checker.py b/ecc/app/eventual_consistency_checker.py index 1c28b53..fb501fe 100644 --- a/ecc/app/eventual_consistency_checker.py +++ b/ecc/app/eventual_consistency_checker.py @@ -155,24 +155,10 @@ def _upsert_rels(self, src_id, src_type, relationships): for x in relationships ], ) - self.conn.upsertEdges( - "Entity", - "IS_HEAD_OF", - "RelationshipType", - [ - (x["source"], x["source"] + ":" + x["type"] + ":" + x["target"], {}) - for x in relationships - ], - ) - self.conn.upsertEdges( - "RelationshipType", - "HAS_TAIL", - "Entity", - [ - (x["source"] + ":" + x["type"] + ":" + x["target"], x["target"], {}) - for x in relationships - ], - ) + # IS_HEAD_OF / HAS_TAIL are meta-schema edges (EntityType ↔ + # RelationshipType); not per-instance. The legacy ECC path + # writes only MENTIONS_RELATIONSHIP from the chunk/document + # source to the RelationshipType meta-vertex. self.conn.upsertEdges( src_type, "MENTIONS_RELATIONSHIP", diff --git a/ecc/app/graphrag/community_summarizer.py b/ecc/app/graphrag/community_summarizer.py index 532b94f..3586e9b 100644 --- a/ecc/app/graphrag/community_summarizer.py +++ b/ecc/app/graphrag/community_summarizer.py @@ -26,7 +26,7 @@ # src: https://github.com/microsoft/graphrag/blob/main/graphrag/index/graph/extractors/summarize/prompts.py -id_pat = re.compile(r"[_\d]*") +id_pat = re.compile(r"(_\d+)+$") class CommunitySummarizer: diff --git a/ecc/app/graphrag/graph_rag.py b/ecc/app/graphrag/graph_rag.py index c7ef5be..d2bee46 100644 --- a/ecc/app/graphrag/graph_rag.py +++ b/ecc/app/graphrag/graph_rag.py @@ -31,11 +31,12 @@ load_q, loading_event, make_headers, + graphrag_mirror_communities, stream_ids, tg_sem, upsert_batch, - add_rels_between_types ) +from common.db.schema_utils import is_structural_type, read_existing_schema_async from pyTigerGraph import AsyncTigerGraphConnection from common.config import embedding_service, entity_extraction_switch, community_detection_switch, doc_process_switch, get_graphrag_config @@ -50,11 +51,18 @@ async def stream_docs( conn: AsyncTigerGraphConnection, docs_chan: Channel, ttl_batches: int = 10, + progress=None, ): """ - Streams the document contents into the docs_chan + Streams the document contents into the docs_chan. + + *progress* (optional) is a callable invoked once when document + streaming completes — runtime hands the rebuild status forward + from "Chunking documents" to "Extracting entities and + relationships" at that boundary. """ - logger.info("streaming docs") + logger.info(f"streaming docs ({ttl_batches} batches)") + n_docs = 0 for i in range(ttl_batches): doc_ids = await stream_ids(conn, "Document", i, ttl_batches) if doc_ids["error"]: @@ -67,19 +75,28 @@ async def stream_docs( async with tg_sem: res = await conn.runInstalledQuery( "StreamDocContent", - params={"doc": d}, + # 1-tuple form for VERTEX params; see + # stream_chunks for the deprecation context. + params={"doc": (d,)}, ) - logger.info(f"stream_docs writes {d} to docs") + # Demoted from INFO — ``d`` is a user document ID. + logger.debug(f"stream_docs writes {d} to docs") await docs_chan.put(res[0]["DocContent"][0]) + n_docs += 1 except Exception as e: exc = traceback.format_exc() logger.error(f"Error retrieving doc: {d} --> {e}\n{exc}") continue # try retrieving the next doc - logger.info("stream_docs done") + logger.info(f"stream_docs done: {n_docs} document(s) streamed") # close the docs chan -- this function is the only sender logger.info("closing docs chan") docs_chan.close() + if progress is not None: + try: + progress("Extracting entities and relationships") + except Exception: + pass async def stream_chunks( conn: AsyncTigerGraphConnection, @@ -90,7 +107,8 @@ async def stream_chunks( """ Streams the chunk contents into the extract_chan and embed_chan """ - logger.info("streaming chunks") + logger.info(f"streaming chunks ({ttl_batches} batches)") + n_chunks = 0 for i in range(ttl_batches): chunk_ids = await stream_ids(conn, "DocumentChunk", i, ttl_batches) if chunk_ids["error"]: @@ -98,24 +116,52 @@ async def stream_chunks( for c in chunk_ids["ids"]: try: - async with tg_sem: - res = await conn.runInstalledQuery( - "StreamChunkContent", - params={"chunk": c}, + # Retry briefly when ChunkContent is empty — that + # happens when stream_ids surfaced a DocumentChunk + # vertex but its HAS_CONTENT edge upsert hasn't + # flushed yet (the loader runs in batches). Without + # the retry the chunk gets silently dropped and + # extracted only on the next ECC sweep. + chunk_rows = [] + for attempt in range(3): + async with tg_sem: + res = await conn.runInstalledQuery( + "StreamChunkContent", + # 1-tuple form is the supported shape for + # VERTEX params in current pyTigerGraph; + # the plain-value form raises a deprecation + # warning and falls back to a slower GET. + params={"chunk": (c,)}, + ) + chunk_rows = (res[0] if res else {}).get("ChunkContent") or [] + if chunk_rows: + break + # Back off and try again — the loader's batch + # interval is a few seconds. + await asyncio.sleep(2 * (attempt + 1)) + if not chunk_rows: + logger.warning( + f"No content row for chunk {c} after retries; skipping" ) - content = res[0]["ChunkContent"][0]["attributes"]["text"].encode('raw_unicode_escape').decode('unicode_escape') - logger.info("chunk writes to extract_chan") + continue + content = chunk_rows[0]["attributes"]["text"].encode( + 'raw_unicode_escape' + ).decode('unicode_escape') + logger.debug("chunk writes to extract_chan") await extract_chan.put((content, c)) # send chunks to be embedded - logger.info("chunk writes to embed_chan") + logger.debug("chunk writes to embed_chan") await embed_chan.put((c, content, "DocumentChunk")) + n_chunks += 1 + if n_chunks % 100 == 0: + logger.info(f"streaming chunks: {n_chunks} streamed") except Exception as e: exc = traceback.format_exc() logger.error(f"Error retrieving chunk: {c} --> {e}\n{exc}") continue # try retrieving the next doc - logger.info("stream_chunks done") + logger.info(f"stream_chunks done: {n_chunks} chunk(s) streamed") logger.info("closing extract_chan") await extract_chan.put(None) @@ -133,10 +179,12 @@ async def chunk_docs( """ logger.info("Chunk Processing Start") doc_tasks = [] + n_docs = 0 async with asyncio.TaskGroup() as grp: while True: try: content = await docs_chan.get() + n_docs += 1 task = grp.create_task( workers.chunk_doc(conn, content, upsert_chan, embed_chan, extract_chan) ) @@ -146,7 +194,7 @@ async def chunk_docs( except Exception: raise - logger.info("Chunk Processing End") + logger.info(f"Chunk Processing End: {n_docs} document(s) processed") logger.info("closing extract_chan") await extract_chan.put(None) @@ -160,20 +208,29 @@ async def upsert(upsert_chan: Channel): """ logger.info("Data Upserting Start") + n_upserts = 0 # consume task queue async with asyncio.TaskGroup() as grp: while True: try: (func, args) = await upsert_chan.get() - logger.info(f"Upserting with {func.__name__}, {args[1:3]}") + # Demoted from INFO — ``args`` carries vertex IDs and + # payloads derived from user documents. + logger.debug(f"Upserting with {func.__name__}, {args[1:3]}") # execute the task grp.create_task(func(*args)) + n_upserts += 1 + # Heartbeat every 200 upserts so a long stage doesn't + # look stalled in the INFO log without exposing the + # underlying data. + if n_upserts % 200 == 0: + logger.info(f"Data Upserting: {n_upserts} dispatched") except ChannelClosed: break except Exception: raise - logger.info("Data Upserting End") + logger.info(f"Data Upserting End: {n_upserts} dispatched") logger.info("closing load_q chan") load_q.close() @@ -255,15 +312,19 @@ async def embed( (v_id, content, index_name) <- q.get() """ logger.info("Embedding Processing Start") + n_embed = 0 + n_reused = 0 async with asyncio.TaskGroup() as grp: # consume task queue while True: try: (v_id, content, index_name) = await embed_chan.get() v_id = (v_id, index_name) - logger.info(f"Embed to {graphname}_{index_name}: {v_id}") + # v_id is a per-vertex identifier derived from user content. + logger.debug(f"Embed to {graphname}_{index_name}: {v_id}") if get_graphrag_config(graphname).get("reuse_embedding", True) and embedding_store.has_embeddings([v_id]): - logger.info(f"Embeddings for {v_id} already exists, skipping to save cost") + logger.debug(f"Embeddings for {v_id} already exists, skipping to save cost") + n_reused += 1 continue grp.create_task( workers.embed( @@ -273,12 +334,17 @@ async def embed( content, ) ) + n_embed += 1 + if n_embed % 100 == 0: + logger.info(f"Embedding Processing: {n_embed} embedded so far") except ChannelClosed: break except Exception: raise - logger.info("Embedding Processing End") + logger.info( + f"Embedding Processing End: {n_embed} embedded, {n_reused} reused" + ) async def extract( @@ -296,6 +362,7 @@ async def extract( """ logger.info("Entity Extration Start") # consume task queue + n_chunks = 0 async with asyncio.TaskGroup() as grp: done_count = 0 while True: @@ -311,12 +378,17 @@ async def extract( grp.create_task( workers.extract(upsert_chan, extractor, conn, *item) ) + n_chunks += 1 + if n_chunks % 50 == 0: + logger.info( + f"Entity Extraction: {n_chunks} chunks dispatched" + ) except ChannelClosed: break except Exception: raise - logger.info("Entity Extration End") + logger.info(f"Entity Extration End: {n_chunks} chunks extracted") logger.info("closing extract, upsert and embed chan") extract_chan.close() @@ -436,12 +508,21 @@ async def summarize_communities( upsert_chan: Channel, embed_chan: Channel, ): + logger.info("Community summarization started") + n_comm = 0 async with asyncio.TaskGroup() as tg: while True: try: c = await comm_process_chan.get() tg.create_task(workers.process_community(conn, upsert_chan, embed_chan, *c)) logger.debug(f"Added community to process: {c}") + n_comm += 1 + # Per-community summarization can take 30s; emit a + # heartbeat every 20 so a long run doesn't go silent. + if n_comm % 20 == 0: + logger.info( + f"Community summarization: {n_comm} dispatched" + ) except ChannelClosed: break except Exception: @@ -450,10 +531,12 @@ async def summarize_communities( logger.info("closing upsert_chan") upsert_chan.close() embed_chan.close() - logger.info("summarize_communities done") + logger.info( + f"Community summarization done: {n_comm} communities dispatched" + ) -async def run(graphname: str, conn: AsyncTigerGraphConnection): +async def run(graphname: str, conn: AsyncTigerGraphConnection, progress=None): """ Set up GraphRAG: - Install necessary queries. @@ -463,12 +546,27 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection): - entities/relationships - upsert everything to the graph - Detect communities and summarize them + + *progress* is an optional ``Callable[[str], None]`` invoked at + each user-visible sub-phase; ECC's ``run_with_tracking`` wires it + to the task's ``stage`` field so the UI rebuild dialog can show + where the job is. """ + def _report(msg: str) -> None: + if progress is None: + return + try: + progress(msg) + except Exception: + pass + + _report("Preparing rebuild") extractor, embedding_store = await init(conn) init_start = time.perf_counter() if doc_process_switch: + _report("Chunking documents") logger.info("Doc Processing Start") docs_chan = Channel(1) embed_chan = Channel() @@ -478,7 +576,7 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection): async with asyncio.TaskGroup() as grp: # get docs - grp.create_task(stream_docs(conn, docs_chan, 100)) + grp.create_task(stream_docs(conn, docs_chan, 100, progress=progress)) # process docs grp.create_task( chunk_docs(conn, docs_chan, embed_chan, upsert_chan, extract_chan) @@ -506,16 +604,10 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection): init_end = time.perf_counter() logger.info("Doc Processing End") - # Type Resolution + # Type Resolution — IS_HEAD_OF / HAS_TAIL writes happen inline in + # the per-relationship extract step (workers.py); no post-processing + # query needed. type_start = time.perf_counter() - if entity_extraction_switch: - logger.info("Type Processing Start") - res = await add_rels_between_types(conn) - if res.get("error", False): - logger.error(f"Error adding relationships between types: {res}") - else: - logger.info(f"Added relationships between types: {res}") - logger.info("Type Processing End") type_end = time.perf_counter() # Community Detection @@ -524,8 +616,22 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection): # schema edits could still leave queries missing. community_start = time.perf_counter() if community_detection_switch: + _report("Detecting communities") await install_queries(COMMUNITY_QUERIES, conn) logger.info("Community Processing Start") + + # Clear pre-existing communities so re-detection is idempotent. + try: + async with tg_sem: + res = await conn.runInstalledQuery( + "graphrag_delete_all_communities" + ) + deleted = (res[0] if res else {}).get("deleted", 0) + if deleted: + logger.info(f"Cleared {deleted} pre-existing Community vertex(es)") + except Exception as e: + logger.warning(f"graphrag_delete_all_communities failed: {e}") + comm_process_chan = Channel() upsert_chan = Channel() embed_chan = Channel() @@ -547,6 +653,31 @@ async def run(graphname: str, conn: AsyncTigerGraphConnection): await embed_chan.join() logger.info("Join upsert_chan") await upsert_chan.join() + + # Mirror Entity → Community memberships onto domain-VT instances + # that share the same id. + try: + existing_schema = await read_existing_schema_async(conn) + domain_vts = sorted( + v for v in existing_schema.vertex_types if not is_structural_type(v) + ) + except Exception as e: + logger.warning(f"read live schema for community mirror failed: {e}") + domain_vts = [] + if domain_vts: + mirrorable = [ + vt for vt in domain_vts + if existing_schema.has_edge_pair("IN_COMMUNITY", vt, "Community") + ] + skipped = sorted(set(domain_vts) - set(mirrorable)) + if skipped: + logger.warning( + f"skipping community mirror for {skipped}: " + f"IN_COMMUNITY pair missing on schema" + ) + if mirrorable: + _report("Updating domain types") + await graphrag_mirror_communities(conn, mirrorable) community_end = time.perf_counter() logger.info("Community Processing End") diff --git a/ecc/app/graphrag/util.py b/ecc/app/graphrag/util.py index 094d95b..897853f 100644 --- a/ecc/app/graphrag/util.py +++ b/ecc/app/graphrag/util.py @@ -30,6 +30,12 @@ get_completion_config, get_graphrag_config, ) +from common.db.schema_utils import ( + gsql_output_error, + is_structural_type, + read_existing_schema_async, + read_type_metadata_async, +) from common.embeddings.base_embedding_store import EmbeddingStore from common.embeddings.tigergraph_embedding_store import TigerGraphEmbeddingStore from common.extractors import GraphExtractor, LLMEntityRelationshipExtractor @@ -53,6 +59,9 @@ "common/gsql/graphrag/louvain/stream_community", "common/gsql/graphrag/get_community_children", "common/gsql/graphrag/communities_have_desc", + "common/gsql/graphrag/graphrag_delete_all_communities", + "common/gsql/graphrag/graphrag_stream_entity_community_pairs", + "common/gsql/graphrag/graphrag_stream_all_ids", ] REQUIRED_QUERIES = [ @@ -61,7 +70,6 @@ "common/gsql/graphrag/StreamChunkContent", "common/gsql/graphrag/SetEpochProcessing", "common/gsql/graphrag/get_vertices_or_remove", - "common/gsql/supportai/create_entity_type_relationships", ] load_q = reusable_channel.ReuseableChannel() @@ -94,8 +102,8 @@ async def install_queries( async with tg_sem: res = await conn.gsql(query) logger.info(f"INSTALL QUERY ALL returned: {str(res)[:200]}") - res_lower = res.lower() if isinstance(res, str) else "" - if "error" in res_lower or "does not exist" in res_lower or "failed" in res_lower: + err = gsql_output_error(res) if isinstance(res, str) else None + if err: raise Exception(res) max_wait = 600 # seconds @@ -142,7 +150,29 @@ async def init( if graph_cfg.get("extractor") == "graphrag": extractor = GraphExtractor() elif graph_cfg.get("extractor") == "llm": - extractor = LLMEntityRelationshipExtractor(get_llm_service(get_completion_config())) + # Read the live schema and pack it into the LLM-extractor + # bundle. ``build_allowed_schema_async`` filters structural + # types, reads attribute schemas + definitions, and renders the + # prompt text in one pass — same shape that query-side tools + # consume via ``render_schema_rep``. + try: + from common.db.schema_utils import build_allowed_schema_async, AllowedSchema + allowed_schema = await build_allowed_schema_async(conn) + except Exception as exc: + logger.warning(f"Loading domain schema for extractor failed: {exc}") + from common.db.schema_utils import AllowedSchema + allowed_schema = AllowedSchema() + + # Strict mode (graphrag_config.strict_mode, default false): + # when false, entities whose type doesn't match a domain VT + # still land in the plain Entity vertex. + strict_mode = bool(graph_cfg.get("strict_mode", False)) + + extractor = LLMEntityRelationshipExtractor( + get_llm_service(get_completion_config(conn.graphname)), + allowed_schema=allowed_schema, + strict_mode=strict_mode, + ) else: raise ValueError("Invalid extractor type") @@ -216,6 +246,55 @@ def process_id(v_id: str): return v_id +# Suffixes the LLM commonly tacks onto type labels without adding +# semantic distinction. Stripped during meta-layer normalization so +# ``Company_Type``, ``Company_Class``, ``Company_Entity`` collapse onto +# the same canonical name. +_TYPE_SUFFIXES = ("_type", "_class", "_entity", "_data", "_info", "_record") + + +def normalize_type_name(name: str) -> str: + """Normalize an LLM-emitted vertex / edge type label so trivial + variants collapse onto a single canonical id. + + Applies in order: + + 1. ``process_id`` (lowercase, whitespace → ``-``, strip parens). + 2. Strip a single trailing semantic-suffix from + :data:`_TYPE_SUFFIXES` (e.g. ``company_type`` → ``company``). + 3. Singularize trailing ``ies`` → ``y`` (``companies`` → + ``company``); strip a single trailing ``s`` only when the + preceding char is a consonant other than ``s``, ``i``, or ``u`` + (``reports`` → ``report``; preserves ``series``, ``status``, + ``news``, ``business``). + + Used only for the EntityType / RelationshipType meta-layer in + Case 1 (no domain types declared) — instance ids stay + untouched. Synonym consolidation (``Company`` vs ``Corporation``) + is out of scope for this deterministic pass. + """ + base = process_id(name) + if not base: + return "" + for suffix in _TYPE_SUFFIXES: + if base.endswith(suffix) and len(base) > len(suffix): + base = base[: -len(suffix)] + break + # Singularize defensively. Length thresholds keep short words + # whose final ``s`` / ``ies`` is part of the singular stem + # (``News``, ``Series``, ``Bus``, ``Status``, ``Yes``). + if base.endswith("ies") and len(base) > 6: + base = base[:-3] + "y" + elif ( + base.endswith("s") + and len(base) > 4 + and base[-2] not in "siu" + and not base[-2].isdigit() + ): + base = base[:-1] + return base + + async def upsert_vertex( conn: AsyncTigerGraphConnection, vertex_type: str, @@ -228,6 +307,159 @@ async def upsert_vertex( await load_q.put(("vertices", (vertex_type, vertex_id, attrs))) +def coerce_attrs_for_schema( + props: dict, + schema: dict, +) -> dict: + """Coerce LLM-emitted properties to the declared TigerGraph types + and drop anything not in *schema*. + + *props* — dict the LLM produced (values may be strings, numbers, + bools depending on the model and the schema instruction). + *schema* — ``{attr_name: tg_type}`` for the destination type + (vertex or edge). ``tg_type`` is one of TG's primitive type names + (case-insensitive): ``STRING``, ``INT``, ``UINT``, ``DOUBLE``, + ``FLOAT``, ``BOOL``, ``DATETIME``. + + Behavior: + * Attribute names are matched case-insensitively; the canonical + schema spelling is used in the returned dict. + * Values that can't be coerced (e.g. a non-numeric string for + an INT field) are silently dropped — partial coverage is + fine; a single bad value shouldn't reject the whole upsert. + * Empty strings / ``None`` / sentinel values like ``"N/A"`` / + ``"unknown"`` are dropped before coercion to avoid writing + junk into typed columns. + """ + if not props or not schema: + return {} + # Build a case-folded lookup once. + schema_ci = {k.casefold(): k for k in schema.keys()} + out: dict = {} + for raw_name, raw_val in props.items(): + if not raw_name: + continue + canonical = schema_ci.get(str(raw_name).casefold()) + if not canonical: + continue + tg_type = (schema.get(canonical) or "").upper() + coerced = _coerce_value(raw_val, tg_type) + if coerced is not None: + out[canonical] = coerced + return out + + +_LLM_NULL_SENTINELS = frozenset({ + "", "n/a", "na", "none", "null", "unknown", "not specified", + "not available", "not applicable", "tbd", "?", +}) + + +# Primitive types accepted inside a TG DISCRIMINATOR(...) clause. +# Discriminator attrs must be present in every upsert; the worker +# fills missing values from ``_DISCRIMINATOR_FALLBACKS`` below. +_DISCRIMINATOR_TYPES = frozenset({"INT", "UINT", "STRING", "DATETIME"}) + +_DISCRIMINATOR_FALLBACKS: dict = { + "INT": 0, + "UINT": 0, + "STRING": "", + "DATETIME": "1970-01-01 00:00:00", +} + + +def coerce_edge_attrs_for_schema( + props: dict, + schema: dict, +) -> dict: + """Coerce LLM-emitted properties for an edge upsert. + + Same matching + coercion as the vertex helper; additionally fills + in default values for any discriminator-typed schema attribute + the LLM did not provide, since TG requires every discriminator + attribute to be present in each upsert. + """ + if not schema: + return {} + coerced = coerce_attrs_for_schema(props or {}, schema) + # Fill missing discriminator-typed attributes with type defaults. + schema_ci = {k.casefold(): k for k in schema.keys()} + for ci_name, canonical in schema_ci.items(): + if canonical in coerced: + continue + tg_type = (schema.get(canonical) or "").upper() + if tg_type in _DISCRIMINATOR_TYPES: + coerced[canonical] = _DISCRIMINATOR_FALLBACKS[tg_type] + return coerced + + +def _coerce_value(value, tg_type: str): + """Convert *value* to the TG type *tg_type*. Returns ``None`` when + coercion would lose meaning (empty value, sentinel like ``"N/A"``, + or a parse failure). Caller drops attrs that come back ``None``. + """ + if value is None: + return None + # Quick string-sentinel filter — applies to every type. + if isinstance(value, str): + s = value.strip() + if s.casefold() in _LLM_NULL_SENTINELS: + return None + + try: + if tg_type in ("INT", "UINT"): + if isinstance(value, bool): + return int(value) + if isinstance(value, (int, float)): + v = int(value) + else: + # Strip thousand separators and surrounding whitespace. + v = int(float(str(value).replace(",", "").strip())) + if tg_type == "UINT" and v < 0: + return None + return v + if tg_type in ("DOUBLE", "FLOAT"): + if isinstance(value, bool): + return float(value) + if isinstance(value, (int, float)): + return float(value) + return float(str(value).replace(",", "").strip()) + if tg_type == "BOOL": + if isinstance(value, bool): + return value + s = str(value).strip().casefold() + if s in ("true", "yes", "y", "1"): + return True + if s in ("false", "no", "n", "0"): + return False + return None + if tg_type == "DATETIME": + # TG accepts 'YYYY-MM-DD HH:MM:SS' (space-separated). + # Accept ISO-8601 with 'T' and normalize. + s = str(value).strip() + if not s: + return None + try: + from dateutil import parser as _dt_parser # type: ignore + dt = _dt_parser.parse(s) + return dt.strftime("%Y-%m-%d %H:%M:%S") + except Exception: + # Fall back to a few common formats without dateutil. + from datetime import datetime as _dt + for fmt in ("%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d %H:%M:%S", "%Y/%m/%d"): + try: + return _dt.strptime(s, fmt).strftime("%Y-%m-%d %H:%M:%S") + except ValueError: + continue + return None + # STRING (and anything we don't recognize): coerce to str. + s = str(value).strip() + return s or None + except (ValueError, TypeError): + return None + + async def upsert_batch(conn: AsyncTigerGraphConnection, data: str): async with tg_sem: try: @@ -292,7 +524,9 @@ async def get_commuinty_children(conn, i: int, c: str): try: resp = await conn.runInstalledQuery( "get_community_children", - params={"comm": c, "iter": i} + # 1-tuple form for VERTEX params; plain value + # is deprecated in current pyTigerGraph. + params={"comm": (c,), "iter": i} ) except: logger.error(f"Get Children err:\n{traceback.format_exc()}") @@ -319,17 +553,6 @@ async def get_commuinty_children(conn, i: int, c: str): return descrs -async def add_rels_between_types(conn): - try: - async with tg_sem: - resp = await conn.runInstalledQuery( - "create_entity_type_relationships" - ) - except Exception as e: - logger.error(f"Check Vert EntityType err:\n{e}") - return {"error": True, "message": e} - return resp[0] - async def check_vertex_has_desc(conn, i: int): try: async with tg_sem: @@ -361,3 +584,82 @@ async def check_embedding_rebuilt(conn, v_type: str): logger.info(resp) return res + + +async def graphrag_mirror_communities( + conn: AsyncTigerGraphConnection, + domain_vts: list[str], +) -> int: + """Mirror Entity → Community memberships onto domain-VT instances + that share the same id. Returns the number of mirror edges written. + """ + if not domain_vts: + return 0 + + async with tg_sem: + try: + res = await conn.runInstalledQuery( + "graphrag_stream_entity_community_pairs", + params={}, + sizeLimit=1000000000, + ) + except Exception as e: + logger.error(f"stream entity-community pairs failed: {e}") + return 0 + + pairs = (res[0] if res else {}).get("pairs", []) or [] + if not pairs: + return 0 + + valid_ids_by_vt: dict[str, set[str]] = {} + for vt in domain_vts: + try: + async with tg_sem: + r = await conn.runInstalledQuery( + "graphrag_stream_all_ids", + params={"v_type": vt}, + sizeLimit=1000000000, + ) + except Exception as e: + logger.warning(f"stream_all_ids({vt}) failed: {e}") + valid_ids_by_vt[vt] = set() + continue + ids = set((r[0] if r else {}).get("@@ids", []) or []) + valid_ids_by_vt[vt] = ids + + written = 0 + chunk_size = 5000 + for vt, valid_ids in valid_ids_by_vt.items(): + if not valid_ids: + continue + edges = [ + (p["entity_id"], p["community_id"]) + for p in pairs + if isinstance(p, dict) + and p.get("entity_id") in valid_ids + and p.get("community_id") + ] + if not edges: + continue + for i in range(0, len(edges), chunk_size): + chunk = edges[i:i + chunk_size] + async with tg_sem: + try: + await conn.upsertEdges( + sourceVertexType=vt, + edgeType="IN_COMMUNITY", + targetVertexType="Community", + edges=chunk, + ) + written += len(chunk) + except Exception as e: + logger.error( + f"upsertEdges IN_COMMUNITY for {vt} (chunk size " + f"{len(chunk)}) failed: {e}" + ) + + logger.info( + f"graphrag_mirror_communities: wrote {written} mirror " + f"IN_COMMUNITY edges across {len(domain_vts)} domain VT(s)" + ) + return written diff --git a/ecc/app/graphrag/workers.py b/ecc/app/graphrag/workers.py index 516b83c..0f692b8 100644 --- a/ecc/app/graphrag/workers.py +++ b/ecc/app/graphrag/workers.py @@ -94,30 +94,32 @@ async def chunk_doc( v_id = util.process_id(doc["v_id"]) if v_id != doc["v_id"]: - logger.info(f"""Cloning doc/content {doc["v_id"]} -> {v_id}""") + # v_id is a sanitized form of a user document ID — DEBUG. + logger.debug(f"""Cloning doc/content {doc["v_id"]} -> {v_id}""") await upsert_chan.put((upsert_doc, (conn, v_id, chunker_type, doc["attributes"]["text"]))) - + # Use get_chunker for all types (including images) # For images, get_chunker returns SingleChunker which preserves markdown image references chunker = ecc_util.get_chunker(chunker_type, graphname=conn.graphname) # decode the text return from tigergraph as it was encoded when written into jsonl file for uploading chunks = chunker.chunk(doc["attributes"]["text"].encode('raw_unicode_escape').decode('unicode_escape')) - - logger.info(f"Chunking {v_id} into {len(chunks)} chunk(s)") + + # v_id / chunk_id derive from user document content. + logger.debug(f"Chunking {v_id} into {len(chunks)} chunk(s)") for i, chunk in enumerate(chunks): chunk_id = f"{v_id}_chunk_{i}" - logger.info(f"Processing chunk {chunk_id}") + logger.debug(f"Processing chunk {chunk_id}") # send chunks to be upserted (func, args) - logger.info("chunk writes to upsert_chan") + logger.debug("chunk writes to upsert_chan") await upsert_chan.put((upsert_chunk, (conn, v_id, chunk_id, chunk))) # send chunks to have entities extracted - logger.info("chunk writes to extract_chan") + logger.debug("chunk writes to extract_chan") await extract_chan.put((chunk, chunk_id)) # send chunks to be embedded - logger.info("chunk writes to embed_chan") + logger.debug("chunk writes to embed_chan") await embed_chan.put((chunk_id, chunk, "DocumentChunk")) return v_id @@ -142,7 +144,7 @@ async def upsert_doc(conn: AsyncTigerGraphConnection, doc_id, ctype, content_tex ) async def upsert_chunk(conn: AsyncTigerGraphConnection, doc_id, chunk_id, chunk): - logger.info(f"Upserting chunk {chunk_id}") + logger.debug(f"Upserting chunk {chunk_id}") date_added = int(time.time()) await util.upsert_vertex( conn, @@ -198,11 +200,11 @@ async def embed( the vertex index to write to """ async with embed_sem: - logger.info(f"Embedding {v_id}") + logger.debug(f"Embedding {v_id}") # if loader is running, wait until it's done if not util.loading_event.is_set(): - logger.info("Embed worker waiting for loading event to finish") + logger.debug("Embed worker waiting for loading event to finish") await util.loading_event.wait() try: await embed_store.aadd_embeddings([(content, [])], [{"vertex_id": v_id}]) @@ -257,20 +259,72 @@ async def extract( async with extract_sem: try: extracted: list[GraphDocument] = await extractor.aextract(chunk) - logger.info( + # chunk_id is user-content-derived; demote. + logger.debug( f"Extracting chunk: {chunk_id} ({len(extracted)} graph docs extracted)" ) except Exception as e: logger.error(f"Failed to extract chunk {chunk_id}: {e}") extracted = [] + # Schema-aware ingest helpers — derive case-insensitive + # lookups from the extractor once per chunk so the loops below + # can map LLM-emitted type strings back to canonical schema names. + domain_vt_canonical: dict = {} + domain_edge_canonical: dict = {} + edge_endpoint_pairs: dict = {} + strict_mode = False + if isinstance(extractor, LLMEntityRelationshipExtractor): + domain_vt_canonical = { + v.casefold(): v for v in (extractor.allowed_vertex_types or []) + } + domain_edge_canonical = { + e.casefold(): e for e in (extractor.allowed_edge_types or []) + } + edge_endpoint_pairs = { + name.casefold(): {(f.casefold(), t.casefold()) for f, t in pairs} + for name, pairs in (extractor.domain_edge_endpoints or {}).items() + } + strict_mode = bool(extractor.strict_mode) + + # ``has_domain_types`` distinguishes the two meta-layer cases: + # Case 1: no domain types on the graph — the EntityType / + # RelationshipType layer becomes a free-text catalog of + # whatever the LLM emitted. + # Case 2: at least one domain type exists — the meta-layer + # is restricted to declared / matched domain types only. + # Non-matched extractions still write to the parallel + # Entity / RELATIONSHIP layer but do not pollute the + # meta layer. + has_domain_types = bool(domain_vt_canonical) or bool(domain_edge_canonical) + # upsert nodes and edges to the graph for doc in extracted: + # Build a node_id → node_type lookup so the relationship + # loop below knows the source/target types (the parser + # currently doesn't carry endpoint types per relationship). + node_type_by_id: dict = {} + for n in doc.nodes: + if not n.id or not n.type: + continue + pid = util.process_id(str(n.id)) + if pid: + node_type_by_id[pid] = n.type + for i, node in enumerate(doc.nodes): - logger.info(f"extract writes entity vert to upsert\nNode: {node.id}") + logger.debug(f"extract writes entity vert to upsert\nNode: {node.id}") v_id = util.process_id(str(node.id)) if len(v_id) == 0: continue + node_type_lower = (node.type or "").casefold() + domain_vt = domain_vt_canonical.get(node_type_lower) + + # Strict mode: drop nodes whose type isn't in the + # schema. When strict_mode is off, non-matched nodes + # fall through to the parallel Entity layer. + if strict_mode and domain_vt is None: + continue + desc = await get_vert_desc(conn, v_id, node) if len(desc[0]) == 0: @@ -290,42 +344,58 @@ async def extract( ), ) ) + # Meta-layer (EntityType / ENTITY_HAS_TYPE) population: + # Case 1 (no domain types): write for every extracted + # node using a normalized form of the LLM-emitted + # type label so trivial variants + # (``Company`` / ``Companies`` / ``company_type``) + # collapse onto one EntityType row. + # Case 2 (domain types exist): write only when the + # node matches a declared domain VT, using the + # canonical domain-VT name as the EntityType id. + meta_type_id = "" if isinstance(extractor, LLMEntityRelationshipExtractor): - logger.info("extract writes type vert to upsert") - type_id = util.process_id(node.type) - if len(type_id) == 0: - continue + if not has_domain_types: + meta_type_id = util.normalize_type_name(node.type) + elif domain_vt is not None: + # Preserve the canonical schema casing + # (``InvestmentFund``) so the EntityType id + # matches what ``upsert_type_metadata`` writes + # at schema-apply time. Lowercasing here would + # produce a duplicate row keyed + # ``investmentfund``. + meta_type_id = domain_vt + if meta_type_id: + logger.debug("extract writes type vert to upsert") await upsert_chan.put( ( - util.upsert_vertex, # func to call + util.upsert_vertex, ( conn, - "EntityType", # v_type - type_id, # v_id - { # attrs - "epoch_added": int(time.time()), - }, - ) + "EntityType", + meta_type_id, + {"epoch_added": int(time.time())}, + ), ) ) - logger.info("extract writes entity_has_type edge to upsert") + logger.debug("extract writes entity_has_type edge to upsert") await upsert_chan.put( ( util.upsert_edge, ( conn, - "Entity", # src_type - v_id, # src_id - "ENTITY_HAS_TYPE", # edgeType - "EntityType", # tgt_type - type_id, # tgt_id - None, # attributes + "Entity", + v_id, + "ENTITY_HAS_TYPE", + "EntityType", + meta_type_id, + None, ), ) ) # link the entity to the chunk it came from - logger.info("extract writes contains edge to upsert") + logger.debug("extract writes contains edge to upsert") await upsert_chan.put( ( util.upsert_edge, @@ -340,6 +410,60 @@ async def extract( ), ) ) + + # Schema-aware: when the node's type matches a domain + # vertex type from the live schema, ALSO upsert the + # vertex as that domain type and link it back to the + # chunk via the multi-pair CONTAINS_ENTITY pair we + # added at init time. + if domain_vt is not None: + logger.debug( + f"extract writes domain {domain_vt} vert + CONTAINS_ENTITY pair" + ) + # Coerce + filter LLM-emitted properties against + # the domain VT's attribute schema before upsert. + # The ``description`` key is for the Entity row and + # never belongs on the domain VT row, so strip it + # before coercion. Domain VTs don't carry the ECC + # bookkeeping ``epoch_added`` attribute either — + # sending it makes TG reject the whole batch. + raw_props = { + k: v for k, v in (node.properties or {}).items() + if k != "description" + } + attr_schema = ( + extractor.entity_type_attributes.get(domain_vt) + if isinstance(extractor, LLMEntityRelationshipExtractor) + else {} + ) + domain_attrs = util.coerce_attrs_for_schema( + raw_props, attr_schema or {} + ) + await upsert_chan.put( + ( + util.upsert_vertex, + ( + conn, + domain_vt, + v_id, + domain_attrs, + ), + ) + ) + await upsert_chan.put( + ( + util.upsert_edge, + ( + conn, + "DocumentChunk", + chunk_id, + "CONTAINS_ENTITY", + domain_vt, + v_id, + None, + ), + ) + ) for node2 in doc.nodes[i + 1:]: v_id2 = util.process_id(str(node2.id)) if len(v_id2) == 0: @@ -360,69 +484,253 @@ async def extract( ) for edge in doc.relationships: - logger.info( + # Edge content includes entity names + relationship + # types pulled from user documents. + logger.debug( f"extract writes relates edge to upsert:{edge.source.id} -({edge.type})-> {edge.target.id}" ) - # upsert verts first to make sure their ID becomes an attr - v_id = util.process_id(edge.source.id) # src_id - if len(v_id) == 0: + src_id = util.process_id(edge.source.id) + tgt_id = util.process_id(edge.target.id) + if len(src_id) == 0 or len(tgt_id) == 0: continue - desc = await get_vert_desc(conn, v_id, edge.source) - if len(desc[0]) == 0: - desc[0] = edge.source.id + + # Look up the source / target types from the per-doc + # node lookup (the parser doesn't currently carry + # endpoint types per relationship). + src_type = node_type_by_id.get(src_id, "") + tgt_type = node_type_by_id.get(tgt_id, "") + + rel_type_lower = (edge.type or "").casefold() + canonical_rel = domain_edge_canonical.get(rel_type_lower) + # Use the canonical-resolved name as the key for the + # endpoint-pair lookup so the check stays correct even + # if ``domain_edge_canonical`` later admits alias → + # canonical mappings. + canonical_rel_key = canonical_rel.casefold() if canonical_rel else "" + valid_pair = ( + canonical_rel is not None + and (src_type.casefold(), tgt_type.casefold()) + in edge_endpoint_pairs.get(canonical_rel_key, set()) + ) + + # Strict mode: only write the typed pattern. Legacy + # Entity → RELATIONSHIP → Entity fallback applies only + # when strict_mode is off. + if strict_mode and not valid_pair: + continue + + # ---- Legacy raw layer: Entity src + Entity tgt + RELATIONSHIP edge ---- + src_desc = await get_vert_desc(conn, src_id, edge.source) + if len(src_desc[0]) == 0: + src_desc[0] = edge.source.id await upsert_chan.put( ( - util.upsert_vertex, # func to call + util.upsert_vertex, ( conn, - "Entity", # v_type - v_id, - { # attrs - "description": desc, + "Entity", + src_id, + { + "description": src_desc, "epoch_added": int(time.time()), }, ), ) ) - v_id = util.process_id(edge.target.id) - if len(v_id) == 0: - continue - desc = await get_vert_desc(conn, v_id, edge.target) - if len(desc[0]) == 0: - desc[0] = edge.target.id + tgt_desc = await get_vert_desc(conn, tgt_id, edge.target) + if len(tgt_desc[0]) == 0: + tgt_desc[0] = edge.target.id await upsert_chan.put( ( - util.upsert_vertex, # func to call + util.upsert_vertex, ( conn, - "Entity", # v_type - v_id, # src_id - { # attrs - "description": desc, + "Entity", + tgt_id, + { + "description": tgt_desc, "epoch_added": int(time.time()), }, ), ) ) - - # upsert the edge between the two entities await upsert_chan.put( ( util.upsert_edge, ( conn, - "Entity", # src_type - util.process_id(edge.source.id), # src_id - "RELATIONSHIP", # edgeType - "Entity", # tgt_type - util.process_id(edge.target.id), # tgt_id - {"relation_type": edge.type}, # attributes + "Entity", + src_id, + "RELATIONSHIP", + "Entity", + tgt_id, + {"relation_type": edge.type}, ), ) ) - # embed "RelationshipType", - # (v_id, content, index_name) - # right now, we're not embedding relationships in graphrag + + # ---- Meta-schema typed-relationship layer ---- + # Two cases: + # Case 1 (no domain types): every extracted + # relationship contributes RelationshipType (via + # LLM-emitted edge.type) and IS_HEAD_OF / HAS_TAIL + # edges between the corresponding EntityType + # vertices (via LLM-emitted src_type / tgt_type). + # Case 2 (domain types exist) with valid_pair: + # same writes but using canonical (declared) names. + # Case 2 without valid_pair: skip the meta-layer + # entirely. The Entity / RELATIONSHIP write + # above is the only persistence for unmatched + # extractions. + # + # IS_HEAD_OF / HAS_TAIL connect EntityType ↔ + # RelationshipType (meta layer), NOT individual domain + # vertex instances. Per-instance domain edges (e.g. + # ``Company → PUBLISHES → Report``) are written + # separately when valid_pair holds. + meta_rel_id = "" + meta_src_et_id = "" + meta_tgt_et_id = "" + if valid_pair: + meta_rel_id = canonical_rel + # Preserve canonical schema casing so the EntityType + # id matches the entity-side write and the row that + # ``upsert_type_metadata`` lays down at schema-apply + # time (``InvestmentFund``, not + # ``investmentfund``). + meta_src_et_id = domain_vt_canonical.get( + src_type.casefold(), "" + ) + meta_tgt_et_id = domain_vt_canonical.get( + tgt_type.casefold(), "" + ) + elif not has_domain_types: + # Case 1: dedup variants via normalize_type_name so + # the meta-layer doesn't overflow with near-duplicate + # labels (``Company``/``Companies``, + # ``WORKS_FOR``/``works_for_type``, etc.). + meta_rel_id = util.normalize_type_name(edge.type) + meta_src_et_id = util.normalize_type_name(src_type) + meta_tgt_et_id = util.normalize_type_name(tgt_type) + + if meta_rel_id and meta_src_et_id and meta_tgt_et_id: + now = int(time.time()) + await upsert_chan.put( + ( + util.upsert_vertex, + (conn, "RelationshipType", meta_rel_id, {"epoch_added": now}), + ) + ) + await upsert_chan.put( + ( + util.upsert_vertex, + (conn, "EntityType", meta_src_et_id, {"epoch_added": now}), + ) + ) + await upsert_chan.put( + ( + util.upsert_vertex, + (conn, "EntityType", meta_tgt_et_id, {"epoch_added": now}), + ) + ) + await upsert_chan.put( + ( + util.upsert_edge, + ( + conn, + "EntityType", + meta_src_et_id, + "IS_HEAD_OF", + "RelationshipType", + meta_rel_id, + None, + ), + ) + ) + await upsert_chan.put( + ( + util.upsert_edge, + ( + conn, + "RelationshipType", + meta_rel_id, + "HAS_TAIL", + "EntityType", + meta_tgt_et_id, + None, + ), + ) + ) + # Chunk → RelationshipType — fires whenever any + # meta-layer write fires (Case 1 always, Case 2 on + # valid_pair). + await upsert_chan.put( + ( + util.upsert_edge, + ( + conn, + "DocumentChunk", + chunk_id, + "MENTIONS_RELATIONSHIP", + "RelationshipType", + meta_rel_id, + None, + ), + ) + ) + + if valid_pair: + # Schema-aware: also write the canonical domain VT + # instances and the per-instance domain edge (the + # schema-declared edge name like ``PUBLISHES``). + # Domain VTs don't carry the ECC bookkeeping + # ``epoch_added`` attribute — sending it makes TG + # reject the whole batch with ``Unknown vertex + # attribute or vector name: epoch_added``. + canonical_src_vt = domain_vt_canonical.get(src_type.casefold()) + canonical_tgt_vt = domain_vt_canonical.get(tgt_type.casefold()) + await upsert_chan.put( + ( + util.upsert_vertex, + (conn, canonical_src_vt, src_id, {}), + ) + ) + await upsert_chan.put( + ( + util.upsert_vertex, + (conn, canonical_tgt_vt, tgt_id, {}), + ) + ) + # Coerce + filter LLM-emitted edge properties + # against the edge's attribute schema. ``description`` + # is the Entity-side payload and never belongs on + # the domain edge row. + edge_raw_props = { + k: v for k, v in (edge.properties or {}).items() + if k != "description" + } + edge_attr_schema = ( + extractor.relationship_type_attributes.get(canonical_rel) + if isinstance(extractor, LLMEntityRelationshipExtractor) + else {} + ) + domain_edge_attrs = util.coerce_edge_attrs_for_schema( + edge_raw_props, edge_attr_schema or {} + ) + await upsert_chan.put( + ( + util.upsert_edge, + ( + conn, + canonical_src_vt, + src_id, + canonical_rel, + canonical_tgt_vt, + tgt_id, + domain_edge_attrs or None, + ), + ) + ) comm_sem = asyncio.Semaphore(util._worker_concurrency) diff --git a/ecc/app/main.py b/ecc/app/main.py index 58751bb..9889dc0 100644 --- a/ecc/app/main.py +++ b/ecc/app/main.py @@ -33,7 +33,7 @@ from common.config import ( db_config, graphrag_config, - embedding_service, + get_embedding_service, get_llm_service, get_completion_config, get_graphrag_config, @@ -92,13 +92,18 @@ def initialize_eventual_consistency_checker( try: maj, minor, patch = conn.getVer().split(".") - if maj >= "4" and minor >= "2": + if maj >= "4" and minor >= "2": # TigerGraph native vector support embedding_store = TigerGraphEmbeddingStore( conn, - embedding_service, + get_embedding_service(), support_ai_instance=False, ) + else: + raise ValueError( + f"TigerGraph version {maj}.{minor}.{patch} is not supported. " + "Requires >= 4.2." + ) graph_cfg = get_graphrag_config(graphname) index_names = graph_cfg.get( "indexes", @@ -108,7 +113,9 @@ def initialize_eventual_consistency_checker( if graph_cfg.get("extractor") == "llm": from common.extractors import LLMEntityRelationshipExtractor - extractor = LLMEntityRelationshipExtractor(get_llm_service(get_completion_config())) + extractor = LLMEntityRelationshipExtractor( + get_llm_service(get_completion_config(graphname)) + ) else: raise ValueError("Invalid extractor type") @@ -116,7 +123,7 @@ def initialize_eventual_consistency_checker( graph_cfg.get("process_interval_seconds", 300), graph_cfg.get("cleanup_interval_seconds", 300), graphname, - embedding_service, + get_embedding_service(), embedding_store, index_names, conn, @@ -171,6 +178,27 @@ def root(): return {"status": "ok"} +@app.get("/version") +def version(): + """Return image-build version info. ``VERSION`` is the repo-root + file copied into the image; ``BUILD_DATE`` is stamped at build + time by the Dockerfile. Both fall back to ``unknown`` when the + files aren't present. + """ + def _safe_read(path: str) -> str: + try: + with open(path) as f: + return f.read().strip() + except Exception: + return "unknown" + + return { + "component": "graphrag-ecc", + "version": _safe_read("/code/VERSION"), + "build_date": _safe_read("/code/BUILD_DATE"), + } + + @app.get("/{graphname}/{ecc_method}/rebuild_status") def rebuild_status( graphname: str, @@ -197,6 +225,7 @@ def rebuild_status( "method": ecc_method, "is_running": task_info.get("status") == "running", "status": task_info.get("status"), + "stage": task_info.get("stage"), "started_at": task_info.get("started_at"), "completed_at": task_info.get("completed_at"), "failed_at": task_info.get("failed_at"), @@ -211,10 +240,24 @@ def rebuild_status( } +def _set_stage(task_key: str, msg: str) -> None: + """Update the human-readable stage label for an in-flight task. + Pulled out so individual stage transitions don't have to know + about the ``running_tasks`` schema. + """ + info = running_tasks.get(task_key) + if info is not None: + info["stage"] = msg + + async def run_with_tracking(task_key: str, run_func, graphname: str, conn): """Wrapper to track running tasks""" try: - running_tasks[task_key] = {"status": "running", "started_at": time.time()} + running_tasks[task_key] = { + "status": "running", + "started_at": time.time(), + "stage": "Preparing rebuild", + } LogWriter.info(f"Starting ECC task: {task_key}") # Verify the graph still exists before doing any work @@ -256,8 +299,16 @@ async def run_with_tracking(task_key: str, run_func, graphname: str, conn): else: LogWriter.warning(f"GraphRAG config reload had issues: {graphrag_result['message']}") - # Now run the actual job with fresh config - await run_func(graphname, conn) + # Now run the actual job with fresh config. Pass a progress + # callback so sub-phases can surface in the UI rebuild dialog. + # ``run_func`` may ignore the kwarg (the supportai legacy path + # does); the call falls back to the no-progress signature on + # ``TypeError``. + progress_cb = lambda msg: _set_stage(task_key, msg) + try: + await run_func(graphname, conn, progress=progress_cb) + except TypeError: + await run_func(graphname, conn) running_tasks[task_key] = {"status": "completed", "completed_at": time.time()} LogWriter.info(f"Completed ECC task: {task_key}") except Exception as e: diff --git a/ecc/app/supportai/supportai_init.py b/ecc/app/supportai/supportai_init.py index 8d59a5b..d622737 100644 --- a/ecc/app/supportai/supportai_init.py +++ b/ecc/app/supportai/supportai_init.py @@ -21,7 +21,7 @@ from aiochannel import Channel from pyTigerGraph import TigerGraphConnection -from common.config import embedding_service, entity_extraction_switch, doc_process_switch +from common.config import get_embedding_service, entity_extraction_switch, doc_process_switch from common.embeddings.base_embedding_store import EmbeddingStore from common.extractors.BaseExtractor import BaseExtractor from supportai import workers @@ -59,7 +59,8 @@ async def stream_docs( async with tg_sem: res = await conn.runInstalledQuery( "StreamDocContent", - params={"doc": d}, + # 1-tuple form for VERTEX params. + params={"doc": (d,)}, ) logger.info("stream_docs writes to docs") await docs_chan.put(res[0]["DocContent"][0]) @@ -90,7 +91,7 @@ async def chunk_docs( # v_id = content["v_id"] # txt = content["attributes"]["text"] - logger.info("chunk writes to extract") + logger.debug("chunk writes to extract") # await embed_chan.put((v_id, txt, "Document")) task = sp.create_task( @@ -137,10 +138,11 @@ async def embed( async with asyncio.TaskGroup() as sp: # consume task queue async for v_id, content, index_name in embed_chan: - logger.info(f"Embed to {graphname}_{index_name}: {v_id}") + # v_id derives from user content. + logger.debug(f"Embed to {graphname}_{index_name}: {v_id}") sp.create_task( workers.embed( - embedding_service, + get_embedding_service(), embedding_store, v_id, content, diff --git a/ecc/app/supportai/util.py b/ecc/app/supportai/util.py index 3e3f07a..1b1328a 100644 --- a/ecc/app/supportai/util.py +++ b/ecc/app/supportai/util.py @@ -12,12 +12,13 @@ from pyTigerGraph import TigerGraphConnection from common.config import ( - embedding_service, + get_embedding_service, graphrag_config, get_llm_service, get_completion_config, get_graphrag_config, ) +from common.db.schema_utils import gsql_output_error from common.embeddings.base_embedding_store import EmbeddingStore from common.embeddings.tigergraph_embedding_store import TigerGraphEmbeddingStore from common.extractors import GraphExtractor, LLMEntityRelationshipExtractor @@ -64,8 +65,8 @@ async def install_queries( async with tg_sem: res = await conn.gsql(query) logger.info(f"INSTALL QUERY ALL returned: {str(res)[:200]}") - res_lower = res.lower() if isinstance(res, str) else "" - if "error" in res_lower or "does not exist" in res_lower or "failed" in res_lower: + err = gsql_output_error(res) if isinstance(res, str) else None + if err: raise Exception(res) max_wait = 300 # seconds @@ -122,7 +123,7 @@ async def init( embedding_store = TigerGraphEmbeddingStore( conn, - embedding_service, + get_embedding_service(), support_ai_instance=True, ) embedding_store.set_graphname(conn.graphname) diff --git a/ecc/app/supportai/workers.py b/ecc/app/supportai/workers.py index 07104fb..ce85274 100644 --- a/ecc/app/supportai/workers.py +++ b/ecc/app/supportai/workers.py @@ -89,26 +89,28 @@ async def chunk_doc( chunker = ecc_util.get_chunker(chunker_type, graphname=conn.graphname) chunks = chunker.chunk(doc["attributes"]["text"]) - logger.info(f"Chunking {v_id} into {len(chunks)} chunk(s)") + # v_id / chunk_id derive from user document content; demote + # to DEBUG so the steady-state log doesn't carry data identifiers. + logger.debug(f"Chunking {v_id} into {len(chunks)} chunk(s)") for i, chunk in enumerate(chunks): chunk_id = f"{v_id}_chunk_{i}" # send chunks to be upserted (func, args) - logger.info("chunk writes to upsert_chan") + logger.debug("chunk writes to upsert_chan") await upsert_chan.put((upsert_chunk, (conn, v_id, chunk_id, chunk))) # send chunks to be embedded - logger.info("chunk writes to embed_chan") + logger.debug("chunk writes to embed_chan") await embed_chan.put((chunk_id, chunk, "DocumentChunk")) # send chunks to have entities extracted - logger.info("chunk writes to extract_chan") + logger.debug("chunk writes to extract_chan") await extract_chan.put((chunk, chunk_id)) return doc["v_id"] async def upsert_chunk(conn: TigerGraphConnection, doc_id, chunk_id, chunk): - logger.info(f"Upserting chunk {chunk_id}") + logger.debug(f"Upserting chunk {chunk_id}") date_added = int(time.time()) await util.upsert_vertex( conn, @@ -160,7 +162,7 @@ async def embed( index_name: str the vertex index to write to """ - logger.info(f"Embedding {v_id}") + logger.debug(f"Embedding {v_id}") await embed_store.aadd_embeddings([(content, [])], [{"vertex_id": v_id}]) @@ -201,12 +203,12 @@ async def extract( chunk: str, chunk_id: str, ): - logger.info(f"Extracting chunk: {chunk_id}") + logger.debug(f"Extracting chunk: {chunk_id}") extracted: list[GraphDocument] = await extractor.aextract(chunk) # upsert nodes and edges to the graph for doc in extracted: for node in doc.nodes: - logger.info(f"extract writes entity vert to upsert\nNode: {node.id}") + logger.debug(f"extract writes entity vert to upsert\nNode: {node.id}") v_id = util.process_id(str(node.id)) if len(v_id) == 0: continue @@ -231,7 +233,7 @@ async def extract( ) # link the entity to the chunk it came from - logger.info("extract writes contains edge to upsert") + logger.debug("extract writes contains edge to upsert") await upsert_chan.put( ( util.upsert_edge, @@ -303,36 +305,12 @@ async def extract( ) ) - # upsert the edge between the two entities - await upsert_chan.put( - ( - util.upsert_edge, - ( - conn, - "Entity", # src_type - util.process_id(edge.source.id), # src_id - "IS_HEAD_OF", # edgeType - "RelationshipType", # tgt_type - edge.type, # tgt_id - ), - ) - ) - await upsert_chan.put( - ( - util.upsert_edge, - ( - conn, - "RelationshipType", # src_type - edge.type, # src_id - "HAS_TAIL", # edgeType - "Entity", # tgt_type - util.process_id(edge.target.id), # tgt_id - ), - ) - ) - + # IS_HEAD_OF / HAS_TAIL are meta-schema edges between + # EntityType ↔ RelationshipType — not written here per + # Entity instance. Legacy supportai ECC paths without + # per-instance entity_type info skip the meta-layer. # link the relationship to the chunk it came from - logger.info("extract writes mentions edge to upsert") + logger.debug("extract writes mentions edge to upsert") await upsert_chan.put( ( util.upsert_edge, diff --git a/graphrag-ui/.npmrc b/graphrag-ui/.npmrc new file mode 100644 index 0000000..87e25fb --- /dev/null +++ b/graphrag-ui/.npmrc @@ -0,0 +1,2 @@ +only-built-dependencies[]=@swc/core +only-built-dependencies[]=esbuild diff --git a/graphrag-ui/Dockerfile b/graphrag-ui/Dockerfile index aec0713..c87dbf0 100644 --- a/graphrag-ui/Dockerfile +++ b/graphrag-ui/Dockerfile @@ -1,4 +1,4 @@ -FROM node:23.7-slim +FROM node:23.7-slim WORKDIR /app ENV PNPM_HOME="/pnpm" @@ -7,6 +7,14 @@ RUN corepack enable COPY . . +# Pull the shared VERSION file from the repo root (exposed via the +# ``repo`` additional build context). Compose ``docker compose build`` +# resolves it automatically; no env vars or wrapper script needed. +COPY --from=repo VERSION ./public/version +RUN mkdir -p public && \ + echo "{\"component\":\"graphrag-ui\",\"version\":\"$(cat public/version | tr -d '\n')\",\"build_date\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > public/version.json && \ + rm public/version + RUN pnpm install RUN pnpm run build RUN pnpm i -g serve diff --git a/graphrag-ui/package-lock.json b/graphrag-ui/package-lock.json new file mode 100644 index 0000000..7def4e4 --- /dev/null +++ b/graphrag-ui/package-lock.json @@ -0,0 +1,10906 @@ +{ + "name": "tg-cbot-v5", + "version": "0.0.5", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tg-cbot-v5", + "version": "0.0.5", + "dependencies": { + "@hookform/resolvers": "^3.6.0", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-radio-group": "^1.2.0", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tabs": "^1.1.0", + "@react-three/drei": "9.56.1", + "@react-three/fiber": "8.13.3", + "@tailwindcss/typography": "^0.5.18", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "i18next": "^23.11.5", + "install": "^0.13.0", + "lucide-react": "^0.390.0", + "npm": "^10.8.1", + "react": "^18.3.1", + "react-chatbot-kit": "^2.2.2", + "react-dom": "^18.3.1", + "react-hook-form": "^7.51.5", + "react-i18next": "^14.1.2", + "react-icons": "^5.2.1", + "react-markdown": "^9.0.1", + "react-router-dom": "^6.23.1", + "react-use-websocket": "^4.8.1", + "reagraph": "4.15.19", + "remark-gfm": "^4.0.0", + "tailwind-merge": "^2.3.0", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.23.8" + }, + "devDependencies": { + "@playwright/test": "^1.59.1", + "@types/node": "^25.6.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@typescript-eslint/eslint-plugin": "^7.5.0", + "@typescript-eslint/parser": "^7.5.0", + "@vitejs/plugin-react-swc": "^3.6.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^6.1.1", + "eslint-plugin-react-refresh": "^0.4.6", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.18", + "typescript": "^5.2.2", + "vite": "^5.2.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz", + "integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==", + "license": "Apache-2.0" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/three": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz", + "integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "three": ">=0.126" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", + "license": "MIT" + }, + "node_modules/@react-three/drei": { + "version": "9.56.1", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.56.1.tgz", + "integrity": "sha512-xHQHMqqn4ww62YVDoXLazFhhrM5pkzoaA/2v5ytjbKjU9hP2iHos3odxGxQEKUS0WXwduziP6ScRkdSevpDFsQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@react-spring/three": "^9.3.1", + "@use-gesture/react": "^10.2.0", + "camera-controls": "^1.38.0", + "detect-gpu": "^5.0.8", + "glsl-noise": "^0.0.0", + "lodash.clamp": "^4.0.3", + "lodash.omit": "^4.5.0", + "lodash.pick": "^4.4.0", + "maath": "^0.5.2", + "meshline": "^3.1.6", + "react-composer": "^5.0.3", + "react-merge-refs": "^1.1.0", + "stats.js": "^0.17.0", + "suspend-react": "^0.0.8", + "three-mesh-bvh": "^0.5.22", + "three-stdlib": "^2.21.6", + "troika-three-text": "^0.47.1", + "utility-types": "^3.10.0", + "zustand": "^3.5.13" + }, + "peerDependencies": { + "@react-three/fiber": ">=8.0", + "react": ">=18.0", + "react-dom": ">=18.0", + "three": ">=0.137" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "8.13.3", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.13.3.tgz", + "integrity": "sha512-mCdTUB8D1kwlsOSxGhUg5nuGHt3HN3aNFc0s9I/N7ayk+nzT2ttLdn49c56nrHu+YK+SU1xnrxe6LqftZgIRmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.26.7", + "its-fine": "^1.0.6", + "react-reconciler": "^0.27.0", + "react-use-measure": "^2.1.1", + "scheduler": "^0.21.0", + "suspend-react": "^0.1.3", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-gl": ">=11.0", + "react": ">=18.0", + "react-dom": ">=18.0", + "react-native": ">=0.64", + "three": ">=0.133" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber/node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.24.tgz", + "integrity": "sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.24", + "@swc/core-darwin-x64": "1.15.24", + "@swc/core-linux-arm-gnueabihf": "1.15.24", + "@swc/core-linux-arm64-gnu": "1.15.24", + "@swc/core-linux-arm64-musl": "1.15.24", + "@swc/core-linux-ppc64-gnu": "1.15.24", + "@swc/core-linux-s390x-gnu": "1.15.24", + "@swc/core-linux-x64-gnu": "1.15.24", + "@swc/core-linux-x64-musl": "1.15.24", + "@swc/core-win32-arm64-msvc": "1.15.24", + "@swc/core-win32-ia32-msvc": "1.15.24", + "@swc/core-win32-x64-msvc": "1.15.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.24.tgz", + "integrity": "sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.24.tgz", + "integrity": "sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.24.tgz", + "integrity": "sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.24.tgz", + "integrity": "sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.24.tgz", + "integrity": "sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.24.tgz", + "integrity": "sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.24.tgz", + "integrity": "sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.24.tgz", + "integrity": "sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.24.tgz", + "integrity": "sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.24.tgz", + "integrity": "sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.24.tgz", + "integrity": "sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.24.tgz", + "integrity": "sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz", + "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.183.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", + "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~1.0.1" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", + "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@yomguithereal/helpers": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@yomguithereal/helpers/-/helpers-1.1.1.tgz", + "integrity": "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz", + "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/camera-controls": { + "version": "1.38.2", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-1.38.2.tgz", + "integrity": "sha512-EfzbovxLssyWpJVG9uKcazSDDIEcd1hUsPhPF/OWWnICsKY9WbLY/2S4UPW73HHbvnVeR/Z9wsWaQKtANy/2Yg==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.126.1" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/ctrl-keys": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ctrl-keys/-/ctrl-keys-1.0.6.tgz", + "integrity": "sha512-fENSKrbIfvX83uHxruP3S/9GizirvgT66vHhgKHOCTVHK+22Xpud/vttg5c5IifRl+6Gom/GjE+ZSXJKf0DMTA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force-3d": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", + "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", + "license": "MIT", + "dependencies": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-octree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", + "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", + "license": "MIT" + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", + "dev": true, + "license": "ISC" + }, + "node_modules/ellipsize": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/ellipsize/-/ellipsize-0.5.1.tgz", + "integrity": "sha512-0jEAyuIRU6U8MN0S5yUqIrkK/AQWkChh642N3zQuGV57s9bsUWYLc0jJOoDIUkZ2sbEL3ySq8xfq71BvG4q3hw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.1.1.tgz", + "integrity": "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "zod": "^3.22.4 || ^4.0.0", + "zod-validation-error": "^3.0.3 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glodrei": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/glodrei/-/glodrei-0.0.1.tgz", + "integrity": "sha512-DMx6ElCSwh1pR4IyDS3LvyFwZHSCCKCqdqo8P1G7klQtqH6PcOjleduCDsHehDtyYQ1E4dzVeoEzHIL1DIxjag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@mediapipe/tasks-vision": "0.10.8", + "@react-spring/three": "~9.6.1", + "@use-gesture/react": "^10.2.24", + "camera-controls": "^2.4.2", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.28", + "glsl-noise": "^0.0.0", + "maath": "^0.10.7", + "meshline": "^3.1.6", + "react-composer": "^5.0.3", + "react-merge-refs": "^1.1.0", + "stats-gl": "^2.0.0", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.7.0", + "three-stdlib": "^2.29.4", + "troika-three-text": "^0.47.2", + "tunnel-rat": "^0.1.2", + "utility-types": "^3.10.0", + "uuid": "^9.0.1", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "@react-three/fiber": ">=8.0", + "react": ">=18.0", + "react-dom": ">=18.0", + "three": ">=0.137" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/glodrei/node_modules/@react-spring/animated": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz", + "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/glodrei/node_modules/@react-spring/core": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz", + "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/rafz": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/glodrei/node_modules/@react-spring/rafz": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz", + "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==", + "license": "MIT" + }, + "node_modules/glodrei/node_modules/@react-spring/shared": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz", + "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/glodrei/node_modules/@react-spring/three": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz", + "integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/core": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "three": ">=0.126" + } + }, + "node_modules/glodrei/node_modules/@react-spring/types": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz", + "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==", + "license": "MIT" + }, + "node_modules/glodrei/node_modules/camera-controls": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz", + "integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.126.1" + } + }, + "node_modules/glodrei/node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, + "node_modules/glodrei/node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/glodrei/node_modules/three-mesh-bvh": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.6.tgz", + "integrity": "sha512-rCjsnxEqR9r1/C/lCqzGLS67NDty/S/eT6rAJfDvsanrIctTWdNoR4ZOGWewCB13h1QkVo2BpmC0wakj1+0m8A==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.151.0" + } + }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/graphology": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.25.4.tgz", + "integrity": "sha512-33g0Ol9nkWdD6ulw687viS8YJQBxqG5LWII6FI6nul0pq6iM2t5EKquOTFDbyTblRB3O9I+7KX4xI8u5ffekAQ==", + "license": "MIT", + "dependencies": { + "events": "^3.3.0", + "obliterator": "^2.0.2" + }, + "peerDependencies": { + "graphology-types": ">=0.24.0" + } + }, + "node_modules/graphology-indices": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/graphology-indices/-/graphology-indices-0.17.0.tgz", + "integrity": "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ==", + "license": "MIT", + "dependencies": { + "graphology-utils": "^2.4.2", + "mnemonist": "^0.39.0" + }, + "peerDependencies": { + "graphology-types": ">=0.20.0" + } + }, + "node_modules/graphology-layout": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/graphology-layout/-/graphology-layout-0.6.1.tgz", + "integrity": "sha512-m9aMvbd0uDPffUCFPng5ibRkb2pmfNvdKjQWeZrf71RS1aOoat5874+DcyNfMeCT4aQguKC7Lj9eCbqZj/h8Ag==", + "license": "MIT", + "dependencies": { + "graphology-utils": "^2.3.0", + "pandemonium": "^2.4.0" + }, + "peerDependencies": { + "graphology-types": ">=0.19.0" + } + }, + "node_modules/graphology-layout-forceatlas2": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.10.1.tgz", + "integrity": "sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==", + "license": "MIT", + "dependencies": { + "graphology-utils": "^2.1.0" + }, + "peerDependencies": { + "graphology-types": ">=0.19.0" + } + }, + "node_modules/graphology-layout-noverlap": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/graphology-layout-noverlap/-/graphology-layout-noverlap-0.4.2.tgz", + "integrity": "sha512-13WwZSx96zim6l1dfZONcqLh3oqyRcjIBsqz2c2iJ3ohgs3605IDWjldH41Gnhh462xGB1j6VGmuGhZ2FKISXA==", + "license": "MIT", + "dependencies": { + "graphology-utils": "^2.3.0" + }, + "peerDependencies": { + "graphology-types": ">=0.19.0" + } + }, + "node_modules/graphology-metrics": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/graphology-metrics/-/graphology-metrics-2.4.0.tgz", + "integrity": "sha512-7WOfOP+mFLCaTJx55Qg4eY+211vr1/b3D/R3biz3SXGhAaCVcWYkfabnmO4O4WBNWANEHtVnFrGgJ0kj6MM6xw==", + "license": "MIT", + "dependencies": { + "graphology-indices": "^0.17.0", + "graphology-shortest-path": "^2.0.0", + "graphology-utils": "^2.4.4", + "mnemonist": "^0.39.0", + "pandemonium": "2.4.1" + }, + "peerDependencies": { + "graphology-types": ">=0.20.0" + } + }, + "node_modules/graphology-shortest-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/graphology-shortest-path/-/graphology-shortest-path-2.1.0.tgz", + "integrity": "sha512-KbT9CTkP/u72vGEJzyRr24xFC7usI9Es3LMmCPHGwQ1KTsoZjxwA9lMKxfU0syvT/w+7fZUdB/Hu2wWYcJBm6Q==", + "license": "MIT", + "dependencies": { + "@yomguithereal/helpers": "^1.1.1", + "graphology-indices": "^0.17.0", + "graphology-utils": "^2.4.3", + "mnemonist": "^0.39.0" + }, + "peerDependencies": { + "graphology-types": ">=0.20.0" + } + }, + "node_modules/graphology-types": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz", + "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==", + "license": "MIT", + "peer": true + }, + "node_modules/graphology-utils": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz", + "integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==", + "license": "MIT", + "peerDependencies": { + "graphology-types": ">=0.23.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hold-event": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/hold-event/-/hold-event-0.2.0.tgz", + "integrity": "sha512-rko5P1XgHzy4B0NR0xVHEpWPgj0i23f8Mf8qsOugd1CHvfLR0PyIyy+8TAQQA9v8qAa1OZ4XuCKk04rxmPGHNQ==", + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/i18next": { + "version": "23.16.8", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", + "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/its-fine": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", + "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.0" + }, + "peerDependencies": { + "react": ">=18.0" + } + }, + "node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.clamp": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/lodash.clamp/-/lodash.clamp-4.0.3.tgz", + "integrity": "sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.omit": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.18.0.tgz", + "integrity": "sha512-hZXIupXdHtocTnvIJ2aCd2vxKYtxex6gbiGuPvgBRnFQO9yu3AtmDAbVuCXcSsQx3INo/1g71OktlFFA/ES8Xg==", + "license": "MIT" + }, + "node_modules/lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", + "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.390.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.390.0.tgz", + "integrity": "sha512-APqbfEcVuHnZbiy3E97gYWLeBdkE4e6NbY6AuVETZDZVn/bQCHYUoHyxcUHyvRopfPOHhFUEvDyyQzHwM+S9/w==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/maath": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.5.3.tgz", + "integrity": "sha512-ut63A4zTd9abtpi+sOHW1fPWPtAFrjK0E17eAthx1k93W/T2cWLKV5oaswyotJVDvvW1EXSdokAqhK5KOu0Qdw==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", + "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "license": "MIT" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mnemonist": { + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", + "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm": { + "version": "10.9.8", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.8.tgz", + "integrity": "sha512-fYwb6ODSmHkqrJQQaCxY3M2lPf/mpgC7ik0HSzzIwG5CGtabRp4bNqikatvCoT42b5INQSqudVH0R7yVmC9hVg==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/redact", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "license": "Artistic-2.0", + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^8.0.5", + "@npmcli/config": "^9.0.0", + "@npmcli/fs": "^4.0.0", + "@npmcli/map-workspaces": "^4.0.2", + "@npmcli/package-json": "^6.2.0", + "@npmcli/promise-spawn": "^8.0.3", + "@npmcli/redact": "^3.2.2", + "@npmcli/run-script": "^9.1.0", + "@sigstore/tuf": "^3.1.1", + "abbrev": "^3.0.1", + "archy": "~1.0.0", + "cacache": "^19.0.1", + "chalk": "^5.6.2", + "ci-info": "^4.4.0", + "cli-columns": "^4.0.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^8.1.0", + "ini": "^5.0.0", + "init-package-json": "^7.0.2", + "is-cidr": "^5.1.1", + "json-parse-even-better-errors": "^4.0.0", + "libnpmaccess": "^9.0.0", + "libnpmdiff": "^7.0.5", + "libnpmexec": "^9.0.5", + "libnpmfund": "^6.0.5", + "libnpmhook": "^11.0.0", + "libnpmorg": "^7.0.0", + "libnpmpack": "^8.0.5", + "libnpmpublish": "^10.0.2", + "libnpmsearch": "^8.0.0", + "libnpmteam": "^7.0.0", + "libnpmversion": "^7.0.0", + "make-fetch-happen": "^14.0.3", + "minimatch": "^9.0.9", + "minipass": "^7.1.3", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^11.5.0", + "nopt": "^8.1.0", + "normalize-package-data": "^7.0.1", + "npm-audit-report": "^6.0.0", + "npm-install-checks": "^7.1.2", + "npm-package-arg": "^12.0.2", + "npm-pick-manifest": "^10.0.0", + "npm-profile": "^11.0.1", + "npm-registry-fetch": "^18.0.2", + "npm-user-validate": "^3.0.0", + "p-map": "^7.0.4", + "pacote": "^19.0.1", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^4.1.0", + "semver": "^7.7.4", + "spdx-expression-parse": "^4.0.0", + "ssri": "^12.0.0", + "supports-color": "^9.4.0", + "tar": "^7.5.11", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^6.0.2", + "which": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "8.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^4.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/metavuln-calculator": "^8.0.0", + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.1", + "@npmcli/query": "^4.0.0", + "@npmcli/redact": "^3.0.0", + "@npmcli/run-script": "^9.0.1", + "bin-links": "^5.0.0", + "cacache": "^19.0.1", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^10.2.2", + "minimatch": "^9.0.4", + "nopt": "^8.0.0", + "npm-install-checks": "^7.1.0", + "npm-package-arg": "^12.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.1", + "pacote": "^19.0.0", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "proggy": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "promise-retry": "^2.0.1", + "read-package-json-fast": "^4.0.0", + "semver": "^7.3.7", + "ssri": "^12.0.0", + "treeverse": "^3.0.0", + "walk-up-path": "^3.0.1" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/package-json": "^6.0.1", + "ci-info": "^4.0.0", + "ini": "^5.0.0", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "6.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "4.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "8.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^19.0.0", + "json-parse-even-better-errors": "^4.0.0", + "pacote": "^20.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator/node_modules/pacote": { + "version": "20.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^7.5.10" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "6.2.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "8.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "3.2.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "9.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "3.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/core": { + "version": "2.0.0", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.4.3", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "3.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "3.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.1", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "2.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "6.2.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.3.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "19.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.6.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "3.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.4.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "4.1.3", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^5.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.4.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/diff": { + "version": "5.2.2", + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.3", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/fdir": { + "version": "6.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.3.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "10.5.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "8.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.2.0", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "7.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^6.0.0", + "npm-package-arg": "^12.0.0", + "promzard": "^2.0.0", + "read": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "10.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "5.1.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^4.1.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "3.4.3", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "7.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.5", + "@npmcli/installed-package-contents": "^3.0.0", + "binary-extensions": "^2.3.0", + "diff": "^5.1.0", + "minimatch": "^9.0.4", + "npm-package-arg": "^12.0.0", + "pacote": "^19.0.0", + "tar": "^7.5.11" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "9.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.5", + "@npmcli/run-script": "^9.0.1", + "ci-info": "^4.0.0", + "npm-package-arg": "^12.0.0", + "pacote": "^19.0.0", + "proc-log": "^5.0.0", + "read": "^4.0.0", + "read-package-json-fast": "^4.0.0", + "semver": "^7.3.7", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "6.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "11.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "8.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.5", + "@npmcli/run-script": "^9.0.1", + "npm-package-arg": "^12.0.0", + "pacote": "^19.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "10.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ci-info": "^4.0.0", + "normalize-package-data": "^7.0.0", + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1", + "proc-log": "^5.0.0", + "semver": "^7.3.7", + "sigstore": "^3.0.0", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.1", + "@npmcli/run-script": "^9.0.1", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "10.4.3", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "14.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "9.0.9", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.1.3", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "4.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/minizlib": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/negotiator": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "11.5.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "8.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "7.0.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^8.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "7.1.2", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "12.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "10.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "11.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "18.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "3.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "7.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/package-json-from-dist": { + "version": "1.0.1", + "inBundle": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/npm/node_modules/pacote": { + "version": "19.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^7.5.10" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.11.1", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/picomatch": { + "version": "4.0.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/proggy": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "4.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "3.1.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "@sigstore/sign": "^3.1.0", + "@sigstore/tuf": "^3.1.0", + "@sigstore/verify": "^2.1.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.7", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.23", + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "12.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "7.5.11", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tinyglobby": { + "version": "0.2.15", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "3.0.1", + "debug": "^4.4.1", + "make-fetch-happen": "^14.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/tuf-js/node_modules/@tufjs/models": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "6.0.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/which": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/which/node_modules/isexe": { + "version": "3.1.5", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "5.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pandemonium": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/pandemonium/-/pandemonium-2.4.1.tgz", + "integrity": "sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==", + "license": "MIT", + "dependencies": { + "mnemonist": "^0.39.2" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-chatbot-kit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-chatbot-kit/-/react-chatbot-kit-2.2.2.tgz", + "integrity": "sha512-8p/i0KkzkhoyG2XsL6Pb6f72k9j7GYNAc5SOa4f9OZwbCD3Q34uEruNPc06qa1wZHKfT6aFna19PA2plFuO2NA==", + "license": "MIT", + "dependencies": { + "react-conditionally-render": "^1.0.2" + } + }, + "node_modules/react-composer": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz", + "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-conditionally-render": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/react-conditionally-render/-/react-conditionally-render-1.0.2.tgz", + "integrity": "sha512-CtjIgaLHVDSgHis3gv/PT/8EnD6GPUL8PrhUjh7DP6S5Y3p56dGu7y2nVg6pYv1kv+fGznRhRmX3assr/vRw3A==", + "license": "ISC" + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.72.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz", + "integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-i18next": { + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.3.tgz", + "integrity": "sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-icons": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-merge-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-1.1.0.tgz", + "integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/react-reconciler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", + "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.21.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-use-gesture": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-9.1.3.tgz", + "integrity": "sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==", + "deprecated": "This package is no longer maintained. Please use @use-gesture/react instead", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-use-websocket": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.13.0.tgz", + "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==", + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reagraph": { + "version": "4.15.19", + "resolved": "https://registry.npmjs.org/reagraph/-/reagraph-4.15.19.tgz", + "integrity": "sha512-acM2agUYyNKyKLzKhnEoMNbBc58KxpBQ5wzIqYvsoVa3Se2weuB8npVfdjJZV9AxW9BaSaeu90NwCrcO3XATTg==", + "license": "Apache-2.0", + "dependencies": { + "@react-spring/three": "9.6.1", + "@react-three/fiber": "8.13.5", + "camera-controls": "^2.8.3", + "classnames": "^2.5.1", + "d3-array": "^3.2.4", + "d3-force-3d": "^3.0.3", + "d3-hierarchy": "^3.1.2", + "d3-scale": "^4.0.2", + "ellipsize": "^0.5.1", + "glodrei": "^0.0.1", + "graphology": "^0.25.4", + "graphology-layout": "^0.6.1", + "graphology-layout-forceatlas2": "^0.10.1", + "graphology-layout-noverlap": "^0.4.2", + "graphology-metrics": "^2.1.0", + "graphology-shortest-path": "^2.0.2", + "hold-event": "^0.2.0", + "react-use-gesture": "^9.1.3", + "reakeys": "^2.0.0", + "three": "^0.154.0", + "three-stdlib": "^2.23.13", + "zustand": "4.3.9" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/reagraph/node_modules/@react-spring/animated": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz", + "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/reagraph/node_modules/@react-spring/core": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz", + "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/rafz": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/reagraph/node_modules/@react-spring/rafz": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz", + "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==", + "license": "MIT" + }, + "node_modules/reagraph/node_modules/@react-spring/shared": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz", + "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/reagraph/node_modules/@react-spring/three": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz", + "integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/core": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "three": ">=0.126" + } + }, + "node_modules/reagraph/node_modules/@react-spring/types": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz", + "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==", + "license": "MIT" + }, + "node_modules/reagraph/node_modules/@react-three/fiber": { + "version": "8.13.5", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.13.5.tgz", + "integrity": "sha512-x9QdsaB/Wm/6NGvRXQahPPWfn2dQce7Fg3C2r00NNzyDdqRKw32YavL+WEqjZOOd0nvFpzv7FtaKc+VCOTR59w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.26.7", + "its-fine": "^1.0.6", + "react-reconciler": "^0.27.0", + "react-use-measure": "^2.1.1", + "scheduler": "^0.21.0", + "suspend-react": "^0.1.3", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-gl": ">=11.0", + "react": ">=18.0", + "react-dom": ">=18.0", + "react-native": ">=0.64", + "three": ">=0.133" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/reagraph/node_modules/@react-three/fiber/node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "license": "MIT", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/reagraph/node_modules/camera-controls": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz", + "integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.126.1" + } + }, + "node_modules/reagraph/node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/reagraph/node_modules/three": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz", + "integrity": "sha512-Uzz8C/5GesJzv8i+Y2prEMYUwodwZySPcNhuJUdsVMH2Yn4Nm8qlbQe6qRN5fOhg55XB0WiLfTPBxVHxpE60ug==", + "license": "MIT" + }, + "node_modules/reagraph/node_modules/zustand": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.9.tgz", + "integrity": "sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/reakeys": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/reakeys/-/reakeys-2.0.6.tgz", + "integrity": "sha512-dmZPhOwU3NuLjy61CLqf3dGEhhetx4Du7m/DlX1eqZrBKcKrDqpR0O1tHyYMB95KVdhVRjrfcuFFawI7EqGyxQ==", + "license": "Apache-2.0", + "dependencies": { + "ctrl-keys": "^1.0.6" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/suspend-react": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.0.8.tgz", + "integrity": "sha512-ZC3r8Hu1y0dIThzsGw0RLZplnX9yXwfItcvaIzJc2VQVi8TGyGDlu92syMB5ulybfvGLHAI5Ghzlk23UBPF8xg==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/three": { + "version": "0.183.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", + "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", + "license": "MIT", + "peer": true + }, + "node_modules/three-mesh-bvh": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.5.24.tgz", + "integrity": "sha512-VTIgfjz8aFoPKTQoMIQQv9jJD4ybFRZuKKE1/kqy78FQcuHQ0+iIWv7C5cSb2inlvs7bNMVY3yRx3RXGZfrvzQ==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.123.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/troika-three-text": { + "version": "0.47.2", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.47.2.tgz", + "integrity": "sha512-qylT0F+U7xGs+/PEf3ujBdJMYWbn0Qci0kLqI5BJG2kW1wdg4T1XSxneypnF05DxFqJhEzuaOR9S2SjiyknMng==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.47.2", + "troika-worker-utils": "^0.47.2", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.47.2", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.47.2.tgz", + "integrity": "sha512-/28plhCxfKtH7MSxEGx8e3b/OXU5A0xlwl+Sbdp0H8FXUHKZDoksduEKmjQayXYtxAyuUiCRunYIv/8Vi7aiyg==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.47.2", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.47.2.tgz", + "integrity": "sha512-mzss4MeyzUkYBppn4x5cdAqrhBHFEuVmMMgLMTyFV23x6GvQMyo+/R5E5Lsbrt7WSt5RfvewjcwD1DChRTA9lA==", + "license": "MIT" + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "license": "MIT", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/graphrag-ui/package.json b/graphrag-ui/package.json index 4ef113e..c20d3dd 100755 --- a/graphrag-ui/package.json +++ b/graphrag-ui/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.0.5", "type": "module", + "packageManager": "pnpm@9.15.0", "scripts": { "dev": "vite", "build": "tsc && vite build", @@ -19,6 +20,9 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.1.0", + "@react-three/drei": "9.56.1", + "@react-three/fiber": "8.13.3", + "@tailwindcss/typography": "^0.5.18", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "i18next": "^23.11.5", @@ -35,12 +39,9 @@ "react-router-dom": "^6.23.1", "react-use-websocket": "^4.8.1", "reagraph": "4.15.19", - "@react-three/fiber": "8.13.3", - "@react-three/drei": "9.56.1", "remark-gfm": "^4.0.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", - "@tailwindcss/typography": "^0.5.18", "zod": "^3.23.8" }, "devDependencies": { @@ -57,5 +58,11 @@ "tailwindcss": "^3.4.18", "typescript": "^5.2.2", "vite": "^5.2.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@swc/core", + "esbuild" + ] } } diff --git a/graphrag-ui/src/actions/ActionProvider.tsx b/graphrag-ui/src/actions/ActionProvider.tsx index 58fc7aa..c73c182 100644 --- a/graphrag-ui/src/actions/ActionProvider.tsx +++ b/graphrag-ui/src/actions/ActionProvider.tsx @@ -1,4 +1,4 @@ -import React, {useState, useCallback, useEffect, useContext} from 'react'; +import React, {useState, useRef, useCallback, useEffect, useContext} from 'react'; import {createClientMessage} from 'react-chatbot-kit'; import useWebSocket, {ReadyState} from 'react-use-websocket'; import Loader from '../components/Loader'; @@ -81,6 +81,7 @@ const ActionProvider: React.FC = ({ }) => { const selectedGraph = useContext(SelectedGraphContext); const selectedRagPattern = useContext(RagPatternContext); + const lastUserQueryRef = useRef(""); const WS_URL = "/ui/" + selectedGraph + "/chat" + "?rag_pattern=" + selectedRagPattern; const [messageHistory, setMessageHistory] = useState[]>( [], @@ -160,12 +161,17 @@ const ActionProvider: React.FC = ({ }); loadedMessages.push(userMessage); } else if (msg.role === "system") { - // Create bot message + // Carry message_id + feedback through so history bubbles can + // open the trace page and reflect the prior thumbs-up/down + // state after a reload. const botMessage = createChatBotMessage({ content: msg.content || "", response_type: "history", query_sources: msg.query_sources, answered_question: msg.answered_question, + message_id: msg.message_id, + messageId: msg.message_id, + feedback: msg.feedback, }); loadedMessages.push(botMessage); } @@ -205,6 +211,7 @@ const ActionProvider: React.FC = ({ }; const defaultQuestions = (msg: string) => { + lastUserQueryRef.current = msg; const clientMessage = createClientMessage(msg, { delay: 300, }); @@ -213,6 +220,7 @@ const ActionProvider: React.FC = ({ }; const queryGraphragWs = (msg) => { + lastUserQueryRef.current = msg; const queryGraphragWsTest = (msg: string) => { sendMessage(msg); }; @@ -223,6 +231,13 @@ const ActionProvider: React.FC = ({ messages: [...prev.messages, loading], })); + // Signal that the chat is now waiting on an answer. Layout chrome + // (Setup / Logout / conversation list / new-chat button) listens for + // this and disables itself so the user can't unmount the in-flight + // streaming connection by navigating away. + document.body.classList.add("chat-streaming"); + window.dispatchEvent(new Event("chat:streaming-start")); + // Dispatch event to refresh conversation list when user sends a question // This ensures the side menu updates when a new message is sent window.dispatchEvent(new CustomEvent('conversationUpdated')); @@ -269,12 +284,22 @@ const ActionProvider: React.FC = ({ return; // Don't create a bot message for conversation ID } + // Attach the user query so the trace page can display it + messageData.userQuery = lastUserQueryRef.current; + // Handle regular bot messages const botMessage = createChatBotMessage(messageData); setState((prev) => { const newPrevMsg = prev.messages.slice(0, -1); - return {...prev, messages: [...newPrevMsg, botMessage]}; + return {...prev, messages: [...newPrevMsg, botMessage]}; }); + + // Final (non-progress) message ends the streaming gate; layout + // chrome re-enables. Progress messages keep the gate held. + if (messageData.response_type !== "progress") { + document.body.classList.remove("chat-streaming"); + window.dispatchEvent(new Event("chat:streaming-end")); + } } catch (error) { console.error("Error parsing WebSocket message:", error); // Handle string messages (progress updates) diff --git a/graphrag-ui/src/components/Bot.tsx b/graphrag-ui/src/components/Bot.tsx index 6386dec..b951de5 100644 --- a/graphrag-ui/src/components/Bot.tsx +++ b/graphrag-ui/src/components/Bot.tsx @@ -52,10 +52,11 @@ const Bot = ({ layout, getConversationId }: { layout?: string | undefined, getCo } } - // Set default ragPattern if no value in sessionStorage + // Set default ragPattern if no value in sessionStorage. "Auto" lets the + // backend RetrieverSelector pick a method per question. if (!sessionStorage.getItem("ragPattern")) { - setRagPattern("Hybrid Search"); - sessionStorage.setItem("ragPattern", "Hybrid Search"); + setRagPattern("Auto"); + sessionStorage.setItem("ragPattern", "Auto"); } const date = new Date(); @@ -70,9 +71,18 @@ const Bot = ({ layout, getConversationId }: { layout?: string | undefined, getCo window.addEventListener('focus', handleFocus); + // Stay in sync when another component (Refresh dialog, Ingest + // dialog, Customize Prompts) changes the shared selectedGraph. + const handleSelectedGraph = () => { + const next = sessionStorage.getItem("selectedGraph") || ''; + if (next !== selectedGraph) setSelectedGraph(next); + }; + window.addEventListener('graphrag:selectedGraph', handleSelectedGraph); + // Cleanup return () => { window.removeEventListener('focus', handleFocus); + window.removeEventListener('graphrag:selectedGraph', handleSelectedGraph); }; }, []); @@ -119,7 +129,7 @@ const Bot = ({ layout, getConversationId }: { layout?: string | undefined, getCo Select a GraphRAG Pattern - {["Similarity Search", "Contextual Search", "Hybrid Search", "Community Search"].map((f, i) => ( + {["Auto", "Similarity Search", "Contextual Search", "Hybrid Search", "Community Search"].map((f, i) => ( handleSelectRag(f)}> {/* */} {f} @@ -140,14 +150,14 @@ const Bot = ({ layout, getConversationId }: { layout?: string | undefined, getCo - + Select a KnowledgeGraph {store?.graphs?.length > 0 ? ( store.graphs.map((f, i) => ( handleSelect(f)}> - {f} + {f} )) ) : ( diff --git a/graphrag-ui/src/components/ConfigScopeToggle.tsx b/graphrag-ui/src/components/ConfigScopeToggle.tsx index b311631..4ef5c23 100644 --- a/graphrag-ui/src/components/ConfigScopeToggle.tsx +++ b/graphrag-ui/src/components/ConfigScopeToggle.tsx @@ -71,7 +71,7 @@ const ConfigScopeToggle: React.FC = ({ disabled={configScope !== "graph"} onValueChange={(value) => onGraphChange(value)} > - + diff --git a/graphrag-ui/src/components/CustomChatMessage.tsx b/graphrag-ui/src/components/CustomChatMessage.tsx index 0aef2ea..43937ef 100755 --- a/graphrag-ui/src/components/CustomChatMessage.tsx +++ b/graphrag-ui/src/components/CustomChatMessage.tsx @@ -13,6 +13,8 @@ import { IoIosCloseCircleOutline } from "react-icons/io"; import { Interactions } from "./Interact"; import { KnowledgeGraphPro } from "./graphs/KnowledgeGraphPro"; import { KnowledgeTablPro } from "./tables/KnowledgeTablePro"; +import { useAlert } from "@/hooks/useAlert"; +import TraceLogs from "@/pages/TraceLogs"; interface IChatbotMessageProps { message?: any; withAvatar?: boolean; @@ -28,6 +30,48 @@ interface IChatbotMessageProps { } const urlRegex = /https?:\/\// + +// Phase 1.5 — render a subtle chip showing which retrieval method ran. +// Reads the auto-selection metadata that supportai_search mirrors into +// query_sources (chosen_retriever / chosen_retriever_reason / chosen_retriever_source). +const METHOD_LABELS: Record = { + similaritysearch: "Similarity", + contextualsearch: "Contextual", + hybridsearch: "Hybrid", + communitysearch: "Community", +}; + +const RetrieverBadge: FC<{ message: any }> = ({ message }) => { + const qs = message?.query_sources; + if (!qs || typeof qs !== "object") return null; + const method = qs.chosen_retriever as string | undefined; + if (!method) return null; + // Suppress for greetings / errors / progress events — those don't run a retriever. + if ( + message.response_type === "progress" || + message.response_type === "greeting" || + message.response_type === "error" + ) { + return null; + } + const label = METHOD_LABELS[method] || method; + const reason = (qs.chosen_retriever_reason as string | undefined) || ""; + const source = (qs.chosen_retriever_source as string | undefined) || ""; + const sourceLabel = source === "manual" ? "manual" : "auto"; + // Reason + source live in the hover tooltip so the inline chip stays + // glanceable; users who want the detail can hover. + const tooltip = reason ? `${sourceLabel}: ${reason}` : sourceLabel; + return ( +
+ 🔎 + {label} +
+ ); +}; + const getReasoning = (msg) => { if(msg.query_sources.reasoning instanceof Array) { @@ -130,6 +174,8 @@ export const CustomChatMessage: FC = ({ const [showResult, setShowResult] = useState(false); const [showGraphVis, setShowGraphVis] = useState(false); const [showTableVis, setShowTableVis] = useState(false); + const [traceMessageId, setTraceMessageId] = useState(null); + const [alert, alertDialog] = useAlert(); // Error handling functions const handleShowExplain = () => { @@ -149,7 +195,11 @@ export const CustomChatMessage: FC = ({ }; const handleShowTable = () => { - if (message.response_type == 'history' || !message.query_sources?.result) { + // Allow opening the table view on history messages too — the + // chat-history backend preserves ``query_sources.result``, so + // there's no reason to deny it just because the message arrived + // from a reload rather than a fresh answer. + if (!message.query_sources?.result) { return false; } setShowTableVis(prev => !prev); @@ -171,6 +221,13 @@ export const CustomChatMessage: FC = ({ return ( <> + {alertDialog} + {traceMessageId && ( + setTraceMessageId(null)} + /> + )} {typeof message === "string" ? (
{message} @@ -185,11 +242,46 @@ export const CustomChatMessage: FC = ({ ) : ( {message.content} )} + { + const messageId = message.messageId || message.message_id || ""; + if (!messageId) { + await alert("Trace log unavailable: this message has no trace ID."); + return; + } + // Guard against a missing/invalid creds value. If we send + // ``Basic null`` (or other unparsable base64), FastAPI's + // HTTPBasic returns 401 + ``WWW-Authenticate: Basic`` and + // the browser pops up its native auth dialog. Better to + // tell the user to sign in again than to flash that popup. + const creds = sessionStorage.getItem("creds"); + if (!creds) { + await alert("Your session has expired. Please log in again."); + return; + } + // Trace JSON lives under /code/trace_logs inside the + // graphrag container and is wiped on container recreate. + // Probe first so we never open an empty dialog when the file is gone. + try { + const probe = await fetch(`/ui/trace/${messageId}`, { + method: "GET", + headers: { Authorization: `Basic ${creds}` }, + }); + if (!probe.ok) { + await alert("Trace log not found."); + return; + } + } catch (err) { + await alert("Failed to reach the trace log endpoint. Please try again."); + return; + } + setTraceMessageId(messageId); + }} />
diff --git a/graphrag-ui/src/components/Interact.tsx b/graphrag-ui/src/components/Interact.tsx index e1426f0..ae93539 100644 --- a/graphrag-ui/src/components/Interact.tsx +++ b/graphrag-ui/src/components/Interact.tsx @@ -10,7 +10,8 @@ import { PiArrowsCounterClockwiseFill } from "react-icons/pi"; import { Feedback, Message } from "@/actions/ActionProvider"; import { PiGraph } from "react-icons/pi"; import { FaTable } from "react-icons/fa"; -import { LuInfo } from "react-icons/lu"; +import { LuInfo, LuActivity } from "react-icons/lu"; +import { useRoles } from "@/hooks/useRoles"; const GRAPHRAG_URL = ""; interface Interactions { @@ -18,15 +19,24 @@ interface Interactions { showExplain: () => boolean; showTable: () => boolean; showGraph: () => boolean; + onViewTrace?: () => void; } -export const Interactions: FC = ({ +export const Interactions: FC = ({ message, showExplain, showTable, showGraph, + onViewTrace, }: Interactions) => { - const [feedback, setFeedback] = useState(Feedback.NoFeedback); + // Seed from the persisted feedback when re-rendering a history + // message so the up/down state matches what the user already + // submitted before the page reloaded. + const [feedback, setFeedback] = useState( + (message?.feedback as Feedback) ?? Feedback.NoFeedback + ); + const { isSuperuser, isGlobalDesigner, isGraphAdmin } = useRoles(); + const canViewTrace = isSuperuser || isGlobalDesigner || isGraphAdmin; const sendFeedback = async (action: Feedback, message: Message) => { const creds = sessionStorage.getItem("creds"); @@ -42,9 +52,30 @@ export const Interactions: FC = ({ }); }; + // Suppress the toolbar for non-answer message types where the + // buttons would be meaningless (progress chips, greeting cards, + // hard errors). + const responseType = message?.response_type; + if (responseType === "progress" || responseType === "greeting" || responseType === "error") { + return null; + } + // Hide the row entirely for the welcome / loading placeholder + // bubble that has neither a real answer nor an answered question. + if (!message?.content && !message?.answered_question) { + return null; + } + + const hasGraphData = Boolean(message?.query_sources?.result?.edges); + const hasTableData = Boolean(message?.query_sources?.result); + // The trace page is keyed by message_id. Some history payloads pre-date + // the message_id capture, so suppress the button when we can't build a + // valid /trace/ URL — otherwise the click opens a blank tab. + const traceMessageId = message?.messageId || message?.message_id || ""; + const hasTraceId = Boolean(traceMessageId); + return (
- {(message.query_sources?.result || message.query_sources?.cypher || message.query_sources?.answer) ? ( + {true ? ( <>
= ({
*/} -
showExplain()} - > - - Explain -
+ {canViewTrace && hasTraceId ? ( +
onViewTrace?.()} + > + + View Trace +
+ ) : ( +
showExplain()} + > + + Explain +
+ )}
{ - if (message.query_sources?.result?.edges) { + if (hasGraphData) { showGraph(); } }} @@ -113,10 +155,11 @@ export const Interactions: FC = ({
{ - if (message.query_sources?.result) { + if (hasTableData) { showTable(); } }} diff --git a/graphrag-ui/src/components/ModeToggle.tsx b/graphrag-ui/src/components/ModeToggle.tsx index 053ac9f..73f072b 100644 --- a/graphrag-ui/src/components/ModeToggle.tsx +++ b/graphrag-ui/src/components/ModeToggle.tsx @@ -1,4 +1,5 @@ import { Moon, Sun, LogOut, Settings } from "lucide-react"; +import { useState, useEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; @@ -19,10 +20,31 @@ export function ModeToggle() { const isLoginRoute = location.pathname === "/"; const [confirm, confirmDialog] = useConfirm(); const { rolesLoaded, canAccessSetup } = useRoles(location.pathname); + // Disable Settings / Logout while a chat answer is streaming so the + // user can't accidentally unmount Chat and lose the in-flight reply. + const [chatStreaming, setChatStreaming] = useState(false); + useEffect(() => { + const onStart = () => setChatStreaming(true); + const onEnd = () => setChatStreaming(false); + window.addEventListener("chat:streaming-start", onStart); + window.addEventListener("chat:streaming-end", onEnd); + return () => { + window.removeEventListener("chat:streaming-start", onStart); + window.removeEventListener("chat:streaming-end", onEnd); + }; + }, []); + const streamingTitle = chatStreaming + ? "Disabled while the chat is generating an answer" + : undefined; const handleLogout = async () => { - // Show confirmation dialog - const shouldLogout = await confirm("Are you sure you want to logout? This will clear all your chat history."); + // Show confirmation dialog; flag pending-chat loss when applicable + // so the user isn't surprised by a generic error-answer afterwards. + // Chat history itself is server-side and survives logout. + const message = chatStreaming + ? "An answer is still being generated. Logging out will drop the connection, and the in-flight answer will be lost (saved as an error in your history). Continue?" + : "Log out of GraphRAG? You'll need to sign in again to continue."; + const shouldLogout = await confirm(message); if (!shouldLogout) { return; } @@ -47,21 +69,22 @@ export function ModeToggle() { }; return ( -
+
{!isLoginRoute && rolesLoaded && canAccessSetup && ( - )} - + {!isLoginRoute && ( - +
+
+
, + document.body + ); +} diff --git a/graphrag-ui/src/components/ui/tag-input.tsx b/graphrag-ui/src/components/ui/tag-input.tsx new file mode 100644 index 0000000..80311ee --- /dev/null +++ b/graphrag-ui/src/components/ui/tag-input.tsx @@ -0,0 +1,186 @@ +import React, { useState, KeyboardEvent } from "react"; + +export interface TypeHint { + name: string; + description: string; + // Edge variants only: ``Name (From -> To)`` captures the endpoint + // pair the user has in mind. Vertex chips leave these undefined. + fromType?: string; + toType?: string; +} + +interface TagInputProps { + values: TypeHint[]; + onChange: (values: TypeHint[]) => void; + placeholder?: string; + disabled?: boolean; + ariaLabel?: string; + // When true, the parser accepts ``Name (From -> To)`` (and the + // unicode ``→``) before the optional ``: description``. Used on the + // Suggested Edge Types row only — vertex chips never carry + // endpoints. + acceptsEndpoints?: boolean; + // Map of lowercased names to a human-readable reason for rejection. + // Lets the parent reject GSQL reserved words, GraphRAG structural + // types, etc., with a clear message instead of a silent drop later. + forbiddenNames?: Record; +} + +// Vertex format: ``Name`` or ``Name: description``. +const VERTEX_RE = /^([A-Za-z_][A-Za-z0-9_]*)\s*(?::\s*(.*))?\s*$/; +// Edge format: ``Name``, ``Name (From -> To)``, ``Name: description``, +// or ``Name (From -> To): description``. ``->`` and unicode ``→`` both +// accepted as the arrow. +const EDGE_RE = + /^([A-Za-z_][A-Za-z0-9_]*)\s*(?:\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:->|→)\s*([A-Za-z_][A-Za-z0-9_]*)\s*\))?\s*(?::\s*(.*))?\s*$/; + +const MAX_DESC_DISPLAY = 32; + +const formatChip = (hint: TypeHint): string => { + let label = hint.name; + if (hint.fromType && hint.toType) { + label += ` (${hint.fromType} → ${hint.toType})`; + } + if (!hint.description) return label; + const desc = + hint.description.length > MAX_DESC_DISPLAY + ? hint.description.slice(0, MAX_DESC_DISPLAY - 1) + "…" + : hint.description; + return `${label}: ${desc}`; +}; + +export const TagInput: React.FC = ({ + values, + onChange, + placeholder, + disabled, + ariaLabel, + acceptsEndpoints = false, + forbiddenNames, +}) => { + const [draft, setDraft] = useState(""); + const [error, setError] = useState(null); + + const reasonFor = (name: string): string | null => { + if (!forbiddenNames) return null; + return forbiddenNames[name.toLowerCase()] || null; + }; + + const commit = () => { + const text = draft.trim(); + if (!text) return; + const re = acceptsEndpoints ? EDGE_RE : VERTEX_RE; + const m = re.exec(text); + if (!m) { + setError( + acceptsEndpoints + ? `"${text}" is not valid. Use \`Name\`, \`Name: description\`, \`Name (From -> To)\`, or \`Name (From -> To): description\`.` + : `"${text}" is not valid. Use \`Name\` or \`Name: description\` (names must start with a letter or underscore).` + ); + return; + } + const name = m[1]; + const fromType = acceptsEndpoints ? (m[2] || "").trim() : ""; + const toType = acceptsEndpoints ? (m[3] || "").trim() : ""; + const descriptionIdx = acceptsEndpoints ? 4 : 2; + const description = (m[descriptionIdx] || "").trim(); + + // Reject reserved/structural names — for every name reference + // (the type name itself + the optional endpoint vertex types). + for (const candidate of [name, fromType, toType].filter(Boolean)) { + const reason = reasonFor(candidate); + if (reason) { + setError(`"${candidate}" cannot be used: ${reason}`); + return; + } + } + + if (values.some((v) => v.name.toLowerCase() === name.toLowerCase())) { + setError(`"${name}" is already in the list.`); + return; + } + + onChange([ + ...values, + { + name, + description, + ...(fromType ? { fromType } : {}), + ...(toType ? { toType } : {}), + }, + ]); + setDraft(""); + setError(null); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === ",") { + e.preventDefault(); + commit(); + } else if (e.key === "Backspace" && draft === "" && values.length > 0) { + // Remove last chip on backspace from empty input. + onChange(values.slice(0, -1)); + } + }; + + const remove = (idx: number) => { + onChange(values.filter((_, i) => i !== idx)); + }; + + return ( +
+
+ {values.map((v, i) => ( + + {formatChip(v)} + {!disabled && ( + + )} + + ))} + { + setDraft(e.target.value); + if (error) setError(null); + }} + onKeyDown={handleKeyDown} + onBlur={commit} + placeholder={values.length === 0 ? placeholder : ""} + disabled={disabled} + className="flex-1 min-w-[120px] bg-transparent outline-none text-sm py-0.5" + /> +
+ {error && ( +

{error}

+ )} +
+ ); +}; + +export default TagInput; diff --git a/graphrag-ui/src/hooks/useAlert.tsx b/graphrag-ui/src/hooks/useAlert.tsx new file mode 100644 index 0000000..cfc8db4 --- /dev/null +++ b/graphrag-ui/src/hooks/useAlert.tsx @@ -0,0 +1,34 @@ +import { useState, ReactElement } from "react"; +import { AlertDialog } from "@/components/ui/alert-dialog"; + +interface AlertOptions { + message: string; + onClose: () => void; +} + +export function useAlert(): [ + (message: string) => Promise, + ReactElement | null, + boolean +] { + const [options, setOptions] = useState(null); + + const alert = (message: string): Promise => + new Promise((resolve) => { + setOptions({ + message, + onClose: () => { + resolve(); + setOptions(null); + }, + }); + }); + + const alertDialog: ReactElement | null = options ? ( + + ) : null; + + const isOpen = options !== null; + + return [alert, alertDialog, isOpen]; +} diff --git a/graphrag-ui/src/hooks/useIdleTimeout.ts b/graphrag-ui/src/hooks/useIdleTimeout.ts index 07f0486..0b54892 100644 --- a/graphrag-ui/src/hooks/useIdleTimeout.ts +++ b/graphrag-ui/src/hooks/useIdleTimeout.ts @@ -44,16 +44,19 @@ export function useIdleTimeout(timeoutMs: number = DEFAULT_TIMEOUT_MS) { const onPause = () => pause(); const onResume = () => resetTimer(); + const onPing = () => resetTimer(); events.forEach((event) => window.addEventListener(event, resetTimer)); window.addEventListener("idle-timer-pause", onPause); window.addEventListener("idle-timer-resume", onResume); + window.addEventListener("idle-timer-ping", onPing); resetTimer(); // Start the timer return () => { events.forEach((event) => window.removeEventListener(event, resetTimer)); window.removeEventListener("idle-timer-pause", onPause); window.removeEventListener("idle-timer-resume", onResume); + window.removeEventListener("idle-timer-ping", onPing); if (timerRef.current) { clearTimeout(timerRef.current); } @@ -70,3 +73,13 @@ export function pauseIdleTimer() { export function resumeIdleTimer() { window.dispatchEvent(new Event("idle-timer-resume")); } + +/** + * Reset the idle timer without requiring a user UI event. Call this + * after a successful status poll for a long-running, user-initiated + * backend flow (init, ingest, rebuild) so the session stays alive + * while the user is watching progress. + */ +export function pingIdleTimer() { + window.dispatchEvent(new Event("idle-timer-ping")); +} diff --git a/graphrag-ui/src/index.css b/graphrag-ui/src/index.css index d2dc878..1be79de 100755 --- a/graphrag-ui/src/index.css +++ b/graphrag-ui/src/index.css @@ -88,6 +88,14 @@ .react-chatbot-kit-chat-input-container { @apply !bg-background !border-[#3D3D3D]; } + /* Block submitting another question while the previous answer is + still streaming. ActionProvider toggles ``chat-streaming`` on + ``document.body`` at stream start / end. */ + body.chat-streaming .react-chatbot-kit-chat-input-container, + body.chat-streaming .react-chatbot-kit-chat-input-form { + pointer-events: none; + opacity: 0.5; + } .open-dg { @apply bg-background; } @@ -185,7 +193,11 @@ .fp .react-chatbot-kit-chat-message-container, .open-dg .react-chatbot-kit-chat-message-container { height: calc(100vh - 100px) !important; - max-width: 960px; + /* Scale with the available width so wider screens use more + horizontal space, but cap reading-line at a comfortable + length. Switch from a hard 960px cap that left huge gutters + on 1440p / 4K monitors. */ + max-width: min(1280px, 90%); margin: 0 auto; } diff --git a/graphrag-ui/src/main.tsx b/graphrag-ui/src/main.tsx index 70a14d3..1788c05 100755 --- a/graphrag-ui/src/main.tsx +++ b/graphrag-ui/src/main.tsx @@ -4,6 +4,7 @@ import "./index.css"; import { Outlet, RouterProvider, createBrowserRouter, Navigate } from "react-router-dom"; import Chat from "./pages/Chat"; import ChatDialog from "./pages/ChatDialog.tsx"; +import TraceLogs from "./pages/TraceLogs.tsx"; import SetupLayout from "./pages/setup/SetupLayout.tsx"; import KGAdmin from "./pages/setup/KGAdmin.tsx"; import IngestGraph from "./pages/setup/IngestGraph.tsx"; @@ -25,6 +26,7 @@ const RequireAuth = ({ children }: { children: any }) => { return children; }; + const Layout = () => { useIdleTimeout(); return ( @@ -56,6 +58,10 @@ const router = createBrowserRouter([ path: "/preferences", element: , }, + { + path: "/trace/:messageId", + element: , + }, { path: "/setup", element: , diff --git a/graphrag-ui/src/pages/Chat.tsx b/graphrag-ui/src/pages/Chat.tsx index e2032eb..a34e237 100644 --- a/graphrag-ui/src/pages/Chat.tsx +++ b/graphrag-ui/src/pages/Chat.tsx @@ -1,16 +1,115 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import Bot from "@/components/Bot"; import SideMenu from "@/components/SideMenu"; -import { RxHamburgerMenu } from "react-icons/rx"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +// Sidebar width is user-resizable via the vertical separator. The +// chosen width is persisted to localStorage so it survives reloads. +const SIDEBAR_STORAGE_KEY = "graphrag:sidebarWidth"; +const DEFAULT_SIDEBAR_WIDTH = 320; +const MIN_SIDEBAR_WIDTH = 220; +const MAX_SIDEBAR_WIDTH = 600; + +const readStoredWidth = (): number => { + try { + const raw = parseInt(localStorage.getItem(SIDEBAR_STORAGE_KEY) || ""); + if (!isNaN(raw) && raw >= MIN_SIDEBAR_WIDTH && raw <= MAX_SIDEBAR_WIDTH) { + return raw; + } + } catch { + // localStorage may be unavailable (private mode); fall through. + } + return DEFAULT_SIDEBAR_WIDTH; +}; const Chat = () => { const [showSidebar, setShowSidebar] = useState(true); + const [sidebarWidth, setSidebarWidth] = useState(readStoredWidth); + const [isDragging, setIsDragging] = useState(false); const [getConversationId, setGetConversationId] = useState(['lkjh']); + + // Drag-to-resize. Track mouse globally while the user holds the + // separator so the resize keeps working even if the cursor strays + // outside the thin handle strip. + useEffect(() => { + if (!isDragging) return; + const onMouseMove = (e: MouseEvent) => { + const clamped = Math.max( + MIN_SIDEBAR_WIDTH, + Math.min(MAX_SIDEBAR_WIDTH, e.clientX) + ); + setSidebarWidth(clamped); + }; + const onMouseUp = () => { + setIsDragging(false); + try { + localStorage.setItem(SIDEBAR_STORAGE_KEY, String(sidebarWidth)); + } catch { + // ignore — width simply won't persist + } + }; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + // Prevent text selection during drag. + document.body.style.userSelect = "none"; + document.body.style.cursor = "col-resize"; + return () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + document.body.style.userSelect = ""; + document.body.style.cursor = ""; + }; + }, [isDragging, sidebarWidth]); + return ( <> + {/* No `relative` on this container — adding one would create a + stacking context that hides the top-right ModeToggle (Setup / + Logout / theme) underneath the Bot header. The chevron below + positions itself against the viewport instead, which is fine + because this container starts flush at the viewport edge. */}
- {showSidebar ? : null} -
setShowSidebar(prev => !prev)}>
+ {showSidebar ? ( + + ) : null} + {/* Drag handle: thin vertical strip on the sidebar's right edge. + Hovering shows a subtle highlight; mousedown enters resize + mode. Sits behind the chevron so the chevron click still + registers as toggle. */} + {showSidebar && ( +
{ + e.preventDefault(); + setIsDragging(true); + }} + aria-label="Resize left menu" + role="separator" + className={ + "hidden md:block fixed top-0 bottom-0 z-10 w-1.5 cursor-col-resize " + + "hover:bg-blue-500/30 dark:hover:bg-blue-400/30 transition-colors" + } + style={{ left: `${sidebarWidth - 3}px` }} + /> + )} +
diff --git a/graphrag-ui/src/pages/Setup.tsx b/graphrag-ui/src/pages/Setup.tsx index d5674ac..f1a8d79 100644 --- a/graphrag-ui/src/pages/Setup.tsx +++ b/graphrag-ui/src/pages/Setup.tsx @@ -20,6 +20,7 @@ import { SelectValue, } from "@/components/ui/select"; import { useConfirm } from "@/hooks/useConfirm"; +import { safeJson } from "@/utils/safeJson"; const DEFAULT_MAX_UPLOAD_SIZE_MB = 100; const envUploadLimit = Number(import.meta.env.VITE_MAX_UPLOAD_SIZE_MB); @@ -106,7 +107,7 @@ const [activeTab, setActiveTab] = useState("upload"); const response = await fetch(`/ui/${ingestGraphName}/uploads/list`, { headers: { Authorization: `Basic ${creds}` }, }); - const data = await response.json(); + const data = await safeJson(response); setUploadedFiles(data.files || []); } catch (error) { console.error("Error fetching files:", error); @@ -162,11 +163,11 @@ const [activeTab, setActiveTab] = useState("upload"); }); if (!response.ok) { - const errorData = await response.json(); + const errorData = await safeJson(response); throw new Error(errorData.detail || `Upload failed: ${response.statusText}`); } - const data = await response.json(); + const data = await safeJson(response); if (data.status === "success") { const uploadedCount = selectedFiles?.length || 0; setUploadMessage("✅ Successfully uploaded the files. Processing..."); @@ -223,11 +224,11 @@ const [activeTab, setActiveTab] = useState("upload"); }); if (!response.ok) { - const errorData = await response.json(); + const errorData = await safeJson(response); throw new Error(errorData.detail || `Upload failed with status ${response.status}`); } - const data = await response.json(); + const data = await safeJson(response); if (data.status === "success") { uploadedCount++; } else { @@ -1000,20 +1001,45 @@ const [activeTab, setActiveTab] = useState("upload"); } }, [refreshOpen, refreshGraphName]); - // Load available graphs from sessionStorage on mount + // Load available graphs. Seed from sessionStorage, then refresh from + // /ui/list_graphs so a graph created mid-session is visible without + // re-login (the post-init success path that updates sessionStorage + // can be skipped if the init fetch times out client-side even though + // the backend completed). useEffect(() => { const store = JSON.parse(sessionStorage.getItem("site") || "{}"); if (store.graphs && Array.isArray(store.graphs)) { setAvailableGraphs(store.graphs); - // Auto-select first graph if available if (store.graphs.length > 0 && !ingestGraphName) { setIngestGraphName(store.graphs[0]); } - // Auto-select first graph for refresh as well if (store.graphs.length > 0 && !refreshGraphName) { setRefreshGraphName(store.graphs[0]); } } + const creds = sessionStorage.getItem("creds"); + if (!creds) return; + fetch("/ui/list_graphs", { + headers: { Authorization: `Basic ${creds}` }, + }) + .then((r) => (r.ok ? r.json() : null)) + .then((data) => { + if (!data || !Array.isArray(data.graphs)) return; + const graphs: string[] = data.graphs; + setAvailableGraphs(graphs); + const cached = JSON.parse(sessionStorage.getItem("site") || "{}"); + cached.graphs = graphs; + sessionStorage.setItem("site", JSON.stringify(cached)); + if (graphs.length > 0 && !ingestGraphName) { + setIngestGraphName(graphs[0]); + } + if (graphs.length > 0 && !refreshGraphName) { + setRefreshGraphName(graphs[0]); + } + }) + .catch(() => { + /* keep cached value; not fatal */ + }); }, []); // Load files when ingest dialog opens or graph name changes diff --git a/graphrag-ui/src/pages/TraceLogs.tsx b/graphrag-ui/src/pages/TraceLogs.tsx new file mode 100644 index 0000000..d5f0a0e --- /dev/null +++ b/graphrag-ui/src/pages/TraceLogs.tsx @@ -0,0 +1,1018 @@ +import { FC, useState, useMemo, useEffect } from "react"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { + LuArrowLeft, + LuChevronDown, + LuChevronUp, + LuCopy, + LuDownload, + LuWrench, + LuBookOpen, + LuActivity, + LuCoins, + LuInfo, +} from "react-icons/lu"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface TraceLogEntry { + id: number; + type: "tool_call" | "citation"; + timestamp: string; + label: string; + detail?: string; + durationMs?: number; + content?: string; + step?: number; +} + +interface TokenUsage { + input_tokens: number; + output_tokens: number; + total_tokens: number; + cost: number; +} + +interface LlmCall extends TokenUsage { + caller_name: string; +} + +interface ToolCallEntry { + id: number; + name: string; + timestamp: string; + durationMs: number; + input?: string; + output?: string; + usage?: TokenUsage & { calls?: LlmCall[] }; +} + +interface CitationEntry { + id: number; + source: string; + cited: boolean; + text: string; +} + +interface TimelineStep { + step: number; + name: string; + durationMs: number; +} + +interface TraceData { + originalQuery: string; + conversationContext: string[]; + status: "completed" | "in_progress" | "failed"; + sessionId: string; + timing: { + totalDuration: number; + toolExecution: number; + llmThinking: number; + startTime: string; + endTime: string; + }; + logs: TraceLogEntry[]; + toolCalls: ToolCallEntry[]; + citations: CitationEntry[]; + timeline: TimelineStep[]; + tokenUsage: TokenUsage; + finalResponse: string; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function formatDuration(seconds: number): string { + if (seconds < 0.01) return `${Math.round(seconds * 1000)}ms`; + return `${seconds.toFixed(2)}s`; +} + +function safeJson(obj: any): string { + if (obj == null) return "N/A"; + if (typeof obj === "string") { + try { + return JSON.stringify(JSON.parse(obj), null, 2); + } catch { + return obj; + } + } + try { + return JSON.stringify(obj, null, 2); + } catch { + return String(obj); + } +} + +const NODE_LABELS: Record = { + entry: "Entry", + supportai: "SupportAI", + map_question_to_schema: "Map Question to Schema", + generate_function: "Generate Function", + generate_cypher: "Generate Cypher", + generate_answer: "Generate Answer", + lookup_history: "Lookup History", + merge_history_context: "Merge History Context", + rewrite_question: "Rewrite Question", + apologize: "Apologize", + greet: "Greet", +}; + +function buildTraceFromMessage(message: any, userQuery?: string): TraceData { + const now = new Date(); + const sessionTs = now.toISOString().replace(/[-:T]/g, "").slice(0, 15); + const sessionId = `chat_${sessionTs}`; + + const query = userQuery || message?.originalQuery || message?.query || "N/A"; + const qs = message?.query_sources || {}; + const totalResponseTime = message?.response_time || 0; + const ts = now.toLocaleTimeString(); + + // ── Tool Calls ────────────────────────────────────────────────────────── + const toolCalls: ToolCallEntry[] = []; + const agentSteps: { + node: string; + duration_s: number; + input?: string; + output?: string; + usage?: TokenUsage & { calls?: LlmCall[] }; + }[] = qs.agent_steps || []; + + if (agentSteps.length > 0) { + agentSteps.forEach((step, i: number) => { + toolCalls.push({ + id: i + 1, + name: NODE_LABELS[step.node] || step.node, + timestamp: ts, + durationMs: Math.round(step.duration_s * 1000), + input: safeJson(step.input), + output: safeJson(step.output), + usage: step.usage, + }); + }); + } + + // ── Citations ─────────────────────────────────────────────────────────── + const rawReasoning = qs.reasoning; + const citations: CitationEntry[] = []; + + if (rawReasoning && Array.isArray(rawReasoning)) { + rawReasoning.forEach((src: any, i: number) => { + if (src == null) return; + const raw = typeof src === "string" ? src : String(src); + const cited = raw.startsWith("* "); + const chunkName = raw.replace(/^\*\s*/, ""); + + citations.push({ + id: i + 1, + source: chunkName, + cited, + text: "", + }); + }); + } + + // ── Logs ──────────────────────────────────────────────────────────────── + const logs: TraceLogEntry[] = []; + let logId = 0; + toolCalls.forEach((tc) => { + logs.push({ + id: logId++, + type: "tool_call", + timestamp: tc.timestamp, + label: `${tc.name} — Input`, + content: tc.input, + durationMs: tc.durationMs, + }); + logs.push({ + id: logId++, + type: "citation", + timestamp: tc.timestamp, + label: `${tc.name} — Output`, + content: tc.output, + }); + }); + + // ── Timeline ──────────────────────────────────────────────────────────── + const timeline: TimelineStep[] = toolCalls.map((tc, i) => ({ + step: i + 1, + name: tc.name, + durationMs: tc.durationMs, + })); + + const totalToolSec = agentSteps.reduce( + (sum: number, s: { duration_s: number }) => sum + s.duration_s, + 0 + ); + const llmThinking = Math.max(0, totalResponseTime - totalToolSec); + const endTime = new Date(now.getTime() + totalResponseTime * 1000); + + // ── Token usage totals ───────────────────────────────────────────────── + const serverTotal = qs.token_usage as TokenUsage | undefined; + const tokenUsage: TokenUsage = serverTotal || agentSteps.reduce( + (acc, s) => { + const u = s.usage; + if (!u) return acc; + return { + input_tokens: acc.input_tokens + (u.input_tokens || 0), + output_tokens: acc.output_tokens + (u.output_tokens || 0), + total_tokens: acc.total_tokens + (u.total_tokens || 0), + cost: acc.cost + (u.cost || 0), + }; + }, + { input_tokens: 0, output_tokens: 0, total_tokens: 0, cost: 0 } as TokenUsage + ); + + return { + originalQuery: query, + conversationContext: [`user: ${query}`], + status: "completed", + sessionId, + timing: { + totalDuration: totalResponseTime, + toolExecution: totalToolSec, + llmThinking, + startTime: now.toLocaleTimeString(), + endTime: endTime.toLocaleTimeString(), + }, + logs, + toolCalls, + citations, + timeline, + tokenUsage, + finalResponse: message?.content || "", + }; +} + +function formatCost(cost: number): string { + if (!cost) return "$0.00"; + if (cost < 0.01) return `$${cost.toFixed(6)}`; + return `$${cost.toFixed(4)}`; +} + +function formatNumber(n: number): string { + return (n || 0).toLocaleString(); +} + +function formatCallerNames(calls: { caller_name: string }[]): string { + if (!calls || calls.length === 0) return "—"; + const counts: Record = {}; + calls.forEach((c) => { + counts[c.caller_name] = (counts[c.caller_name] || 0) + 1; + }); + return Object.entries(counts) + .map(([name, count]) => (count > 1 ? `${name} ×${count}` : name)) + .join(", "); +} + +// ─── Sub-components ─────────────────────────────────────────────────────────── + +const StatusBadge: FC<{ status: string }> = ({ status }) => { + const color = + status === "completed" + ? "bg-emerald-500" + : status === "in_progress" + ? "bg-blue-500" + : "bg-red-500"; + return ( + + {status} + + ); +}; + +const TimingRow: FC<{ + items: { value: string; label: string; color: string }[]; +}> = ({ items }) => ( +
+ {items.map((item, i) => ( +
+ + {item.value} + + + {item.label} + +
+ ))} +
+); + +const ExpandableRow: FC<{ + children: React.ReactNode; + content?: string; + defaultOpen?: boolean; +}> = ({ children, content, defaultOpen = false }) => { + const [open, setOpen] = useState(defaultOpen); + return ( +
+
setOpen((p) => !p)} + > +
+ {children} +
+
+ {content && ( + + )} + {open ? ( + + ) : ( + + )} +
+
+ {open && content && ( +
+
{content}
+
+ )} +
+ ); +}; + +// ─── Tab Panels ─────────────────────────────────────────────────────────────── + +const LogsPanel: FC<{ trace: TraceData }> = ({ trace }) => { + const [collapsed, setCollapsed] = useState(false); + + return ( +
+
+
+ + {trace.logs.length} agent steps + + + Nodes ({trace.toolCalls.length}) + + + Citations ({trace.citations.length}) + +
+ +
+ +
+ {trace.logs.map((log) => ( +
+
+
+
+
+
+ + + + Node + + + {log.timestamp} + + + {log.label} + + {log.durationMs != null && log.durationMs > 0 && ( + + ({formatDuration(log.durationMs / 1000)}) + + )} + +
+
+ ))} +
+
+ ); +}; + +const ToolCallExpandable: FC<{ tc: ToolCallEntry }> = ({ tc }) => { + const [open, setOpen] = useState(false); + return ( +
+
setOpen((p) => !p)} + > +
+ + {tc.id} + + {tc.name} + {tc.timestamp} +
+
+ {tc.usage && tc.usage.total_tokens > 0 && ( + + {formatNumber(tc.usage.total_tokens)} tokens + + )} + {tc.durationMs > 0 && ( + + {formatDuration(tc.durationMs / 1000)} + + )} + {open ? ( + + ) : ( + + )} +
+
+ {open && ( +
+ {tc.usage && tc.usage.total_tokens > 0 && ( +
+

+ LLM Usage +

+
+
+
Input
+
{formatNumber(tc.usage.input_tokens)}
+
+
+
Output
+
{formatNumber(tc.usage.output_tokens)}
+
+
+
Total
+
{formatNumber(tc.usage.total_tokens)}
+
+
+
Cost
+
{formatCost(tc.usage.cost)}
+
+
+ {tc.usage.calls && tc.usage.calls.length > 0 && ( +
+ {tc.usage.calls.length} LLM call{tc.usage.calls.length !== 1 ? "s" : ""}:{" "} + {formatCallerNames(tc.usage.calls)} +
+ )} +
+ )} +
+

+ Input +

+
+              {tc.input || "N/A"}
+            
+
+
+

+ Output +

+
+              {tc.output || "N/A"}
+            
+
+
+ )} +
+ ); +}; + +const ToolCallsPanel: FC<{ trace: TraceData }> = ({ trace }) => ( +
+ {trace.toolCalls.map((tc) => ( + + ))} +
+); + + +const CitationRow: FC<{ c: CitationEntry }> = ({ c }) => ( +
+
+ + [{c.source}] + {c.cited && ( + + Cited + + )} +
+
+); + +const CitationsPanel: FC<{ trace: TraceData }> = ({ trace }) => ( +
+ {trace.citations.length === 0 ? ( +

+ No citations available for this trace. +

+ ) : ( + trace.citations.map((c) => ) + )} +
+); + +const TimelinePanel: FC<{ trace: TraceData }> = ({ trace }) => ( +
+ {trace.timeline.map((item, i) => ( +
+
+ Step {item.step} +
+
+ + {formatDuration(item.durationMs / 1000)} + + {item.name} +
+
+ ))} +
+); + +const TokenOverviewPanel: FC<{ trace: TraceData }> = ({ trace }) => { + const usage = trace.tokenUsage; + const nodesWithUsage = trace.toolCalls.filter( + (tc) => tc.usage && tc.usage.total_tokens > 0 + ); + + return ( +
+ {/* Totals */} +
+
+
+ Input Tokens +
+
+ {formatNumber(usage.input_tokens)} +
+
+
+
+ Output Tokens +
+
+ {formatNumber(usage.output_tokens)} +
+
+
+
+ Total Tokens +
+
+ {formatNumber(usage.total_tokens)} +
+
+
+
+ Est. Cost + + + + Cost is estimated based on the model's published per-token pricing. Actual billing may differ. + + + +
+
+ {formatCost(usage.cost)} +
+
estimated
+
+
+ + {/* Per-node breakdown */} +
+
+

Usage by Node

+
+ {nodesWithUsage.length === 0 ? ( +

+ No LLM usage recorded for this trace. +

+ ) : ( +
+ + + + + + + + + + + + + {nodesWithUsage.map((tc) => ( + + + + + + + + + ))} + + + + + + + + + + +
NodeInputOutputTotal + + Est. Cost + + + + Cost is estimated based on the model's published per-token pricing. Actual billing may differ. + + + + LLM Calls
{tc.name} + {formatNumber(tc.usage!.input_tokens)} + + {formatNumber(tc.usage!.output_tokens)} + + {formatNumber(tc.usage!.total_tokens)} + + {formatCost(tc.usage!.cost)} + + {tc.usage!.calls && tc.usage!.calls.length > 0 + ? formatCallerNames(tc.usage!.calls) + : "—"} +
Total + {formatNumber(usage.input_tokens)} + + {formatNumber(usage.output_tokens)} + + {formatNumber(usage.total_tokens)} + + + {formatCost(usage.cost)} + + +
+
+ )} +
+
+ ); +}; + +// ─── Main Page ──────────────────────────────────────────────────────────────── + +interface TraceLogsProps { + // When provided, the component renders inside a Dialog and uses these + // props instead of route params / location state. Closing the dialog + // calls onClose. When omitted, the component renders as a full page + // route (the original ``/trace/:messageId`` behaviour, kept for + // direct-link backward compat). + messageIdProp?: string; + onClose?: () => void; +} + +const TraceLogs: FC = ({ messageIdProp, onClose }) => { + const location = useLocation(); + const navigate = useNavigate(); + const params = useParams<{ messageId: string }>(); + const messageId = messageIdProp || params.messageId; + const isDialog = !!onClose; + + const stateMessage = isDialog ? null : location.state?.message; + const stateUserQuery = isDialog ? null : location.state?.userQuery; + + const [apiData, setApiData] = useState(null); + const [loading, setLoading] = useState(true); + + // Always fetch the trace JSON — the backend writes it on every response, + // so the API is the canonical source. Falls back to the navigation-state + // message only if the API call fails (e.g. trace file got wiped by + // container recreation). Page-mode messages restored from chat history + // don't carry the full ``query_sources.agent_steps`` payload that the + // trace view needs, so we can't trust ``stateMessage`` alone. + useEffect(() => { + if (!messageId) { + setLoading(false); + return; + } + const creds = sessionStorage.getItem("creds"); + // Skip the API call when there are no creds — sending ``Basic null`` + // makes FastAPI's HTTPBasic challenge with ``WWW-Authenticate: Basic`` + // which triggers the browser's native auth popup. Better to show + // "no data" and let the user log back in via the normal flow. + if (!creds) { + setLoading(false); + setApiData(null); + return; + } + setLoading(true); + fetch(`/ui/trace/${messageId}`, { + headers: { Authorization: `Basic ${creds}` }, + }) + .then((res) => { + if (!res.ok) throw new Error("Not found"); + return res.json(); + }) + .then((data) => setApiData(data)) + .catch(() => setApiData(null)) + .finally(() => setLoading(false)); + }, [messageId]); + + const message = apiData ? { + content: apiData.natural_language_response, + response_time: apiData.response_time, + response_type: apiData.response_type, + query_sources: apiData.query_sources, + } : stateMessage; + const userQuery = apiData?.user_query || stateUserQuery; + + const trace = useMemo( + () => (message ? buildTraceFromMessage(message, userQuery) : null), + [message, userQuery] + ); + + const handleBack = () => { + if (onClose) { + onClose(); + return; + } + if (window.history.length > 1) { + navigate(-1); + } else { + navigate("/chat"); + } + }; + + const handleDownload = () => { + if (!trace) return; + const blob = new Blob([JSON.stringify(trace, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `trace_${trace.sessionId}.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const wrap = (inner: JSX.Element) => { + if (isDialog) { + return ( + !o && onClose && onClose()}> + e.preventDefault()} + onOpenAutoFocus={(e) => e.preventDefault()} + > + {inner} + + + ); + } + return
{inner}
; + }; + + if (loading) { + return wrap( +
+

Loading trace data...

+
+ ); + } + + if (!trace) { + return wrap( +
+

Trace data not found.

+
+ ); + } + + return wrap( + <> + {/* Header — only the title in dialog mode (Download moves to the + footer alongside Close), so nothing constrains dialog width. + Page mode keeps the original sticky flex bar with Back + + Download. DialogTitle / DialogDescription wire up the + accessibility labels Radix expects on a Dialog. */} + {isDialog ? ( + <> + Trace Logs + + Execution trace, citations, and token usage for the selected chat response. + + + ) : ( +
+
+
+ +

Trace Logs

+
+
+ +
+
+
+ )} + +
+ {/* Original Query */} +
+

Original Query

+
+ {trace.originalQuery} +
+
+ + {/* Conversation Context */} +
+

Conversation Context

+
+ {trace.conversationContext.map((line, i) => ( +

+ {line} +

+ ))} +
+
+ + {/* Timing Overview */} +
+

Timing Overview

+ + {/* Timeline bar */} +
+
+
+
+
+
+ Start + {trace.timing.startTime} + {trace.timing.endTime} +
+
+
+ + {/* Tabs */} + + + + Citations + + {trace.citations.length} + + + + + Logs + + + Tool Calls + + {trace.toolCalls.length} + + + + Timeline + + + + Token Overview + {trace.tokenUsage.total_tokens > 0 && ( + + {formatNumber(trace.tokenUsage.total_tokens)} + + )} + + + + + + + + + + + + + + + + + + + + + {/* Footer — Download (primary action style) + Close (outline, + matches other dialogs). Replaces the top-right Download in + dialog mode so the dialog can size to content. */} +
+ {isDialog && ( + + )} + +
+
+ + ); +}; + +export default TraceLogs; diff --git a/graphrag-ui/src/pages/setup/CustomizePrompts.tsx b/graphrag-ui/src/pages/setup/CustomizePrompts.tsx index d16fe59..92142e6 100644 --- a/graphrag-ui/src/pages/setup/CustomizePrompts.tsx +++ b/graphrag-ui/src/pages/setup/CustomizePrompts.tsx @@ -6,18 +6,27 @@ import ConfigScopeToggle from "@/components/ConfigScopeToggle"; import { useRoles } from "@/hooks/useRoles"; import { useLocation } from "react-router-dom"; +// Ordered to follow the lifecycle of a graph: setup → ingest → rebuild +// → query. The Customize Prompts page lists them in the same order so +// admins read them top-down in the order they fire. +// +// ``query_generation`` (map_question_to_schema) is intentionally not +// listed here — Query Guidance now covers its only end-user-facing +// customization need (domain hints + examples). The underlying prompt +// is still available on disk and editable via direct API for advanced +// use cases. const ALL_PROMPT_TYPES = [ - { id: "chatbot_response", name: "Chatbot Responses", description: "Customize how the chatbot responds to user questions" }, - { id: "entity_relationship", name: "Entity Relationships", description: "Configure entity and relationship extraction from document chunks" }, - { id: "community_summarization", name: "Community Summarization", description: "Define how community summaries are generated" }, - { id: "query_generation", name: "Schema Instructions", description: "Configure instructions for schema filtering and schema generation" }, + { id: "schema_extraction", name: "Schema Extraction", description: "Rules the LLM follows when proposing a domain schema from sample documents (Initialize Graph dialog)." }, + { id: "entity_relationship", name: "Entity Relationships", description: "Extract entities and relationships from document chunks during ingest." }, + { id: "community_summarization", name: "Community Summarization", description: "Summarize each community after Louvain detection during rebuild." }, + { id: "query_guidance", name: "Query Guidance", description: "Free-form domain hints and example mappings — injected into question-to-schema, generate-function, generate-cypher, and generate-gsql prompts. Empty by default. Max 8000 characters." }, + { id: "chatbot_response", name: "Chatbot Responses", description: "How the chatbot composes the final answer to the user from retrieved context." }, ]; const CustomizePrompts = () => { const location = useLocation(); const { isSuperuser, isGlobalDesigner } = useRoles(location.pathname); const graphOnly = !isSuperuser && !isGlobalDesigner; - const [configuredProvider, setConfiguredProvider] = useState(""); const [isLoading, setIsLoading] = useState(true); const [expandedPrompt, setExpandedPrompt] = useState(null); // Only the prompt types returned by the backend (filtered by access level) @@ -29,14 +38,18 @@ const CustomizePrompts = () => { entity_relationship: "", community_summarization: "", query_generation: "", + schema_extraction: "", + query_guidance: "", }); - + // Template variables that should not be edited (stored separately) const [promptTemplates, setPromptTemplates] = useState({ chatbot_response: "", entity_relationship: "", community_summarization: "", query_generation: "", + schema_extraction: "", + query_guidance: "", }); // Only render prompt types the backend returned for this user @@ -126,6 +139,12 @@ const CustomizePrompts = () => { query_generation: data.prompts.query_generation?.editable_content !== undefined ? data.prompts.query_generation.editable_content : (typeof data.prompts.query_generation === 'string' ? data.prompts.query_generation : ""), + schema_extraction: data.prompts.schema_extraction?.editable_content !== undefined + ? data.prompts.schema_extraction.editable_content + : (typeof data.prompts.schema_extraction === 'string' ? data.prompts.schema_extraction : ""), + query_guidance: data.prompts.query_guidance?.editable_content !== undefined + ? data.prompts.query_guidance.editable_content + : (typeof data.prompts.query_guidance === 'string' ? data.prompts.query_guidance : ""), }); // Store template variables separately @@ -134,22 +153,11 @@ const CustomizePrompts = () => { entity_relationship: data.prompts.entity_relationship?.template_variables || "", community_summarization: data.prompts.community_summarization?.template_variables || "", query_generation: data.prompts.query_generation?.template_variables || "", + schema_extraction: data.prompts.schema_extraction?.template_variables || "", + query_guidance: data.prompts.query_guidance?.template_variables || "", }); - - // Set configured provider - const providerMap: Record = { - openai: "OpenAI", - azure: "Azure OpenAI", - genai: "Google GenAI (Gemini)", - vertexai: "Google Vertex AI", - bedrock: "AWS Bedrock", - ollama: "Ollama", - }; - const provider = data.configured_provider?.toLowerCase() || "openai"; - setConfiguredProvider(providerMap[provider] || data.configured_provider || "OpenAI"); } catch (error) { console.error("Error loading prompts:", error); - setConfiguredProvider("OpenAI"); } finally { setIsLoading(false); } @@ -180,6 +188,26 @@ const CustomizePrompts = () => { } }, [graphOnly]); + // Stay in sync when another component (Bot, Refresh dialog, + // Ingest dialog) changes the shared selectedGraph. The prompts + // are scoped per graph, so a change triggers a re-fetch too. + useEffect(() => { + const handler = () => { + const next = sessionStorage.getItem("selectedGraph") || ""; + if (next === selectedGraph) return; + setSelectedGraph(next); + if (next) { + setConfigScope("graph"); + fetchPrompts(next); + } else if (!graphOnly) { + setConfigScope("global"); + fetchPrompts(""); + } + }; + window.addEventListener("graphrag:selectedGraph", handler); + return () => window.removeEventListener("graphrag:selectedGraph", handler); + }, [selectedGraph, graphOnly]); + return (
@@ -232,28 +260,6 @@ const CustomizePrompts = () => {
- {/* Configured Provider - Read Only */} -
- -
- - {isLoading && ( -
- -
- )} -
-

- Prompts are configured for your currently active LLM provider. Change provider in Server Configuration. -

-
- {/* Save Message */} {saveMessage && (
{ const [numSeenMin, setNumSeenMin] = useState("2"); const [communityLevel, setCommunityLevel] = useState("2"); const [docOnly, setDocOnly] = useState(false); + const [enableRouterFallback, setEnableRouterFallback] = useState(true); - // Advanced ingestion settings + // Collapsible section toggles (Configuration Scope and General Settings + // are always shown). Advanced Ingestion stays collapsed by default — + // matches the prior behavior. + const [showChunker, setShowChunker] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); + const [showSchema, setShowSchema] = useState(false); + const [showEndpoints, setShowEndpoints] = useState(false); const [loadBatchSize, setLoadBatchSize] = useState("500"); const [upsertDelay, setUpsertDelay] = useState("0"); const [maxConcurrency, setMaxConcurrency] = useState("10"); + // Schema-aware initialization (Phase 1 sample-doc path) + const [schemaMaxSampleFiles, setSchemaMaxSampleFiles] = useState("5"); + const [schemaMaxTotalMb, setSchemaMaxTotalMb] = useState("50"); + // Dynamic schema behavior at extraction and retrieval time + const [strictMode, setStrictMode] = useState(false); + // Tri-state: "auto" leaves the key unset (server picks based on whether + // a domain schema exists); "true"/"false" force the behavior. + const [retrievalIncludeEntity, setRetrievalIncludeEntity] = useState<"auto" | "true" | "false">("auto"); + // Chunker-specific settings const [chunkSize, setChunkSize] = useState(""); const [overlapSize, setOverlapSize] = useState(""); @@ -72,9 +87,15 @@ const GraphRAGConfig = () => { setNumSeenMin(String(graphragConfig.num_seen_min ?? 2)); setCommunityLevel(String(graphragConfig.community_level ?? 2)); setDocOnly(graphragConfig.doc_only ?? false); + setEnableRouterFallback(graphragConfig.enable_router_fallback ?? true); setLoadBatchSize(String(graphragConfig.load_batch_size ?? 500)); setUpsertDelay(String(graphragConfig.upsert_delay ?? 0)); setMaxConcurrency(String(graphragConfig.default_concurrency ?? 10)); + setSchemaMaxSampleFiles(String(graphragConfig.schema_max_sample_files ?? 5)); + setSchemaMaxTotalMb(String(graphragConfig.schema_max_total_mb ?? 50)); + setStrictMode(graphragConfig.strict_mode ?? false); + const rie = graphragConfig.retrieval_include_entity; + setRetrievalIncludeEntity(rie === undefined || rie === null ? "auto" : rie ? "true" : "false"); const chunkerConfig = graphragConfig.chunker_config || {}; setChunkSize(String(chunkerConfig.chunk_size ?? "")); @@ -88,43 +109,79 @@ const GraphRAGConfig = () => { setIsLoading(true); const effectiveScope = scope ?? configScope; const effectiveGraph = graphname ?? selectedGraph; - try { - const creds = sessionStorage.getItem("creds"); - const params = new URLSearchParams(); - if (effectiveGraph) params.set("graphname", effectiveGraph); - if (effectiveScope === "graph") params.set("scope", "graph"); - const queryString = params.toString() ? `?${params.toString()}` : ""; - const response = await fetch(`/ui/config${queryString}`, { - headers: { Authorization: `Basic ${creds}` }, - }); - - if (!response.ok) { - throw new Error("Failed to fetch configuration"); - } - - const data = await response.json(); - - const deepCopy = (obj: any) => JSON.parse(JSON.stringify(obj || {})); - loadedGlobalConfig.current = deepCopy(data.graphrag_config); + const creds = sessionStorage.getItem("creds"); + const params = new URLSearchParams(); + if (effectiveGraph) params.set("graphname", effectiveGraph); + if (effectiveScope === "graph") params.set("scope", "graph"); + const queryString = params.toString() ? `?${params.toString()}` : ""; + const url = `/ui/config${queryString}`; + + // Transient backend failures (cold start, brief upstream timeouts via + // nginx, momentary 502/503/504) are common right after a service + // restart and produced the intermittent "Failed to fetch configuration" + // on this page. Retry a few times with backoff before surfacing an + // error to the user. Auth failures (401/403) and 4xx are not retried. + const shouldRetry = (status: number | null, err: unknown) => { + if (err && status === null) return true; // network error + if (status !== null && status >= 500) return true; + return false; + }; + + const maxAttempts = 3; + let lastErr: any = null; + let lastStatus: number | null = null; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const response = await fetch(url, { + headers: { Authorization: `Basic ${creds}` }, + }); + lastStatus = response.status; + if (!response.ok) { + if (attempt < maxAttempts && shouldRetry(response.status, null)) { + await new Promise((r) => setTimeout(r, 500 * attempt)); + continue; + } + throw new Error(`HTTP ${response.status}`); + } - if (effectiveScope === "graph" && data.graphrag_overrides) { - loadedGraphOverrides.current = deepCopy(data.graphrag_overrides); - setGraphOverrides(data.graphrag_overrides); - // Show per-graph values: merge global + overrides for display - const merged = { ...data.graphrag_config, ...data.graphrag_overrides }; - applyGraphragConfig(merged); - } else { - loadedGraphOverrides.current = {}; - setGraphOverrides({}); - applyGraphragConfig(data.graphrag_config); + const data = await response.json(); + + const deepCopy = (obj: any) => JSON.parse(JSON.stringify(obj || {})); + loadedGlobalConfig.current = deepCopy(data.graphrag_config); + + if (effectiveScope === "graph" && data.graphrag_overrides) { + loadedGraphOverrides.current = deepCopy(data.graphrag_overrides); + setGraphOverrides(data.graphrag_overrides); + // Show per-graph values: merge global + overrides for display + const merged = { ...data.graphrag_config, ...data.graphrag_overrides }; + applyGraphragConfig(merged); + } else { + loadedGraphOverrides.current = {}; + setGraphOverrides({}); + applyGraphragConfig(data.graphrag_config); + } + // Clear any prior transient error banner on success. + setMessage(""); + setMessageType(""); + setIsLoading(false); + return; + } catch (error: any) { + lastErr = error; + if (attempt < maxAttempts && shouldRetry(lastStatus, error)) { + await new Promise((r) => setTimeout(r, 500 * attempt)); + continue; + } + break; } - } catch (error: any) { - console.error("Error fetching config:", error); - setMessage(`Failed to load configuration: ${error.message}`); - setMessageType("error"); - } finally { - setIsLoading(false); } + + console.error("Error fetching config:", lastErr, "status=", lastStatus); + setMessage( + `Failed to load configuration${lastStatus ? ` (HTTP ${lastStatus})` : ""}. Please retry.` + ); + setMessageType("error"); + setIsLoading(false); }; const handleSave = async () => { @@ -154,10 +211,21 @@ const GraphRAGConfig = () => { num_seen_min: parseInt(numSeenMin), community_level: parseInt(communityLevel), doc_only: docOnly, + enable_router_fallback: enableRouterFallback, load_batch_size: parseInt(loadBatchSize), upsert_delay: parseInt(upsertDelay), default_concurrency: parseInt(maxConcurrency), + schema_max_sample_files: parseInt(schemaMaxSampleFiles), + schema_max_total_mb: parseInt(schemaMaxTotalMb), + strict_mode: strictMode, }; + // retrieval_include_entity: only include the key when the user has + // picked an explicit value. "auto" should leave it unset so the + // server-side fallback (False with domain schema, True otherwise) + // applies. + if (retrievalIncludeEntity !== "auto") { + currentConfig.retrieval_include_entity = retrievalIncludeEntity === "true"; + } // Display defaults — used to avoid saving values the user never changed const displayDefaults: Record = { @@ -170,9 +238,13 @@ const GraphRAGConfig = () => { num_seen_min: 2, community_level: 2, doc_only: false, + enable_router_fallback: true, load_batch_size: 500, upsert_delay: 0, default_concurrency: 10, + schema_max_sample_files: 5, + schema_max_total_mb: 50, + strict_mode: false, }; // Determine which config to diff against based on scope @@ -455,19 +527,52 @@ const GraphRAGConfig = () => { Retrieve original documents instead of document chunks in results

+ +
+
+ setEnableRouterFallback(e.target.checked)} + /> + +
+

+ Fall back to vector search when structured-data retrieval fails. +

+
{/* Chunker Settings */}
-

- Chunker Settings -

-

- Configure document chunking for ingestion -

+ + {!showChunker && ( +

+ Configure document chunking for ingestion. +

+ )} -
+ {showChunker && ( +
+

+ Configure document chunking for ingestion. +

@@ -595,6 +700,7 @@ const GraphRAGConfig = () => {
+ )}
{message && ( @@ -649,7 +755,7 @@ const GraphRAGConfig = () => { onChange={(e) => setLoadBatchSize(e.target.value)} />

- Vertices per upsert batch + Number of vertices written per batch.

@@ -691,17 +797,167 @@ const GraphRAGConfig = () => { )}
- {/* Service Endpoints (global only) */} - {configScope !== "graph" && ( -
-

- Service Endpoints + {/* Schema extraction (sample-doc proposal path) + dynamic schema + behavior (how domain types are enforced and retrieved). */} +
+ + {!showSchema && ( +

+ Sample-doc schema extraction limits and runtime behavior of + the domain schema. +

+ )} + + {showSchema && ( +
+

+ Controls for the schema-extraction sample-doc workflow and the + runtime behavior of the dynamic (domain) schema during entity + extraction and retrieval. +

+ + {/* Schema extraction sub-section */} +
+

+ Schema Extraction (Sample Documents) +

+

+ Limits for the Generate from sample documents path on + the Initialize Knowledge Graph dialog. +

+
+
+ + setSchemaMaxSampleFiles(e.target.value)} + /> +

+ Maximum number of sample documents per schema-extraction run +

+
+
+ + setSchemaMaxTotalMb(e.target.value)} + /> +

+ Combined upload cap across all sample files +

+
+
+
+ + {/* Dynamic schema runtime sub-section */} +
+

+ Dynamic Schema Runtime +

+

+ How the domain schema is enforced during entity extraction and + used by retrievers at query time.

+
+
+ setStrictMode(e.target.checked)} + /> + +
+

+ Drop extracted entities and relationships that don't match + the domain schema. +

+
+ +
+ + +

+ Include generic entities in retrieval alongside domain types. +

+
+
+
+
+ )} +
+ + {/* Service Endpoints (global only) */} + {configScope !== "graph" && ( +
+ + {!showEndpoints && ( +

+ Internal service URLs (global only). +

+ )} + + {showEndpoints && ( +
+

+ Configure internal service URLs. These are global settings and cannot be overridden per graph. +

+ )}
)} diff --git a/graphrag-ui/src/pages/setup/IngestGraph.tsx b/graphrag-ui/src/pages/setup/IngestGraph.tsx index db9677a..69197be 100644 --- a/graphrag-ui/src/pages/setup/IngestGraph.tsx +++ b/graphrag-ui/src/pages/setup/IngestGraph.tsx @@ -19,7 +19,7 @@ import { SelectValue, } from "@/components/ui/select"; import { useConfirm } from "@/hooks/useConfirm"; -import { pauseIdleTimer, resumeIdleTimer } from "@/hooks/useIdleTimeout"; +import { pingIdleTimer } from "@/hooks/useIdleTimeout"; interface IngestGraphProps { isModal?: boolean; @@ -46,7 +46,11 @@ const formatBytes = (bytes: number) => { const IngestGraph: React.FC = ({ isModal = false }) => { const [confirm, confirmDialog] = useConfirm(); const [availableGraphs, setAvailableGraphs] = useState([]); - const [ingestGraphName, setIngestGraphName] = useState(""); + // Seed from the shared ``selectedGraph`` so the dropdown matches + // whatever was last picked elsewhere (KGAdmin refresh, Bot, etc.). + const [ingestGraphName, setIngestGraphName] = useState( + sessionStorage.getItem("selectedGraph") || "" + ); const [selectedFiles, setSelectedFiles] = useState(null); const fileInputRef = useRef(null); const [uploadedFiles, setUploadedFiles] = useState([]); @@ -488,17 +492,45 @@ const IngestGraph: React.FC = ({ isModal = false }) => { const creds = sessionStorage.getItem("creds"); const folderPath = sourceType === "uploaded" ? `uploads/${ingestGraphName}` : `downloaded_files_cloud/${ingestGraphName}`; - // Use existing ingestJobData if available, otherwise construct from folder path - const jobData = ingestJobData || { - load_job_id: "load_documents_content_json", - data_source_id: { - data_source: "server", - data_source_config: { data_path: folderPath }, - loader_config: {}, - file_format: "multi" - }, - data_path: folderPath, - }; + // If no cached job from a prior create_ingest, run it now. The + // backend's /ingest endpoint expects the data_source_id dict + // shape that create_ingest emits (with the resolved JSONL temp + // folder at top-level ``data_path``); building a fallback in + // the UI loses that contract. + let jobData = ingestJobData; + if (!jobData) { + setIngestMessage("Step 1/2: Preparing ingest job..."); + const createResp = await fetch( + `/ui/${ingestGraphName}/create_ingest`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Basic ${creds}`, + }, + body: JSON.stringify({ + data_source: "server", + data_source_config: { data_path: folderPath }, + loader_config: {}, + file_format: "multi", + }), + } + ); + if (!createResp.ok) { + const err = await createResp.json(); + throw new Error( + err.detail || `Failed to create ingest job: ${createResp.statusText}` + ); + } + const createData = await createResp.json(); + jobData = { + load_job_id: createData.load_job_id, + data_source_id: createData.data_source_id, + data_path: createData.data_path || folderPath, + }; + setIngestJobData(jobData); + setIngestMessage("Step 2/2: Loading documents into knowledge graph..."); + } const ingestResponse = await fetch(`/ui/${ingestGraphName}/ingest`, { method: "POST", @@ -861,27 +893,62 @@ const IngestGraph: React.FC = ({ isModal = false }) => { } }; - // Pause idle timer while ingestion is running + // Keep the idle timer alive while any long-running upload / conversion + // / ingest is in flight. Ping every 60s — actively resets the idle + // countdown instead of relying on a pause/resume event sequence that + // can drift in nested-modal contexts. useEffect(() => { - if (isIngesting) { - pauseIdleTimer(); - } else { - resumeIdleTimer(); - } - }, [isIngesting]); - - // Load available graphs from sessionStorage on mount + if (!(isUploading || isProcessingFiles || isIngesting)) return; + pingIdleTimer(); + const id = setInterval(() => pingIdleTimer(), 60_000); + return () => clearInterval(id); + }, [isUploading, isProcessingFiles, isIngesting]); + + // Load available graphs. Seed from sessionStorage for instant render, + // then refresh from /ui/list_graphs so newly-initialized graphs show + // up without a re-login. useEffect(() => { const store = JSON.parse(sessionStorage.getItem("site") || "{}"); if (store.graphs && Array.isArray(store.graphs)) { setAvailableGraphs(store.graphs); - // Auto-select first graph if available if (store.graphs.length > 0 && !ingestGraphName) { setIngestGraphName(store.graphs[0]); } } + const creds = sessionStorage.getItem("creds"); + if (!creds) return; + fetch("/ui/list_graphs", { + headers: { Authorization: `Basic ${creds}` }, + }) + .then((r) => (r.ok ? r.json() : null)) + .then((data) => { + if (!data || !Array.isArray(data.graphs)) return; + const graphs: string[] = data.graphs; + setAvailableGraphs(graphs); + const cached = JSON.parse(sessionStorage.getItem("site") || "{}"); + cached.graphs = graphs; + sessionStorage.setItem("site", JSON.stringify(cached)); + if (graphs.length > 0 && !ingestGraphName) { + setIngestGraphName(graphs[0]); + } + }) + .catch(() => { + /* keep cached value; not fatal */ + }); }, []); + // Keep the Ingest dialog's graph picker in sync with the shared + // ``selectedGraph`` so changing the graph elsewhere (KGAdmin + // refresh, Bot) immediately reflects here. + useEffect(() => { + const handler = () => { + const next = sessionStorage.getItem("selectedGraph") || ""; + if (next && next !== ingestGraphName) setIngestGraphName(next); + }; + window.addEventListener("graphrag:selectedGraph", handler); + return () => window.removeEventListener("graphrag:selectedGraph", handler); + }, [ingestGraphName]); + // Load files when graph name changes useEffect(() => { if (ingestGraphName) { @@ -914,7 +981,11 @@ const IngestGraph: React.FC = ({ isModal = false }) => { setGraphName(e.target.value)} - disabled={isInitializing} - className="dark:border-[#3D3D3D] dark:bg-shadeA" - onKeyDown={(e) => { - if (e.key === "Enter" && !isInitializing) { - handleInitializeGraph(); +
+ {/* Wrapper carries the visual styling (matching the + SelectTrigger used by other graph selectors); the + inner is borderless/transparent so its + native text rendering can't clip the underscore + glyph against the bottom border. */} +
+ > + { + setGraphName(e.target.value); + if (!graphNameDropdownOpen) setGraphNameDropdownOpen(true); + }} + onFocus={() => setGraphNameDropdownOpen(true)} + disabled={isInitializing || isExtractingSchema} + className="flex-1 bg-transparent outline-none border-0 p-0 text-sm text-black dark:text-white placeholder:text-muted-foreground disabled:opacity-50" + // appearance:none disables Chrome's native input + // rendering (which on macOS clips descenders like + // '_' even when the wrapper has plenty of room). + // lineHeight + a slightly taller wrapper finish + // the job of making the underscore glyph fully + // visible in long names. + style={{ + WebkitAppearance: "none", + appearance: "none", + lineHeight: "1.5", + }} + onKeyDown={(e) => { + if (e.key === "Enter" && !isInitializing && !isExtractingSchema) { + handleInitializeGraph(); + } else if (e.key === "Escape") { + setGraphNameDropdownOpen(false); + } + }} + /> + +
+ {graphNameDropdownOpen && (() => { + const q = graphName.trim().toLowerCase(); + const filtered = q + ? availableGraphs.filter((g) => g.toLowerCase().includes(q)) + : availableGraphs; + if (filtered.length === 0) return null; + return ( +
+ {filtered.map((g) => ( + + ))} +
+ ); + })()} +
+

+ +
+ +
+ + + +
+ + {schemaSource === "none" && ( +
+ {precheckMessage ? ( +

+ {precheckMessage} +

+ ) : ( +

+ Click Check existing schema to verify the graph before initializing. +

+ )} + +
+ )} + + {schemaSource === "samples" && ( +
+ +

+ Up to {maxSampleFiles} files, ≤10 MB each, ≤{maxTotalMb} MB total. + Selected: {sampleFiles.length} + {sampleFiles.length > 0 && + ` (${(sampleFiles.reduce((s, f) => s + f.size, 0) / (1024 * 1024)).toFixed(1)} MB)`} +

+
+

+ Suggested types (optional). Vertex format:{" "} + Name{" "} + or{" "} + Name: description. + Edge format adds an optional endpoint pair:{" "} + Name (From -> To){" "} + or{" "} + Name (From -> To): description. + Press Enter or comma to add each entry. +

+
+ + +
+
+ + +
+
+ + + {draftProposal && ( +
+
+

+ Review and edit the draft below. Each vertex auto-gets a primary + key id (STRING) — you don't need to add it. Click + Initialize when ready. +

+ +
+ + {/* Vertex types */} +
+
+

+ Vertex types ({draftProposal.vertices.length}) +

+
+ + +
+
+
+ {draftProposal.vertices.map((v, vIdx) => ( +
+
+ + + setDraftProposal((p) => + p + ? { + ...p, + vertices: p.vertices.map((vv, i) => + i === vIdx ? { ...vv, name: e.target.value } : vv + ), + } + : p + ) + } + disabled={isInitializing || isExtractingSchema} + className="flex-1 h-8 text-sm dark:border-[#3D3D3D] dark:bg-shadeA" + /> + {collapsedVertices.has(vIdx) && ( + + {v.attributes.length} attr{v.attributes.length === 1 ? "" : "s"} + + )} + +
+ {!collapsedVertices.has(vIdx) && (<> + + setDraftProposal((p) => + p + ? { + ...p, + vertices: p.vertices.map((vv, i) => + i === vIdx + ? { ...vv, description: e.target.value } + : vv + ), + } + : p + ) + } + disabled={isInitializing || isExtractingSchema} + className="h-8 text-sm dark:border-[#3D3D3D] dark:bg-shadeA" + /> +
+ Attributes ({v.attributes.length}); primary key id auto-added + {attributesCollapsed && ( + — collapsed + )} +
+ {!attributesCollapsed && v.attributes.map((a, aIdx) => ( +
+ + setDraftProposal((p) => + p + ? { + ...p, + vertices: p.vertices.map((vv, i) => + i === vIdx + ? { + ...vv, + attributes: vv.attributes.map( + (aa, j) => + j === aIdx + ? { + ...aa, + // Auto-replace whitespace + // with underscores so the + // displayed name always + // matches the GSQL + // identifier that will be + // emitted (whitespace is + // not a valid char in + // GSQL idents). + name: e.target.value.replace( + /\s+/g, + "_" + ), + } + : aa + ), + } + : vv + ), + } + : p + ) + } + disabled={isInitializing || isExtractingSchema} + className="flex-1 h-7 text-xs font-mono dark:border-[#3D3D3D] dark:bg-shadeA" + /> + + +
+ ))} + {!attributesCollapsed && ( + + )} + )} +
+ ))} +
+
+ + {/* Edge types */} +
+
+

+ Edge types ({draftProposal.edges.length}) +

+
+ + +
+
+
+ {draftProposal.edges.map((e, eIdx) => ( +
+
+ + + setDraftProposal((p) => + p + ? { + ...p, + edges: p.edges.map((ee, i) => + i === eIdx + ? { ...ee, name: ev.target.value } + : ee + ), + } + : p + ) + } + disabled={isInitializing || isExtractingSchema} + className="flex-1 h-8 text-sm dark:border-[#3D3D3D] dark:bg-shadeA" + /> + {collapsedEdges.has(eIdx) && ( + + {e.pairs.length} pair{e.pairs.length === 1 ? "" : "s"}, {e.attributes.length} attr + {e.attributes.length === 1 ? "" : "s"} + + )} + +
+ {!collapsedEdges.has(eIdx) && (<> + + setDraftProposal((p) => + p + ? { + ...p, + edges: p.edges.map((ee, i) => + i === eIdx + ? { ...ee, description: ev.target.value } + : ee + ), + } + : p + ) + } + disabled={isInitializing || isExtractingSchema} + className="h-8 text-sm dark:border-[#3D3D3D] dark:bg-shadeA" + /> +
+ Endpoints (FROM → TO): +
+ {e.pairs.map((pair, pIdx) => ( +
+ + setDraftProposal((p) => + p + ? { + ...p, + edges: p.edges.map((ee, i) => + i === eIdx + ? { + ...ee, + pairs: ee.pairs.map((pr, j) => + j === pIdx + ? [ev.target.value, pr[1]] + : pr + ) as Array<[string, string]>, + } + : ee + ), + } + : p + ) + } + disabled={isInitializing || isExtractingSchema} + className="flex-1 h-7 text-xs dark:border-[#3D3D3D] dark:bg-shadeA" + /> + + + setDraftProposal((p) => + p + ? { + ...p, + edges: p.edges.map((ee, i) => + i === eIdx + ? { + ...ee, + pairs: ee.pairs.map((pr, j) => + j === pIdx + ? [pr[0], ev.target.value] + : pr + ) as Array<[string, string]>, + } + : ee + ), + } + : p + ) + } + disabled={isInitializing || isExtractingSchema} + className="flex-1 h-7 text-xs dark:border-[#3D3D3D] dark:bg-shadeA" + /> + +
+ ))} + +
+ Attributes ({e.attributes.length}, optional) + {attributesCollapsed && ( + — collapsed + )} +
+ {!attributesCollapsed && e.attributes.map((a, aIdx) => ( +
+ + setDraftProposal((p) => + p + ? { + ...p, + edges: p.edges.map((ee, i) => + i === eIdx + ? { + ...ee, + attributes: ee.attributes.map( + (aa, j) => + j === aIdx + ? { + ...aa, + // Auto-replace whitespace + // with underscores — + // GSQL idents can't have + // spaces, and rendering + // them as `_` makes the + // visual unambiguous. + name: ev.target.value.replace( + /\s+/g, + "_" + ), + } + : aa + ), + } + : ee + ), + } + : p + ) + } + disabled={isInitializing || isExtractingSchema} + className="flex-1 h-7 text-xs font-mono dark:border-[#3D3D3D] dark:bg-shadeA" + /> + + +
+ ))} + {!attributesCollapsed && ( + + )} + )} +
+ ))} +
+
+
+ )} +
+ )} + + {schemaSource === "gsql" && ( +
+

+ Paste TigerGraph GSQL ADD VERTEX / + ADD [UN]DIRECTED EDGE statements (or output of + gsql ls). If you don't include a + PRIMARY_ID, the system auto-adds + PRIMARY_ID id STRING. Lines that don't match + VERTEX / EDGE patterns are silently ignored. +

+